@ahmed109/gatekeeper-react 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 +75 -2
- package/dist/index.cjs +180 -0
- package/dist/index.d.cts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +153 -0
- package/package.json +66 -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,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ahmed109/gatekeeper-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Declarative React authorization and feature-flag toolkit with fail-safe loading state.
|
|
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-react @ahmed109/gatekeeper-core
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @ahmed109/gatekeeper-react @ahmed109/gatekeeper-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Peer Dependencies
|
|
16
|
+
|
|
17
|
+
- `react`: `>=18.0.0`
|
|
18
|
+
- `react-dom`: `>=18.0.0`
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Fail-Safe Loading**: Authorization decisions default to closed (`false`) during `isLoading: true`, eliminating UI flashes of restricted content.
|
|
23
|
+
- **Declarative Guards**: `<Can>` and `<Feature>` components with customizable `fallback` slots.
|
|
24
|
+
- **Reactive Hooks**: `usePermission()`, `useFeatureFlag()`, `useCanAll()`, `useCanAny()`, and `useGatekeeper()`.
|
|
25
|
+
- **Permission Memoization**: Automatically caches effective permission sets to minimize evaluation overhead.
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import React from 'react';
|
|
31
|
+
import { GatekeeperProvider, Can, Feature, usePermission } from '@ahmed109/gatekeeper-react';
|
|
32
|
+
import type { GatekeeperUser } from '@ahmed109/gatekeeper-core';
|
|
33
|
+
|
|
34
|
+
const currentUser: GatekeeperUser = {
|
|
35
|
+
id: 'usr_123',
|
|
36
|
+
name: 'Alice',
|
|
37
|
+
role: 'manager',
|
|
38
|
+
permissions: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const activeFlags = {
|
|
42
|
+
'beta-analytics': true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function App() {
|
|
46
|
+
return (
|
|
47
|
+
<GatekeeperProvider user={currentUser} flags={activeFlags} isLoading={false}>
|
|
48
|
+
<Dashboard />
|
|
49
|
+
</GatekeeperProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Dashboard() {
|
|
54
|
+
const canManage = usePermission('settings.manage');
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
<h1>Operations Dashboard</h1>
|
|
59
|
+
|
|
60
|
+
{/* Declarative permission guard */}
|
|
61
|
+
<Can permission="users.read" fallback={<p>Access denied to users list.</p>}>
|
|
62
|
+
<UsersList />
|
|
63
|
+
</Can>
|
|
64
|
+
|
|
65
|
+
{/* Declarative feature flag guard */}
|
|
66
|
+
<Feature flag="beta-analytics" fallback={<p>Analytics coming soon.</p>}>
|
|
67
|
+
<AnalyticsPanel />
|
|
68
|
+
</Feature>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT © [Ahmed Tamer](https://github.com/Ahmed1092002)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
Can: () => Can,
|
|
24
|
+
Feature: () => Feature,
|
|
25
|
+
GatekeeperContext: () => GatekeeperContext,
|
|
26
|
+
GatekeeperProvider: () => GatekeeperProvider,
|
|
27
|
+
useCanAll: () => useCanAll,
|
|
28
|
+
useCanAny: () => useCanAny,
|
|
29
|
+
useFeatureFlag: () => useFeatureFlag,
|
|
30
|
+
useGatekeeper: () => useGatekeeper,
|
|
31
|
+
usePermission: () => usePermission
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/context.tsx
|
|
36
|
+
var import_react = require("react");
|
|
37
|
+
var import_gatekeeper_core = require("@ahmed109/gatekeeper-core");
|
|
38
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
39
|
+
var GatekeeperContext = (0, import_react.createContext)(null);
|
|
40
|
+
var GatekeeperProvider = ({
|
|
41
|
+
user,
|
|
42
|
+
currentUser,
|
|
43
|
+
subject,
|
|
44
|
+
flags,
|
|
45
|
+
featureFlags,
|
|
46
|
+
roleMap = import_gatekeeper_core.DEFAULT_ROLE_PERMISSIONS,
|
|
47
|
+
policy,
|
|
48
|
+
context,
|
|
49
|
+
isLoading = false,
|
|
50
|
+
children,
|
|
51
|
+
fallback = null
|
|
52
|
+
}) => {
|
|
53
|
+
const activeUser = subject ?? user ?? currentUser ?? null;
|
|
54
|
+
const rawFlags = flags ?? featureFlags;
|
|
55
|
+
const normalizedFlags = (0, import_react.useMemo)(() => (0, import_gatekeeper_core.normalizeFeatureFlags)(rawFlags), [rawFlags]);
|
|
56
|
+
const effectivePermissions = (0, import_react.useMemo)(() => {
|
|
57
|
+
if (isLoading) return [];
|
|
58
|
+
return Array.from((0, import_gatekeeper_core.getUserEffectivePermissions)(activeUser, roleMap));
|
|
59
|
+
}, [activeUser, roleMap, isLoading]);
|
|
60
|
+
const contextValue = (0, import_react.useMemo)(() => {
|
|
61
|
+
return {
|
|
62
|
+
user: activeUser,
|
|
63
|
+
subject: activeUser,
|
|
64
|
+
flags: normalizedFlags,
|
|
65
|
+
roleMap,
|
|
66
|
+
policy,
|
|
67
|
+
context,
|
|
68
|
+
isLoading,
|
|
69
|
+
can: (permission, canOptions) => {
|
|
70
|
+
if (isLoading) return false;
|
|
71
|
+
const combinedOptions = {
|
|
72
|
+
roleMap: canOptions?.roleMap ?? roleMap,
|
|
73
|
+
policy: canOptions?.policy ?? policy,
|
|
74
|
+
context: canOptions?.context ?? context
|
|
75
|
+
};
|
|
76
|
+
return (0, import_gatekeeper_core.can)(activeUser, permission, combinedOptions);
|
|
77
|
+
},
|
|
78
|
+
canAll: (permissions, canOptions) => {
|
|
79
|
+
if (isLoading) return false;
|
|
80
|
+
const combinedOptions = {
|
|
81
|
+
roleMap: canOptions?.roleMap ?? roleMap,
|
|
82
|
+
policy: canOptions?.policy ?? policy,
|
|
83
|
+
context: canOptions?.context ?? context
|
|
84
|
+
};
|
|
85
|
+
return (0, import_gatekeeper_core.canAll)(activeUser, permissions, combinedOptions);
|
|
86
|
+
},
|
|
87
|
+
canAny: (permissions, canOptions) => {
|
|
88
|
+
if (isLoading) return false;
|
|
89
|
+
const combinedOptions = {
|
|
90
|
+
roleMap: canOptions?.roleMap ?? roleMap,
|
|
91
|
+
policy: canOptions?.policy ?? policy,
|
|
92
|
+
context: canOptions?.context ?? context
|
|
93
|
+
};
|
|
94
|
+
return (0, import_gatekeeper_core.canAny)(activeUser, permissions, combinedOptions);
|
|
95
|
+
},
|
|
96
|
+
isFeatureEnabled: (flagKey) => {
|
|
97
|
+
if (isLoading) return false;
|
|
98
|
+
return (0, import_gatekeeper_core.isFeatureEnabled)(normalizedFlags, flagKey);
|
|
99
|
+
},
|
|
100
|
+
effectivePermissions
|
|
101
|
+
};
|
|
102
|
+
}, [activeUser, normalizedFlags, roleMap, policy, context, isLoading, effectivePermissions]);
|
|
103
|
+
if (isLoading && fallback) {
|
|
104
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GatekeeperContext.Provider, { value: contextValue, children: fallback });
|
|
105
|
+
}
|
|
106
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GatekeeperContext.Provider, { value: contextValue, children });
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/hooks.ts
|
|
110
|
+
var import_react2 = require("react");
|
|
111
|
+
function useGatekeeper() {
|
|
112
|
+
const context = (0, import_react2.useContext)(GatekeeperContext);
|
|
113
|
+
if (!context) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"useGatekeeper must be used within a <GatekeeperProvider>. Make sure to wrap your application with <GatekeeperProvider>."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return context;
|
|
119
|
+
}
|
|
120
|
+
function usePermission(permission, options) {
|
|
121
|
+
const { can } = useGatekeeper();
|
|
122
|
+
return can(permission, options);
|
|
123
|
+
}
|
|
124
|
+
function useFeatureFlag(flagKey) {
|
|
125
|
+
const { isFeatureEnabled } = useGatekeeper();
|
|
126
|
+
return isFeatureEnabled(flagKey);
|
|
127
|
+
}
|
|
128
|
+
function useCanAll(permissions, options) {
|
|
129
|
+
const { canAll } = useGatekeeper();
|
|
130
|
+
return canAll(permissions, options);
|
|
131
|
+
}
|
|
132
|
+
function useCanAny(permissions, options) {
|
|
133
|
+
const { canAny } = useGatekeeper();
|
|
134
|
+
return canAny(permissions, options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/components.tsx
|
|
138
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
139
|
+
var Can = ({
|
|
140
|
+
permission,
|
|
141
|
+
permissions,
|
|
142
|
+
requireAll = true,
|
|
143
|
+
context,
|
|
144
|
+
policy,
|
|
145
|
+
fallback = null,
|
|
146
|
+
children
|
|
147
|
+
}) => {
|
|
148
|
+
const { can, canAll, canAny } = useGatekeeper();
|
|
149
|
+
let isAllowed = false;
|
|
150
|
+
const options = context || policy ? { context, policy } : void 0;
|
|
151
|
+
if (permission) {
|
|
152
|
+
isAllowed = can(permission, options);
|
|
153
|
+
} else if (permissions && permissions.length > 0) {
|
|
154
|
+
isAllowed = requireAll ? canAll(permissions, options) : canAny(permissions, options);
|
|
155
|
+
}
|
|
156
|
+
if (!isAllowed) {
|
|
157
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: fallback });
|
|
158
|
+
}
|
|
159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
|
160
|
+
};
|
|
161
|
+
var Feature = ({ flag, fallback = null, children }) => {
|
|
162
|
+
const { isFeatureEnabled } = useGatekeeper();
|
|
163
|
+
const enabled = isFeatureEnabled(flag);
|
|
164
|
+
if (!enabled) {
|
|
165
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: fallback });
|
|
166
|
+
}
|
|
167
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
|
168
|
+
};
|
|
169
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
170
|
+
0 && (module.exports = {
|
|
171
|
+
Can,
|
|
172
|
+
Feature,
|
|
173
|
+
GatekeeperContext,
|
|
174
|
+
GatekeeperProvider,
|
|
175
|
+
useCanAll,
|
|
176
|
+
useCanAny,
|
|
177
|
+
useFeatureFlag,
|
|
178
|
+
useGatekeeper,
|
|
179
|
+
usePermission
|
|
180
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Permission, AuthorizationPolicy, AuthorizationSubject, RolePermissionsMap, EvaluateOptions, FeatureFlag } from '@ahmed109/gatekeeper-core';
|
|
2
|
+
export { AuthorizationResult, FeatureFlag, GatekeeperUser, Permission, Role, RolePermissionsMap } from '@ahmed109/gatekeeper-core';
|
|
3
|
+
import React, { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface GatekeeperContextValue {
|
|
6
|
+
user: AuthorizationSubject | null;
|
|
7
|
+
subject: AuthorizationSubject | null;
|
|
8
|
+
flags: Record<string, boolean>;
|
|
9
|
+
roleMap: RolePermissionsMap;
|
|
10
|
+
policy?: AuthorizationPolicy;
|
|
11
|
+
context?: Record<string, unknown>;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
can: (permission: Permission, options?: EvaluateOptions) => boolean;
|
|
14
|
+
canAll: (permissions: Permission[], options?: EvaluateOptions) => boolean;
|
|
15
|
+
canAny: (permissions: Permission[], options?: EvaluateOptions) => boolean;
|
|
16
|
+
isFeatureEnabled: (flagKey: string) => boolean;
|
|
17
|
+
effectivePermissions: string[];
|
|
18
|
+
}
|
|
19
|
+
interface GatekeeperProviderProps {
|
|
20
|
+
user?: AuthorizationSubject | null;
|
|
21
|
+
currentUser?: AuthorizationSubject | null;
|
|
22
|
+
subject?: AuthorizationSubject | null;
|
|
23
|
+
flags?: Record<string, boolean> | FeatureFlag[];
|
|
24
|
+
featureFlags?: Record<string, boolean> | FeatureFlag[];
|
|
25
|
+
roleMap?: RolePermissionsMap;
|
|
26
|
+
policy?: AuthorizationPolicy;
|
|
27
|
+
context?: Record<string, unknown>;
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
fallback?: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
interface CanProps {
|
|
33
|
+
/** Single permission to check */
|
|
34
|
+
permission?: Permission;
|
|
35
|
+
/** Multiple permissions to check */
|
|
36
|
+
permissions?: Permission[];
|
|
37
|
+
/** If true when multiple permissions given, user must have all. Default is true */
|
|
38
|
+
requireAll?: boolean;
|
|
39
|
+
/** Optional scoped context for policy evaluation */
|
|
40
|
+
context?: Record<string, unknown>;
|
|
41
|
+
/** Optional policy callback overriding or supplementing provider policy */
|
|
42
|
+
policy?: AuthorizationPolicy;
|
|
43
|
+
/** Custom fallback component if unauthorized */
|
|
44
|
+
fallback?: ReactNode;
|
|
45
|
+
/** Content to render when authorized */
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
interface FeatureProps {
|
|
49
|
+
/** Feature flag key to check */
|
|
50
|
+
flag: string;
|
|
51
|
+
/** Custom fallback component if flag is disabled */
|
|
52
|
+
fallback?: ReactNode;
|
|
53
|
+
/** Content to render when flag is active */
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare const GatekeeperContext: React.Context<GatekeeperContextValue | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Top-level provider for Gatekeeper authorization and feature flags in React.
|
|
60
|
+
*/
|
|
61
|
+
declare const GatekeeperProvider: React.FC<GatekeeperProviderProps>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Access the full Gatekeeper context.
|
|
65
|
+
*/
|
|
66
|
+
declare function useGatekeeper(): GatekeeperContextValue;
|
|
67
|
+
/**
|
|
68
|
+
* React hook to check if the current user has a specific permission.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const canDelete = usePermission('users.delete');
|
|
72
|
+
* const canEdit = usePermission('projects.update', { context: { projectOwnerId: 'u1' } });
|
|
73
|
+
*/
|
|
74
|
+
declare function usePermission(permission: Permission, options?: EvaluateOptions): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* React hook to check if a feature flag is enabled.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const hasBeta = useFeatureFlag('beta-analytics');
|
|
80
|
+
*/
|
|
81
|
+
declare function useFeatureFlag(flagKey: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* React hook to check if the current user has all specified permissions.
|
|
84
|
+
*/
|
|
85
|
+
declare function useCanAll(permissions: Permission[], options?: EvaluateOptions): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* React hook to check if the current user has at least one of the specified permissions.
|
|
88
|
+
*/
|
|
89
|
+
declare function useCanAny(permissions: Permission[], options?: EvaluateOptions): boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Conditionally renders children if the authenticated user has the required permission(s).
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* <Can permission="users.delete" fallback={<p>Not allowed</p>}>
|
|
96
|
+
* <DeleteUserButton />
|
|
97
|
+
* </Can>
|
|
98
|
+
*/
|
|
99
|
+
declare const Can: React.FC<CanProps>;
|
|
100
|
+
/**
|
|
101
|
+
* Conditionally renders children if a specific feature flag is enabled.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* <Feature flag="new-dashboard" fallback={<LegacyDashboard />}>
|
|
105
|
+
* <NewDashboard />
|
|
106
|
+
* </Feature>
|
|
107
|
+
*/
|
|
108
|
+
declare const Feature: React.FC<FeatureProps>;
|
|
109
|
+
|
|
110
|
+
export { Can, type CanProps, Feature, type FeatureProps, GatekeeperContext, type GatekeeperContextValue, GatekeeperProvider, type GatekeeperProviderProps, useCanAll, useCanAny, useFeatureFlag, useGatekeeper, usePermission };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Permission, AuthorizationPolicy, AuthorizationSubject, RolePermissionsMap, EvaluateOptions, FeatureFlag } from '@ahmed109/gatekeeper-core';
|
|
2
|
+
export { AuthorizationResult, FeatureFlag, GatekeeperUser, Permission, Role, RolePermissionsMap } from '@ahmed109/gatekeeper-core';
|
|
3
|
+
import React, { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface GatekeeperContextValue {
|
|
6
|
+
user: AuthorizationSubject | null;
|
|
7
|
+
subject: AuthorizationSubject | null;
|
|
8
|
+
flags: Record<string, boolean>;
|
|
9
|
+
roleMap: RolePermissionsMap;
|
|
10
|
+
policy?: AuthorizationPolicy;
|
|
11
|
+
context?: Record<string, unknown>;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
can: (permission: Permission, options?: EvaluateOptions) => boolean;
|
|
14
|
+
canAll: (permissions: Permission[], options?: EvaluateOptions) => boolean;
|
|
15
|
+
canAny: (permissions: Permission[], options?: EvaluateOptions) => boolean;
|
|
16
|
+
isFeatureEnabled: (flagKey: string) => boolean;
|
|
17
|
+
effectivePermissions: string[];
|
|
18
|
+
}
|
|
19
|
+
interface GatekeeperProviderProps {
|
|
20
|
+
user?: AuthorizationSubject | null;
|
|
21
|
+
currentUser?: AuthorizationSubject | null;
|
|
22
|
+
subject?: AuthorizationSubject | null;
|
|
23
|
+
flags?: Record<string, boolean> | FeatureFlag[];
|
|
24
|
+
featureFlags?: Record<string, boolean> | FeatureFlag[];
|
|
25
|
+
roleMap?: RolePermissionsMap;
|
|
26
|
+
policy?: AuthorizationPolicy;
|
|
27
|
+
context?: Record<string, unknown>;
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
fallback?: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
interface CanProps {
|
|
33
|
+
/** Single permission to check */
|
|
34
|
+
permission?: Permission;
|
|
35
|
+
/** Multiple permissions to check */
|
|
36
|
+
permissions?: Permission[];
|
|
37
|
+
/** If true when multiple permissions given, user must have all. Default is true */
|
|
38
|
+
requireAll?: boolean;
|
|
39
|
+
/** Optional scoped context for policy evaluation */
|
|
40
|
+
context?: Record<string, unknown>;
|
|
41
|
+
/** Optional policy callback overriding or supplementing provider policy */
|
|
42
|
+
policy?: AuthorizationPolicy;
|
|
43
|
+
/** Custom fallback component if unauthorized */
|
|
44
|
+
fallback?: ReactNode;
|
|
45
|
+
/** Content to render when authorized */
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
interface FeatureProps {
|
|
49
|
+
/** Feature flag key to check */
|
|
50
|
+
flag: string;
|
|
51
|
+
/** Custom fallback component if flag is disabled */
|
|
52
|
+
fallback?: ReactNode;
|
|
53
|
+
/** Content to render when flag is active */
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare const GatekeeperContext: React.Context<GatekeeperContextValue | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Top-level provider for Gatekeeper authorization and feature flags in React.
|
|
60
|
+
*/
|
|
61
|
+
declare const GatekeeperProvider: React.FC<GatekeeperProviderProps>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Access the full Gatekeeper context.
|
|
65
|
+
*/
|
|
66
|
+
declare function useGatekeeper(): GatekeeperContextValue;
|
|
67
|
+
/**
|
|
68
|
+
* React hook to check if the current user has a specific permission.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const canDelete = usePermission('users.delete');
|
|
72
|
+
* const canEdit = usePermission('projects.update', { context: { projectOwnerId: 'u1' } });
|
|
73
|
+
*/
|
|
74
|
+
declare function usePermission(permission: Permission, options?: EvaluateOptions): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* React hook to check if a feature flag is enabled.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const hasBeta = useFeatureFlag('beta-analytics');
|
|
80
|
+
*/
|
|
81
|
+
declare function useFeatureFlag(flagKey: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* React hook to check if the current user has all specified permissions.
|
|
84
|
+
*/
|
|
85
|
+
declare function useCanAll(permissions: Permission[], options?: EvaluateOptions): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* React hook to check if the current user has at least one of the specified permissions.
|
|
88
|
+
*/
|
|
89
|
+
declare function useCanAny(permissions: Permission[], options?: EvaluateOptions): boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Conditionally renders children if the authenticated user has the required permission(s).
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* <Can permission="users.delete" fallback={<p>Not allowed</p>}>
|
|
96
|
+
* <DeleteUserButton />
|
|
97
|
+
* </Can>
|
|
98
|
+
*/
|
|
99
|
+
declare const Can: React.FC<CanProps>;
|
|
100
|
+
/**
|
|
101
|
+
* Conditionally renders children if a specific feature flag is enabled.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* <Feature flag="new-dashboard" fallback={<LegacyDashboard />}>
|
|
105
|
+
* <NewDashboard />
|
|
106
|
+
* </Feature>
|
|
107
|
+
*/
|
|
108
|
+
declare const Feature: React.FC<FeatureProps>;
|
|
109
|
+
|
|
110
|
+
export { Can, type CanProps, Feature, type FeatureProps, GatekeeperContext, type GatekeeperContextValue, GatekeeperProvider, type GatekeeperProviderProps, useCanAll, useCanAny, useFeatureFlag, useGatekeeper, usePermission };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/context.tsx
|
|
2
|
+
import { createContext, useMemo } from "react";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_ROLE_PERMISSIONS,
|
|
5
|
+
can as coreCan,
|
|
6
|
+
canAll as coreCanAll,
|
|
7
|
+
canAny as coreCanAny,
|
|
8
|
+
getUserEffectivePermissions,
|
|
9
|
+
isFeatureEnabled as coreIsFeatureEnabled,
|
|
10
|
+
normalizeFeatureFlags
|
|
11
|
+
} from "@ahmed109/gatekeeper-core";
|
|
12
|
+
import { jsx } from "react/jsx-runtime";
|
|
13
|
+
var GatekeeperContext = createContext(null);
|
|
14
|
+
var GatekeeperProvider = ({
|
|
15
|
+
user,
|
|
16
|
+
currentUser,
|
|
17
|
+
subject,
|
|
18
|
+
flags,
|
|
19
|
+
featureFlags,
|
|
20
|
+
roleMap = DEFAULT_ROLE_PERMISSIONS,
|
|
21
|
+
policy,
|
|
22
|
+
context,
|
|
23
|
+
isLoading = false,
|
|
24
|
+
children,
|
|
25
|
+
fallback = null
|
|
26
|
+
}) => {
|
|
27
|
+
const activeUser = subject ?? user ?? currentUser ?? null;
|
|
28
|
+
const rawFlags = flags ?? featureFlags;
|
|
29
|
+
const normalizedFlags = useMemo(() => normalizeFeatureFlags(rawFlags), [rawFlags]);
|
|
30
|
+
const effectivePermissions = useMemo(() => {
|
|
31
|
+
if (isLoading) return [];
|
|
32
|
+
return Array.from(getUserEffectivePermissions(activeUser, roleMap));
|
|
33
|
+
}, [activeUser, roleMap, isLoading]);
|
|
34
|
+
const contextValue = useMemo(() => {
|
|
35
|
+
return {
|
|
36
|
+
user: activeUser,
|
|
37
|
+
subject: activeUser,
|
|
38
|
+
flags: normalizedFlags,
|
|
39
|
+
roleMap,
|
|
40
|
+
policy,
|
|
41
|
+
context,
|
|
42
|
+
isLoading,
|
|
43
|
+
can: (permission, canOptions) => {
|
|
44
|
+
if (isLoading) return false;
|
|
45
|
+
const combinedOptions = {
|
|
46
|
+
roleMap: canOptions?.roleMap ?? roleMap,
|
|
47
|
+
policy: canOptions?.policy ?? policy,
|
|
48
|
+
context: canOptions?.context ?? context
|
|
49
|
+
};
|
|
50
|
+
return coreCan(activeUser, permission, combinedOptions);
|
|
51
|
+
},
|
|
52
|
+
canAll: (permissions, canOptions) => {
|
|
53
|
+
if (isLoading) return false;
|
|
54
|
+
const combinedOptions = {
|
|
55
|
+
roleMap: canOptions?.roleMap ?? roleMap,
|
|
56
|
+
policy: canOptions?.policy ?? policy,
|
|
57
|
+
context: canOptions?.context ?? context
|
|
58
|
+
};
|
|
59
|
+
return coreCanAll(activeUser, permissions, combinedOptions);
|
|
60
|
+
},
|
|
61
|
+
canAny: (permissions, canOptions) => {
|
|
62
|
+
if (isLoading) return false;
|
|
63
|
+
const combinedOptions = {
|
|
64
|
+
roleMap: canOptions?.roleMap ?? roleMap,
|
|
65
|
+
policy: canOptions?.policy ?? policy,
|
|
66
|
+
context: canOptions?.context ?? context
|
|
67
|
+
};
|
|
68
|
+
return coreCanAny(activeUser, permissions, combinedOptions);
|
|
69
|
+
},
|
|
70
|
+
isFeatureEnabled: (flagKey) => {
|
|
71
|
+
if (isLoading) return false;
|
|
72
|
+
return coreIsFeatureEnabled(normalizedFlags, flagKey);
|
|
73
|
+
},
|
|
74
|
+
effectivePermissions
|
|
75
|
+
};
|
|
76
|
+
}, [activeUser, normalizedFlags, roleMap, policy, context, isLoading, effectivePermissions]);
|
|
77
|
+
if (isLoading && fallback) {
|
|
78
|
+
return /* @__PURE__ */ jsx(GatekeeperContext.Provider, { value: contextValue, children: fallback });
|
|
79
|
+
}
|
|
80
|
+
return /* @__PURE__ */ jsx(GatekeeperContext.Provider, { value: contextValue, children });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/hooks.ts
|
|
84
|
+
import { useContext } from "react";
|
|
85
|
+
function useGatekeeper() {
|
|
86
|
+
const context = useContext(GatekeeperContext);
|
|
87
|
+
if (!context) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"useGatekeeper must be used within a <GatekeeperProvider>. Make sure to wrap your application with <GatekeeperProvider>."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return context;
|
|
93
|
+
}
|
|
94
|
+
function usePermission(permission, options) {
|
|
95
|
+
const { can } = useGatekeeper();
|
|
96
|
+
return can(permission, options);
|
|
97
|
+
}
|
|
98
|
+
function useFeatureFlag(flagKey) {
|
|
99
|
+
const { isFeatureEnabled } = useGatekeeper();
|
|
100
|
+
return isFeatureEnabled(flagKey);
|
|
101
|
+
}
|
|
102
|
+
function useCanAll(permissions, options) {
|
|
103
|
+
const { canAll } = useGatekeeper();
|
|
104
|
+
return canAll(permissions, options);
|
|
105
|
+
}
|
|
106
|
+
function useCanAny(permissions, options) {
|
|
107
|
+
const { canAny } = useGatekeeper();
|
|
108
|
+
return canAny(permissions, options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/components.tsx
|
|
112
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
113
|
+
var Can = ({
|
|
114
|
+
permission,
|
|
115
|
+
permissions,
|
|
116
|
+
requireAll = true,
|
|
117
|
+
context,
|
|
118
|
+
policy,
|
|
119
|
+
fallback = null,
|
|
120
|
+
children
|
|
121
|
+
}) => {
|
|
122
|
+
const { can, canAll, canAny } = useGatekeeper();
|
|
123
|
+
let isAllowed = false;
|
|
124
|
+
const options = context || policy ? { context, policy } : void 0;
|
|
125
|
+
if (permission) {
|
|
126
|
+
isAllowed = can(permission, options);
|
|
127
|
+
} else if (permissions && permissions.length > 0) {
|
|
128
|
+
isAllowed = requireAll ? canAll(permissions, options) : canAny(permissions, options);
|
|
129
|
+
}
|
|
130
|
+
if (!isAllowed) {
|
|
131
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
132
|
+
}
|
|
133
|
+
return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
134
|
+
};
|
|
135
|
+
var Feature = ({ flag, fallback = null, children }) => {
|
|
136
|
+
const { isFeatureEnabled } = useGatekeeper();
|
|
137
|
+
const enabled = isFeatureEnabled(flag);
|
|
138
|
+
if (!enabled) {
|
|
139
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
140
|
+
}
|
|
141
|
+
return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
142
|
+
};
|
|
143
|
+
export {
|
|
144
|
+
Can,
|
|
145
|
+
Feature,
|
|
146
|
+
GatekeeperContext,
|
|
147
|
+
GatekeeperProvider,
|
|
148
|
+
useCanAll,
|
|
149
|
+
useCanAny,
|
|
150
|
+
useFeatureFlag,
|
|
151
|
+
useGatekeeper,
|
|
152
|
+
usePermission
|
|
153
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahmed109/gatekeeper-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "React authorization and feature-flag toolkit with fail-safe loading state powered by Gatekeeper Core",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"authorization",
|
|
8
|
+
"permissions",
|
|
9
|
+
"feature-flags",
|
|
10
|
+
"rbac",
|
|
11
|
+
"hooks",
|
|
12
|
+
"components"
|
|
13
|
+
],
|
|
14
|
+
"author": "Ahmed Tamer",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Ahmed1092002/npm-packages.git",
|
|
19
|
+
"directory": "gatekeeper/packages/react"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Ahmed1092002/npm-packages/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/Ahmed1092002/npm-packages/tree/main/gatekeeper#readme",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": ">=18.0.0",
|
|
46
|
+
"react-dom": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@ahmed109/gatekeeper-core": "^0.3.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
53
|
+
"@testing-library/react": "^16.2.0",
|
|
54
|
+
"@types/react": "^19.0.8",
|
|
55
|
+
"@types/react-dom": "^19.0.3",
|
|
56
|
+
"jsdom": "^26.0.0",
|
|
57
|
+
"react": "^19.0.0",
|
|
58
|
+
"react-dom": "^19.0.0",
|
|
59
|
+
"tsup": "^8.3.6",
|
|
60
|
+
"typescript": "^5.7.3",
|
|
61
|
+
"vitest": "^3.0.5"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --external react,react-dom,@ahmed109/gatekeeper-core",
|
|
65
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch --external react,react-dom,@ahmed109/gatekeeper-core",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"lint": "eslint src"
|
|
68
|
+
}
|
|
6
69
|
}
|