@ahmed109/gatekeeper-express 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +91 -2
- package/dist/index.cjs +147 -0
- package/dist/index.d.cts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +119 -0
- package/package.json +61 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ahmed Tamer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,92 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ahmed109/gatekeeper-express
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Express middleware for authorization and feature flags with customizable callbacks.
|
|
4
|
+
|
|
5
|
+
Part of the **Gatekeeper** toolkit. Built on top of [`@ahmed109/gatekeeper-core`](https://www.npmjs.com/package/@ahmed109/gatekeeper-core).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ahmed109/gatekeeper-express @ahmed109/gatekeeper-core
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @ahmed109/gatekeeper-express @ahmed109/gatekeeper-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Peer Dependencies
|
|
16
|
+
|
|
17
|
+
- `express`: `>=4.18.0`
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Route Guards**: `requirePermission()`, `requireAnyPermission()`, and `requireFeatureFlag()`.
|
|
22
|
+
- **Customizable Auth Callbacks**: Route-level and global `onUnauthorized(req, res)` and `onForbidden(req, res, missing)`.
|
|
23
|
+
- **Safe Fallbacks**: Returns standard JSON 401/403 responses if custom callbacks are not supplied.
|
|
24
|
+
- **Request Augmentation**: Safely types `req.user`, `req.gatekeeperFlags`, and `req.can(perm)`.
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import express from 'express';
|
|
30
|
+
import {
|
|
31
|
+
gatekeeperMiddleware,
|
|
32
|
+
requirePermission,
|
|
33
|
+
requireFeatureFlag,
|
|
34
|
+
} from '@ahmed109/gatekeeper-express';
|
|
35
|
+
|
|
36
|
+
const app = express();
|
|
37
|
+
app.use(express.json());
|
|
38
|
+
|
|
39
|
+
// Attach authenticated user and active flags in upstream middleware
|
|
40
|
+
app.use((req, _res, next) => {
|
|
41
|
+
req.user = {
|
|
42
|
+
id: 'usr_123',
|
|
43
|
+
name: 'Bob Manager',
|
|
44
|
+
role: 'manager',
|
|
45
|
+
permissions: [],
|
|
46
|
+
};
|
|
47
|
+
req.gatekeeperFlags = {
|
|
48
|
+
'export-reports': true,
|
|
49
|
+
};
|
|
50
|
+
next();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Mount Gatekeeper helper middleware
|
|
54
|
+
app.use(gatekeeperMiddleware());
|
|
55
|
+
|
|
56
|
+
// Protected route: requires 'reports.view' permission
|
|
57
|
+
app.get('/api/reports', requirePermission('reports.view'), (req, res) => {
|
|
58
|
+
res.json({ reports: [] });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Protected route: requires permission AND active feature flag
|
|
62
|
+
app.get(
|
|
63
|
+
'/api/reports/export',
|
|
64
|
+
requirePermission('reports.view'),
|
|
65
|
+
requireFeatureFlag('export-reports'),
|
|
66
|
+
(req, res) => {
|
|
67
|
+
res.send('id,title,date\n1,Audit,2026-10-17');
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Custom forbidden handler
|
|
72
|
+
app.post(
|
|
73
|
+
'/api/admin/action',
|
|
74
|
+
requirePermission('settings.manage', {
|
|
75
|
+
onForbidden: (req, res, missing) => {
|
|
76
|
+
res.status(403).json({
|
|
77
|
+
customError: 'FORBIDDEN_CUSTOM',
|
|
78
|
+
missingPermissions: missing,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
(req, res) => {
|
|
83
|
+
res.json({ success: true });
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
app.listen(3001);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT © [Ahmed Tamer](https://github.com/Ahmed1092002)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FORBIDDEN_PAYLOAD: () => FORBIDDEN_PAYLOAD,
|
|
24
|
+
UNAUTHORIZED_PAYLOAD: () => UNAUTHORIZED_PAYLOAD,
|
|
25
|
+
gatekeeperMiddleware: () => gatekeeperMiddleware,
|
|
26
|
+
requireAnyPermission: () => requireAnyPermission,
|
|
27
|
+
requireFeatureFlag: () => requireFeatureFlag,
|
|
28
|
+
requirePermission: () => requirePermission
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/middleware.ts
|
|
33
|
+
var import_gatekeeper_core = require("@ahmed109/gatekeeper-core");
|
|
34
|
+
var UNAUTHORIZED_PAYLOAD = {
|
|
35
|
+
error: "UNAUTHORIZED",
|
|
36
|
+
message: "Authentication required."
|
|
37
|
+
};
|
|
38
|
+
var FORBIDDEN_PAYLOAD = {
|
|
39
|
+
error: "FORBIDDEN",
|
|
40
|
+
message: "You do not have permission to perform this action."
|
|
41
|
+
};
|
|
42
|
+
function gatekeeperMiddleware(options = {}) {
|
|
43
|
+
return (req, _res, next) => {
|
|
44
|
+
req.gatekeeperOptions = options;
|
|
45
|
+
const extractor = options.getSubject ?? options.getUser;
|
|
46
|
+
const subject = extractor ? extractor(req) : req.subject ?? req.user;
|
|
47
|
+
if (subject) {
|
|
48
|
+
if (!req.user) req.user = subject;
|
|
49
|
+
if (!req.subject) req.subject = subject;
|
|
50
|
+
}
|
|
51
|
+
if (options.getFlags && !req.gatekeeperFlags) {
|
|
52
|
+
req.gatekeeperFlags = options.getFlags(req);
|
|
53
|
+
}
|
|
54
|
+
req.can = (permission, canOptions) => {
|
|
55
|
+
const activeUser = extractor ? extractor(req) : req.subject ?? req.user;
|
|
56
|
+
const resolvedContext = canOptions?.context ?? options.context ?? (options.getContext ? options.getContext(req) : void 0);
|
|
57
|
+
return (0, import_gatekeeper_core.can)(activeUser, permission, {
|
|
58
|
+
roleMap: canOptions?.roleMap ?? options.roleMap,
|
|
59
|
+
policy: canOptions?.policy ?? options.policy,
|
|
60
|
+
context: resolvedContext
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
next();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function requirePermission(permission, options = {}) {
|
|
67
|
+
return async (req, res, next) => {
|
|
68
|
+
const userExtractor = options.getSubject ?? options.getUser ?? req.gatekeeperOptions?.getSubject ?? req.gatekeeperOptions?.getUser;
|
|
69
|
+
const user = userExtractor ? userExtractor(req) : req.subject ?? req.user;
|
|
70
|
+
const roleMap = options.roleMap ?? req.gatekeeperOptions?.roleMap;
|
|
71
|
+
const policy = options.policy ?? req.gatekeeperOptions?.policy;
|
|
72
|
+
const context = options.context ?? (options.getContext ? options.getContext(req) : req.gatekeeperOptions?.context ?? req.gatekeeperOptions?.getContext?.(req));
|
|
73
|
+
if (!user) {
|
|
74
|
+
const onUnauthorized = options.onUnauthorized ?? req.gatekeeperOptions?.onUnauthorized;
|
|
75
|
+
if (onUnauthorized) {
|
|
76
|
+
await onUnauthorized(res, req);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.status(401).json(UNAUTHORIZED_PAYLOAD);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const hasPermission = (0, import_gatekeeper_core.can)(user, permission, { roleMap, policy, context });
|
|
83
|
+
if (!hasPermission) {
|
|
84
|
+
const onForbidden = options.onForbidden ?? req.gatekeeperOptions?.onForbidden;
|
|
85
|
+
if (onForbidden) {
|
|
86
|
+
await onForbidden(res, [permission], req);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
res.status(403).json(FORBIDDEN_PAYLOAD);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
next();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function requireAnyPermission(permissions, options = {}) {
|
|
96
|
+
return async (req, res, next) => {
|
|
97
|
+
const userExtractor = options.getSubject ?? options.getUser ?? req.gatekeeperOptions?.getSubject ?? req.gatekeeperOptions?.getUser;
|
|
98
|
+
const user = userExtractor ? userExtractor(req) : req.subject ?? req.user;
|
|
99
|
+
const roleMap = options.roleMap ?? req.gatekeeperOptions?.roleMap;
|
|
100
|
+
const policy = options.policy ?? req.gatekeeperOptions?.policy;
|
|
101
|
+
const context = options.context ?? (options.getContext ? options.getContext(req) : req.gatekeeperOptions?.context ?? req.gatekeeperOptions?.getContext?.(req));
|
|
102
|
+
if (!user) {
|
|
103
|
+
const onUnauthorized = options.onUnauthorized ?? req.gatekeeperOptions?.onUnauthorized;
|
|
104
|
+
if (onUnauthorized) {
|
|
105
|
+
await onUnauthorized(res, req);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
res.status(401).json(UNAUTHORIZED_PAYLOAD);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const hasAny = (0, import_gatekeeper_core.canAny)(user, permissions, { roleMap, policy, context });
|
|
112
|
+
if (!hasAny) {
|
|
113
|
+
const onForbidden = options.onForbidden ?? req.gatekeeperOptions?.onForbidden;
|
|
114
|
+
if (onForbidden) {
|
|
115
|
+
await onForbidden(res, permissions, req);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
res.status(403).json(FORBIDDEN_PAYLOAD);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
next();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function requireFeatureFlag(flagKey, options = {}) {
|
|
125
|
+
return (req, res, next) => {
|
|
126
|
+
const flagsResolver = options.getFlags ?? req.gatekeeperOptions?.getFlags;
|
|
127
|
+
const flags = flagsResolver ? flagsResolver(req) : req.gatekeeperFlags;
|
|
128
|
+
const enabled = (0, import_gatekeeper_core.isFeatureEnabled)(flags, flagKey);
|
|
129
|
+
if (!enabled) {
|
|
130
|
+
res.status(403).json({
|
|
131
|
+
error: "FORBIDDEN",
|
|
132
|
+
message: `Feature '${flagKey}' is disabled.`
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
next();
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
140
|
+
0 && (module.exports = {
|
|
141
|
+
FORBIDDEN_PAYLOAD,
|
|
142
|
+
UNAUTHORIZED_PAYLOAD,
|
|
143
|
+
gatekeeperMiddleware,
|
|
144
|
+
requireAnyPermission,
|
|
145
|
+
requireFeatureFlag,
|
|
146
|
+
requirePermission
|
|
147
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Request, Response, RequestHandler } from 'express';
|
|
2
|
+
import { AuthorizationSubject, RolePermissionsMap, AuthorizationPolicy, Permission, EvaluateOptions } from '@ahmed109/gatekeeper-core';
|
|
3
|
+
export { AuthorizationResult, FeatureFlag, GatekeeperUser, Permission, Role, RolePermissionsMap } from '@ahmed109/gatekeeper-core';
|
|
4
|
+
|
|
5
|
+
type UnauthorizedHandler = (res: Response, req: Request) => void | Promise<void>;
|
|
6
|
+
type ForbiddenHandler = (res: Response, missingPermissions: Permission[], req: Request) => void | Promise<void>;
|
|
7
|
+
declare global {
|
|
8
|
+
namespace Express {
|
|
9
|
+
interface Request {
|
|
10
|
+
user?: AuthorizationSubject;
|
|
11
|
+
subject?: AuthorizationSubject;
|
|
12
|
+
gatekeeperFlags?: Record<string, boolean>;
|
|
13
|
+
gatekeeperOptions?: GatekeeperMiddlewareOptions;
|
|
14
|
+
can?: (permission: Permission, options?: EvaluateOptions) => boolean;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
interface GatekeeperMiddlewareOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Optional custom role-to-permissions map to use across middleware checks.
|
|
21
|
+
*/
|
|
22
|
+
roleMap?: RolePermissionsMap;
|
|
23
|
+
/**
|
|
24
|
+
* Optional custom user extractor if user is stored in a non-standard property.
|
|
25
|
+
*/
|
|
26
|
+
getUser?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
27
|
+
/**
|
|
28
|
+
* Optional custom subject extractor.
|
|
29
|
+
*/
|
|
30
|
+
getSubject?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
31
|
+
/**
|
|
32
|
+
* Optional feature flag resolver.
|
|
33
|
+
*/
|
|
34
|
+
getFlags?: (req: Request) => Record<string, boolean> | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Optional global policy callback for scoped or attribute checks.
|
|
37
|
+
*/
|
|
38
|
+
policy?: AuthorizationPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* Optional resolver for contextual attributes (e.g. { tenantId: req.params.tenantId }).
|
|
41
|
+
*/
|
|
42
|
+
getContext?: (req: Request) => Record<string, unknown> | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Static context dictionary.
|
|
45
|
+
*/
|
|
46
|
+
context?: Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Custom unauthorized response handler for missing users (defaults to 401 JSON).
|
|
49
|
+
*/
|
|
50
|
+
onUnauthorized?: UnauthorizedHandler;
|
|
51
|
+
/**
|
|
52
|
+
* Custom forbidden response handler for denied permissions (defaults to 403 JSON).
|
|
53
|
+
*/
|
|
54
|
+
onForbidden?: ForbiddenHandler;
|
|
55
|
+
}
|
|
56
|
+
interface PermissionMiddlewareOptions {
|
|
57
|
+
roleMap?: RolePermissionsMap;
|
|
58
|
+
getUser?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
59
|
+
getSubject?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
60
|
+
policy?: AuthorizationPolicy;
|
|
61
|
+
getContext?: (req: Request) => Record<string, unknown> | undefined;
|
|
62
|
+
context?: Record<string, unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* Custom unauthorized response handler overriding global settings.
|
|
65
|
+
*/
|
|
66
|
+
onUnauthorized?: UnauthorizedHandler;
|
|
67
|
+
/**
|
|
68
|
+
* Custom forbidden response handler overriding global settings.
|
|
69
|
+
*/
|
|
70
|
+
onForbidden?: ForbiddenHandler;
|
|
71
|
+
}
|
|
72
|
+
interface FeatureFlagMiddlewareOptions {
|
|
73
|
+
getFlags?: (req: Request) => Record<string, boolean> | undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Standard HTTP 401 response payload.
|
|
78
|
+
*/
|
|
79
|
+
declare const UNAUTHORIZED_PAYLOAD: {
|
|
80
|
+
error: string;
|
|
81
|
+
message: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Standard HTTP 403 response payload.
|
|
85
|
+
*/
|
|
86
|
+
declare const FORBIDDEN_PAYLOAD: {
|
|
87
|
+
error: string;
|
|
88
|
+
message: string;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Global Gatekeeper middleware that attaches utility helpers to the Request object.
|
|
92
|
+
*/
|
|
93
|
+
declare function gatekeeperMiddleware(options?: GatekeeperMiddlewareOptions): RequestHandler;
|
|
94
|
+
/**
|
|
95
|
+
* Express middleware requiring the authenticated user to possess a specific permission.
|
|
96
|
+
*
|
|
97
|
+
* @param permission - The permission required to access the route.
|
|
98
|
+
* @param options - Optional custom role map, user extractor, scoped policy, or failure callbacks.
|
|
99
|
+
*/
|
|
100
|
+
declare function requirePermission(permission: Permission, options?: PermissionMiddlewareOptions): RequestHandler;
|
|
101
|
+
/**
|
|
102
|
+
* Express middleware requiring the authenticated user to possess AT LEAST ONE
|
|
103
|
+
* of the specified permissions.
|
|
104
|
+
*/
|
|
105
|
+
declare function requireAnyPermission(permissions: Permission[], options?: PermissionMiddlewareOptions): RequestHandler;
|
|
106
|
+
/**
|
|
107
|
+
* Express middleware that guards an endpoint behind a feature flag.
|
|
108
|
+
*/
|
|
109
|
+
declare function requireFeatureFlag(flagKey: string, options?: FeatureFlagMiddlewareOptions): RequestHandler;
|
|
110
|
+
|
|
111
|
+
export { FORBIDDEN_PAYLOAD, type FeatureFlagMiddlewareOptions, type ForbiddenHandler, type GatekeeperMiddlewareOptions, type PermissionMiddlewareOptions, UNAUTHORIZED_PAYLOAD, type UnauthorizedHandler, gatekeeperMiddleware, requireAnyPermission, requireFeatureFlag, requirePermission };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Request, Response, RequestHandler } from 'express';
|
|
2
|
+
import { AuthorizationSubject, RolePermissionsMap, AuthorizationPolicy, Permission, EvaluateOptions } from '@ahmed109/gatekeeper-core';
|
|
3
|
+
export { AuthorizationResult, FeatureFlag, GatekeeperUser, Permission, Role, RolePermissionsMap } from '@ahmed109/gatekeeper-core';
|
|
4
|
+
|
|
5
|
+
type UnauthorizedHandler = (res: Response, req: Request) => void | Promise<void>;
|
|
6
|
+
type ForbiddenHandler = (res: Response, missingPermissions: Permission[], req: Request) => void | Promise<void>;
|
|
7
|
+
declare global {
|
|
8
|
+
namespace Express {
|
|
9
|
+
interface Request {
|
|
10
|
+
user?: AuthorizationSubject;
|
|
11
|
+
subject?: AuthorizationSubject;
|
|
12
|
+
gatekeeperFlags?: Record<string, boolean>;
|
|
13
|
+
gatekeeperOptions?: GatekeeperMiddlewareOptions;
|
|
14
|
+
can?: (permission: Permission, options?: EvaluateOptions) => boolean;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
interface GatekeeperMiddlewareOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Optional custom role-to-permissions map to use across middleware checks.
|
|
21
|
+
*/
|
|
22
|
+
roleMap?: RolePermissionsMap;
|
|
23
|
+
/**
|
|
24
|
+
* Optional custom user extractor if user is stored in a non-standard property.
|
|
25
|
+
*/
|
|
26
|
+
getUser?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
27
|
+
/**
|
|
28
|
+
* Optional custom subject extractor.
|
|
29
|
+
*/
|
|
30
|
+
getSubject?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
31
|
+
/**
|
|
32
|
+
* Optional feature flag resolver.
|
|
33
|
+
*/
|
|
34
|
+
getFlags?: (req: Request) => Record<string, boolean> | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Optional global policy callback for scoped or attribute checks.
|
|
37
|
+
*/
|
|
38
|
+
policy?: AuthorizationPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* Optional resolver for contextual attributes (e.g. { tenantId: req.params.tenantId }).
|
|
41
|
+
*/
|
|
42
|
+
getContext?: (req: Request) => Record<string, unknown> | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Static context dictionary.
|
|
45
|
+
*/
|
|
46
|
+
context?: Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Custom unauthorized response handler for missing users (defaults to 401 JSON).
|
|
49
|
+
*/
|
|
50
|
+
onUnauthorized?: UnauthorizedHandler;
|
|
51
|
+
/**
|
|
52
|
+
* Custom forbidden response handler for denied permissions (defaults to 403 JSON).
|
|
53
|
+
*/
|
|
54
|
+
onForbidden?: ForbiddenHandler;
|
|
55
|
+
}
|
|
56
|
+
interface PermissionMiddlewareOptions {
|
|
57
|
+
roleMap?: RolePermissionsMap;
|
|
58
|
+
getUser?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
59
|
+
getSubject?: (req: Request) => AuthorizationSubject | undefined | null;
|
|
60
|
+
policy?: AuthorizationPolicy;
|
|
61
|
+
getContext?: (req: Request) => Record<string, unknown> | undefined;
|
|
62
|
+
context?: Record<string, unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* Custom unauthorized response handler overriding global settings.
|
|
65
|
+
*/
|
|
66
|
+
onUnauthorized?: UnauthorizedHandler;
|
|
67
|
+
/**
|
|
68
|
+
* Custom forbidden response handler overriding global settings.
|
|
69
|
+
*/
|
|
70
|
+
onForbidden?: ForbiddenHandler;
|
|
71
|
+
}
|
|
72
|
+
interface FeatureFlagMiddlewareOptions {
|
|
73
|
+
getFlags?: (req: Request) => Record<string, boolean> | undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Standard HTTP 401 response payload.
|
|
78
|
+
*/
|
|
79
|
+
declare const UNAUTHORIZED_PAYLOAD: {
|
|
80
|
+
error: string;
|
|
81
|
+
message: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Standard HTTP 403 response payload.
|
|
85
|
+
*/
|
|
86
|
+
declare const FORBIDDEN_PAYLOAD: {
|
|
87
|
+
error: string;
|
|
88
|
+
message: string;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Global Gatekeeper middleware that attaches utility helpers to the Request object.
|
|
92
|
+
*/
|
|
93
|
+
declare function gatekeeperMiddleware(options?: GatekeeperMiddlewareOptions): RequestHandler;
|
|
94
|
+
/**
|
|
95
|
+
* Express middleware requiring the authenticated user to possess a specific permission.
|
|
96
|
+
*
|
|
97
|
+
* @param permission - The permission required to access the route.
|
|
98
|
+
* @param options - Optional custom role map, user extractor, scoped policy, or failure callbacks.
|
|
99
|
+
*/
|
|
100
|
+
declare function requirePermission(permission: Permission, options?: PermissionMiddlewareOptions): RequestHandler;
|
|
101
|
+
/**
|
|
102
|
+
* Express middleware requiring the authenticated user to possess AT LEAST ONE
|
|
103
|
+
* of the specified permissions.
|
|
104
|
+
*/
|
|
105
|
+
declare function requireAnyPermission(permissions: Permission[], options?: PermissionMiddlewareOptions): RequestHandler;
|
|
106
|
+
/**
|
|
107
|
+
* Express middleware that guards an endpoint behind a feature flag.
|
|
108
|
+
*/
|
|
109
|
+
declare function requireFeatureFlag(flagKey: string, options?: FeatureFlagMiddlewareOptions): RequestHandler;
|
|
110
|
+
|
|
111
|
+
export { FORBIDDEN_PAYLOAD, type FeatureFlagMiddlewareOptions, type ForbiddenHandler, type GatekeeperMiddlewareOptions, type PermissionMiddlewareOptions, UNAUTHORIZED_PAYLOAD, type UnauthorizedHandler, gatekeeperMiddleware, requireAnyPermission, requireFeatureFlag, requirePermission };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
import {
|
|
3
|
+
can as coreCan,
|
|
4
|
+
canAny as coreCanAny,
|
|
5
|
+
isFeatureEnabled
|
|
6
|
+
} from "@ahmed109/gatekeeper-core";
|
|
7
|
+
var UNAUTHORIZED_PAYLOAD = {
|
|
8
|
+
error: "UNAUTHORIZED",
|
|
9
|
+
message: "Authentication required."
|
|
10
|
+
};
|
|
11
|
+
var FORBIDDEN_PAYLOAD = {
|
|
12
|
+
error: "FORBIDDEN",
|
|
13
|
+
message: "You do not have permission to perform this action."
|
|
14
|
+
};
|
|
15
|
+
function gatekeeperMiddleware(options = {}) {
|
|
16
|
+
return (req, _res, next) => {
|
|
17
|
+
req.gatekeeperOptions = options;
|
|
18
|
+
const extractor = options.getSubject ?? options.getUser;
|
|
19
|
+
const subject = extractor ? extractor(req) : req.subject ?? req.user;
|
|
20
|
+
if (subject) {
|
|
21
|
+
if (!req.user) req.user = subject;
|
|
22
|
+
if (!req.subject) req.subject = subject;
|
|
23
|
+
}
|
|
24
|
+
if (options.getFlags && !req.gatekeeperFlags) {
|
|
25
|
+
req.gatekeeperFlags = options.getFlags(req);
|
|
26
|
+
}
|
|
27
|
+
req.can = (permission, canOptions) => {
|
|
28
|
+
const activeUser = extractor ? extractor(req) : req.subject ?? req.user;
|
|
29
|
+
const resolvedContext = canOptions?.context ?? options.context ?? (options.getContext ? options.getContext(req) : void 0);
|
|
30
|
+
return coreCan(activeUser, permission, {
|
|
31
|
+
roleMap: canOptions?.roleMap ?? options.roleMap,
|
|
32
|
+
policy: canOptions?.policy ?? options.policy,
|
|
33
|
+
context: resolvedContext
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
next();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function requirePermission(permission, options = {}) {
|
|
40
|
+
return async (req, res, next) => {
|
|
41
|
+
const userExtractor = options.getSubject ?? options.getUser ?? req.gatekeeperOptions?.getSubject ?? req.gatekeeperOptions?.getUser;
|
|
42
|
+
const user = userExtractor ? userExtractor(req) : req.subject ?? req.user;
|
|
43
|
+
const roleMap = options.roleMap ?? req.gatekeeperOptions?.roleMap;
|
|
44
|
+
const policy = options.policy ?? req.gatekeeperOptions?.policy;
|
|
45
|
+
const context = options.context ?? (options.getContext ? options.getContext(req) : req.gatekeeperOptions?.context ?? req.gatekeeperOptions?.getContext?.(req));
|
|
46
|
+
if (!user) {
|
|
47
|
+
const onUnauthorized = options.onUnauthorized ?? req.gatekeeperOptions?.onUnauthorized;
|
|
48
|
+
if (onUnauthorized) {
|
|
49
|
+
await onUnauthorized(res, req);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
res.status(401).json(UNAUTHORIZED_PAYLOAD);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const hasPermission = coreCan(user, permission, { roleMap, policy, context });
|
|
56
|
+
if (!hasPermission) {
|
|
57
|
+
const onForbidden = options.onForbidden ?? req.gatekeeperOptions?.onForbidden;
|
|
58
|
+
if (onForbidden) {
|
|
59
|
+
await onForbidden(res, [permission], req);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
res.status(403).json(FORBIDDEN_PAYLOAD);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
next();
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function requireAnyPermission(permissions, options = {}) {
|
|
69
|
+
return async (req, res, next) => {
|
|
70
|
+
const userExtractor = options.getSubject ?? options.getUser ?? req.gatekeeperOptions?.getSubject ?? req.gatekeeperOptions?.getUser;
|
|
71
|
+
const user = userExtractor ? userExtractor(req) : req.subject ?? req.user;
|
|
72
|
+
const roleMap = options.roleMap ?? req.gatekeeperOptions?.roleMap;
|
|
73
|
+
const policy = options.policy ?? req.gatekeeperOptions?.policy;
|
|
74
|
+
const context = options.context ?? (options.getContext ? options.getContext(req) : req.gatekeeperOptions?.context ?? req.gatekeeperOptions?.getContext?.(req));
|
|
75
|
+
if (!user) {
|
|
76
|
+
const onUnauthorized = options.onUnauthorized ?? req.gatekeeperOptions?.onUnauthorized;
|
|
77
|
+
if (onUnauthorized) {
|
|
78
|
+
await onUnauthorized(res, req);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
res.status(401).json(UNAUTHORIZED_PAYLOAD);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const hasAny = coreCanAny(user, permissions, { roleMap, policy, context });
|
|
85
|
+
if (!hasAny) {
|
|
86
|
+
const onForbidden = options.onForbidden ?? req.gatekeeperOptions?.onForbidden;
|
|
87
|
+
if (onForbidden) {
|
|
88
|
+
await onForbidden(res, permissions, req);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
res.status(403).json(FORBIDDEN_PAYLOAD);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
next();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function requireFeatureFlag(flagKey, options = {}) {
|
|
98
|
+
return (req, res, next) => {
|
|
99
|
+
const flagsResolver = options.getFlags ?? req.gatekeeperOptions?.getFlags;
|
|
100
|
+
const flags = flagsResolver ? flagsResolver(req) : req.gatekeeperFlags;
|
|
101
|
+
const enabled = isFeatureEnabled(flags, flagKey);
|
|
102
|
+
if (!enabled) {
|
|
103
|
+
res.status(403).json({
|
|
104
|
+
error: "FORBIDDEN",
|
|
105
|
+
message: `Feature '${flagKey}' is disabled.`
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
next();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
FORBIDDEN_PAYLOAD,
|
|
114
|
+
UNAUTHORIZED_PAYLOAD,
|
|
115
|
+
gatekeeperMiddleware,
|
|
116
|
+
requireAnyPermission,
|
|
117
|
+
requireFeatureFlag,
|
|
118
|
+
requirePermission
|
|
119
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahmed109/gatekeeper-express",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Express middleware for authorization and feature flags with customizable callbacks powered by Gatekeeper Core",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"express",
|
|
7
|
+
"middleware",
|
|
8
|
+
"authorization",
|
|
9
|
+
"rbac",
|
|
10
|
+
"permissions",
|
|
11
|
+
"feature-flags"
|
|
12
|
+
],
|
|
13
|
+
"author": "Ahmed Tamer",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Ahmed1092002/npm-packages.git",
|
|
18
|
+
"directory": "gatekeeper/packages/express"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/Ahmed1092002/npm-packages/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Ahmed1092002/npm-packages/tree/main/gatekeeper#readme",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.cjs",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js",
|
|
35
|
+
"require": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"express": ">=4.18.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@ahmed109/gatekeeper-core": "^0.3.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/express": "^5.0.0",
|
|
51
|
+
"@types/supertest": "^6.0.2",
|
|
52
|
+
"express": "^4.21.2",
|
|
53
|
+
"supertest": "^7.0.0",
|
|
54
|
+
"tsup": "^8.3.6",
|
|
55
|
+
"typescript": "^5.7.3",
|
|
56
|
+
"vitest": "^3.0.5"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --external express,@ahmed109/gatekeeper-core",
|
|
60
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch --external express,@ahmed109/gatekeeper-core",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"lint": "eslint src"
|
|
63
|
+
}
|
|
6
64
|
}
|