role-permission-engine 1.0.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.
@@ -0,0 +1,385 @@
1
+ /**
2
+ * @fileoverview Core permission-checking utility functions.
3
+ *
4
+ * These are pure, framework-agnostic functions that form the heart of
5
+ * the role-permission-engine. They have zero side effects and can be
6
+ * used independently of any React component.
7
+ *
8
+ * @module utils/checkPermission
9
+ */
10
+
11
+ // ─── Type Definitions ────────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * The logical operator used when evaluating multiple roles or permissions.
15
+ *
16
+ * - `"any"` – The user must satisfy **at least one** of the required items (OR logic).
17
+ * - `"all"` – The user must satisfy **every** required item (AND logic).
18
+ *
19
+ * @typedef {"any" | "all"} LogicOperator
20
+ */
21
+
22
+ /**
23
+ * A single role string, e.g. `"admin"`, `"editor"`, `"viewer"`.
24
+ *
25
+ * @typedef {string} Role
26
+ */
27
+
28
+ /**
29
+ * A single permission string, typically in `"action:resource"` format,
30
+ * e.g. `"read:users"`, `"write:posts"`, `"delete:*"`.
31
+ *
32
+ * @typedef {string} Permission
33
+ */
34
+
35
+ /**
36
+ * The result returned by permission-check functions.
37
+ *
38
+ * @typedef {Object} PermissionResult
39
+ * @property {boolean} allowed - Whether the user is allowed access.
40
+ * @property {string} reason - A human-readable explanation of the result.
41
+ */
42
+
43
+ // ─── Role Checking ────────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Checks whether a user's roles satisfy the required roles using
47
+ * the specified logic operator.
48
+ *
49
+ * This is a **pure utility** — it accepts any string as a role.
50
+ * You define what roles mean in your own application. Examples:
51
+ * `"admin"`, `"manager"`, `"superuser"`, `"read-only"`, `"tenant-owner"`, etc.
52
+ *
53
+ * @param {string[]} userRoles
54
+ * Array of role strings currently assigned to the user.
55
+ * - Comparison is **case-insensitive** and **trims whitespace**.
56
+ * - Example: `['Admin', ' editor ']` is treated as `['admin', 'editor']`.
57
+ * - Pass an empty array `[]` when the user has no roles.
58
+ *
59
+ * @param {string[]} requiredRoles
60
+ * Array of role strings required to pass this check.
61
+ * - If this array is **empty** (`[]`) or not provided, the check
62
+ * is skipped and `allowed: true` is returned immediately.
63
+ * - Example: `['admin', 'superuser']`
64
+ *
65
+ * @param {"any" | "all"} [logic="any"]
66
+ * Controls how multiple `requiredRoles` are evaluated.
67
+ *
68
+ * | Value | Alias | Behaviour | Example |
69
+ * |---------|-------|-----------------------------------------------------|----------------------------------------------|
70
+ * | `"any"` | OR | User needs **at least one** of the required roles | `required: ['admin','editor']` → admin **or** editor is enough |
71
+ * | `"all"` | AND | User needs **every single** required role | `required: ['admin','editor']` → must have **both** |
72
+ *
73
+ * **Default:** `"any"` (OR logic).
74
+ *
75
+ * ```js
76
+ * // Default — no third argument needed for OR logic
77
+ * hasRole(['editor'], ['admin', 'editor']);
78
+ * // same as:
79
+ * hasRole(['editor'], ['admin', 'editor'], 'any');
80
+ * ```
81
+ *
82
+ * @returns {PermissionResult}
83
+ * `{ allowed: boolean, reason: string }`
84
+ *
85
+ * @example <caption>OR logic (default) — user has at least one required role</caption>
86
+ * hasRole(['editor'], ['admin', 'editor']);
87
+ * // => { allowed: true, reason: 'User has at least one required role.' }
88
+ *
89
+ * @example <caption>OR logic — user has none of the required roles</caption>
90
+ * hasRole(['viewer'], ['admin', 'editor'], 'any');
91
+ * // => { allowed: false, reason: 'User does not have any of the required roles: admin, editor' }
92
+ *
93
+ * @example <caption>AND logic — user must have every required role</caption>
94
+ * hasRole(['admin', 'editor'], ['admin', 'editor'], 'all');
95
+ * // => { allowed: true, reason: 'User has all required roles.' }
96
+ *
97
+ * @example <caption>AND logic — user is missing one role</caption>
98
+ * hasRole(['editor'], ['admin', 'editor'], 'all');
99
+ * // => { allowed: false, reason: 'User is missing required roles: admin' }
100
+ *
101
+ * @example <caption>Empty required roles — always allowed</caption>
102
+ * hasRole([], []);
103
+ * // => { allowed: true, reason: 'No roles required — access granted.' }
104
+ */
105
+ function hasRole(userRoles, requiredRoles, logic = "any") {
106
+ if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
107
+ return {
108
+ allowed: true,
109
+ reason: "No roles required — access granted."
110
+ };
111
+ }
112
+ if (!Array.isArray(userRoles) || userRoles.length === 0) {
113
+ return {
114
+ allowed: false,
115
+ reason: "User has no roles assigned."
116
+ };
117
+ }
118
+ const normalizedUser = userRoles.map(r => r.toLowerCase().trim());
119
+ const normalizedRequired = requiredRoles.map(r => r.toLowerCase().trim());
120
+ if (logic === "all") {
121
+ const missing = normalizedRequired.filter(r => !normalizedUser.includes(r));
122
+ if (missing.length > 0) {
123
+ return {
124
+ allowed: false,
125
+ reason: `User is missing required roles: ${missing.join(", ")}`
126
+ };
127
+ }
128
+ return {
129
+ allowed: true,
130
+ reason: "User has all required roles."
131
+ };
132
+ }
133
+
134
+ // Default: "any" (OR logic)
135
+ const matched = normalizedRequired.filter(r => normalizedUser.includes(r));
136
+ if (matched.length === 0) {
137
+ return {
138
+ allowed: false,
139
+ reason: `User does not have any of the required roles: ${normalizedRequired.join(", ")}`
140
+ };
141
+ }
142
+ return {
143
+ allowed: true,
144
+ reason: "User has at least one required role."
145
+ };
146
+ }
147
+
148
+ // ─── Permission Checking ──────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * Checks whether a user's permissions satisfy the required permissions
152
+ * using the specified logic operator. Supports wildcard `"*"` permissions.
153
+ *
154
+ * This is a **pure utility** — it accepts any string as a permission.
155
+ * You define what permissions mean in your own application. A common
156
+ * convention is `"action:resource"` format (e.g. `"read:invoices"`,
157
+ * `"export:reports"`, `"delete:accounts"`), but any string works.
158
+ *
159
+ * **Wildcard rules:**
160
+ * | User permission | Matches |
161
+ * |-----------------|----------------------------------------------|
162
+ * | `"*"` | Every possible permission — full super-access |
163
+ * | `"read:*"` | Any permission starting with `"read:"` |
164
+ * | `"write:posts"` | Only `"write:posts"` exactly |
165
+ *
166
+ * @param {string[]} userPermissions
167
+ * Array of permission strings the user currently holds.
168
+ * - Comparison is **case-insensitive** and **trims whitespace**.
169
+ * - Pass `['*']` to grant superuser access to all checks.
170
+ * - Pass an empty array `[]` when the user has no permissions.
171
+ *
172
+ * @param {string[]} requiredPermissions
173
+ * Array of permission strings required to pass this check.
174
+ * - If this array is **empty** (`[]`) or not provided, the check
175
+ * is skipped and `allowed: true` is returned immediately.
176
+ *
177
+ * @param {"any" | "all"} [logic="any"]
178
+ * Controls how multiple `requiredPermissions` are evaluated.
179
+ *
180
+ * | Value | Alias | Behaviour | Example |
181
+ * |---------|-------|-----------------------------------------------------------|------------------------------------------------------|
182
+ * | `"any"` | OR | User needs **at least one** of the required permissions | `required: ['read:x','write:x']` → read **or** write is enough |
183
+ * | `"all"` | AND | User needs **every single** required permission | `required: ['read:x','write:x']` → must have **both** |
184
+ *
185
+ * **Default:** `"any"` (OR logic).
186
+ *
187
+ * ```js
188
+ * // Default — OR logic, no third argument needed
189
+ * hasPermission(['read:reports'], ['read:reports', 'write:reports']);
190
+ * // same as:
191
+ * hasPermission(['read:reports'], ['read:reports', 'write:reports'], 'any');
192
+ * ```
193
+ *
194
+ * @returns {PermissionResult}
195
+ * `{ allowed: boolean, reason: string }`
196
+ *
197
+ * @example <caption>OR logic (default) — user has one of the required permissions</caption>
198
+ * hasPermission(['read:invoices'], ['read:invoices', 'write:invoices']);
199
+ * // => { allowed: true, reason: 'User has at least one required permission.' }
200
+ *
201
+ * @example <caption>OR logic — user has none</caption>
202
+ * hasPermission(['read:posts'], ['write:posts', 'delete:posts'], 'any');
203
+ * // => { allowed: false, reason: 'User does not have any of the required permissions: ...' }
204
+ *
205
+ * @example <caption>AND logic — must have all required permissions</caption>
206
+ * hasPermission(['read:users', 'write:users'], ['read:users', 'write:users'], 'all');
207
+ * // => { allowed: true, reason: 'User has all required permissions.' }
208
+ *
209
+ * @example <caption>Full wildcard — grants everything</caption>
210
+ * hasPermission(['*'], ['read:users', 'delete:posts'], 'all');
211
+ * // => { allowed: true, reason: 'User has wildcard permission (*) — full access granted.' }
212
+ *
213
+ * @example <caption>Namespace wildcard — "read:*" satisfies any "read:" permission</caption>
214
+ * hasPermission(['read:*'], ['read:invoices', 'read:reports'], 'all');
215
+ * // => { allowed: true, reason: 'User has all required permissions.' }
216
+ */
217
+ function hasPermission(userPermissions, requiredPermissions, logic = "any") {
218
+ if (!Array.isArray(requiredPermissions) || requiredPermissions.length === 0) {
219
+ return {
220
+ allowed: true,
221
+ reason: 'No permissions required — access granted.'
222
+ };
223
+ }
224
+ if (!Array.isArray(userPermissions) || userPermissions.length === 0) {
225
+ return {
226
+ allowed: false,
227
+ reason: 'User has no permissions assigned.'
228
+ };
229
+ }
230
+ const normalizedUser = userPermissions.map(p => p.toLowerCase().trim());
231
+ const normalizedRequired = requiredPermissions.map(p => p.toLowerCase().trim());
232
+
233
+ // Full wildcard check
234
+ if (normalizedUser.includes("*")) {
235
+ return {
236
+ allowed: true,
237
+ reason: "User has wildcard permission (*) — full access granted."
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Checks if a single required permission is satisfied by the user's permission set,
243
+ * including namespace wildcard matching (e.g. "read:*" matches "read:users").
244
+ *
245
+ * @param {string} required - The required permission to satisfy.
246
+ * @returns {boolean} Whether the user satisfies this specific permission.
247
+ */
248
+ function isSatisfied(required) {
249
+ if (normalizedUser.includes(required)) return true;
250
+
251
+ // Namespace wildcard: "read:*" should satisfy "read:users"
252
+ const [requiredNamespace] = required.split(":");
253
+ return normalizedUser.includes(`${requiredNamespace}:*`);
254
+ }
255
+ if (logic === "all") {
256
+ const missing = normalizedRequired.filter(p => !isSatisfied(p));
257
+ if (missing.length > 0) {
258
+ return {
259
+ allowed: false,
260
+ reason: `User is missing required permissions: ${missing.join(", ")}`
261
+ };
262
+ }
263
+ return {
264
+ allowed: true,
265
+ reason: "User has all required permissions."
266
+ };
267
+ }
268
+
269
+ // Default: "any" (OR logic)
270
+ const matched = normalizedRequired.filter(p => isSatisfied(p));
271
+ if (matched.length === 0) {
272
+ return {
273
+ allowed: false,
274
+ reason: `User does not have any of the required permissions: ${normalizedRequired.join(", ")}`
275
+ };
276
+ }
277
+ return {
278
+ allowed: true,
279
+ reason: "User has at least one required permission."
280
+ };
281
+ }
282
+
283
+ // ─── Combined Check ───────────────────────────────────────────────────────────
284
+
285
+ /**
286
+ * Performs a **combined** role AND permission check in a single call.
287
+ *
288
+ * Both the role check and the permission check must independently pass
289
+ * for `allowed` to be `true`. If a constraint array is empty or not
290
+ * provided, that constraint is automatically satisfied (open access).
291
+ *
292
+ * @param {Object} options
293
+ *
294
+ * @param {string[]} [options.userRoles=[]]
295
+ * The roles currently held by the user. Any strings are accepted;
296
+ * you define what roles mean in your app.
297
+ *
298
+ * @param {string[]} [options.userPermissions=[]]
299
+ * The permissions currently held by the user. Supports wildcards:
300
+ * `"*"` (all access) and `"namespace:*"` (all in namespace).
301
+ *
302
+ * @param {string[]} [options.requiredRoles=[]]
303
+ * Roles the user must have to pass the role check.
304
+ * Pass `[]` (default) to skip the role check entirely.
305
+ *
306
+ * @param {string[]} [options.requiredPermissions=[]]
307
+ * Permissions the user must have to pass the permission check.
308
+ * Pass `[]` (default) to skip the permission check entirely.
309
+ *
310
+ * @param {"any" | "all"} [options.roleLogic="any"]
311
+ * Controls how `requiredRoles` are evaluated.
312
+ *
313
+ * | Value | Behaviour | Default? |
314
+ * |---------|--------------------------------------------------|----------|
315
+ * | `"any"` | User needs **at least one** required role (OR) | ✅ Yes |
316
+ * | `"all"` | User needs **every** required role (AND) | No |
317
+ *
318
+ * ```js
319
+ * // 'any' is the default — no need to specify it explicitly for OR logic
320
+ * checkAccess({ requiredRoles: ['admin', 'editor'], roleLogic: 'any' });
321
+ * ```
322
+ *
323
+ * @param {"any" | "all"} [options.permissionLogic="any"]
324
+ * Controls how `requiredPermissions` are evaluated.
325
+ *
326
+ * | Value | Behaviour | Default? |
327
+ * |---------|-------------------------------------------------------|----------|
328
+ * | `"any"` | User needs **at least one** required permission (OR) | ✅ Yes |
329
+ * | `"all"` | User needs **every** required permission (AND) | No |
330
+ *
331
+ * ```js
332
+ * // Require the user to have ALL listed permissions (AND logic)
333
+ * checkAccess({
334
+ * requiredPermissions: ['read:report', 'export:report'],
335
+ * permissionLogic: 'all',
336
+ * });
337
+ * ```
338
+ *
339
+ * @returns {PermissionResult}
340
+ * `{ allowed: boolean, reason: string }` — `allowed` is `true` only
341
+ * when **both** role and permission checks pass.
342
+ *
343
+ * @example <caption>Combined check — OR role logic + OR permission logic (both defaults)</caption>
344
+ * checkAccess({
345
+ * userRoles: ['editor'],
346
+ * userPermissions: ['write:posts'],
347
+ * requiredRoles: ['editor', 'admin'], // editor OR admin
348
+ * requiredPermissions: ['write:posts'], // write:posts required
349
+ * });
350
+ * // => { allowed: true, reason: 'Access granted.' }
351
+ *
352
+ * @example <caption>AND logic for permissions — user must have ALL listed permissions</caption>
353
+ * checkAccess({
354
+ * userRoles: ['manager'],
355
+ * userPermissions: ['approve:leaves'],
356
+ * requiredRoles: ['manager'],
357
+ * requiredPermissions: ['approve:leaves', 'view:team'],
358
+ * permissionLogic: 'all', // must have BOTH permissions
359
+ * });
360
+ * // => { allowed: false, reason: 'User is missing required permissions: view:team' }
361
+ *
362
+ * @example <caption>No constraints — always allowed</caption>
363
+ * checkAccess({ userRoles: ['guest'], userPermissions: [] });
364
+ * // => { allowed: true, reason: 'Access granted.' }
365
+ */
366
+ function checkAccess({
367
+ userRoles = [],
368
+ userPermissions = [],
369
+ requiredRoles = [],
370
+ requiredPermissions = [],
371
+ roleLogic = "any",
372
+ permissionLogic = "any"
373
+ }) {
374
+ const roleResult = hasRole(userRoles, requiredRoles, roleLogic);
375
+ if (!roleResult.allowed) return roleResult;
376
+ const permResult = hasPermission(userPermissions, requiredPermissions, permissionLogic);
377
+ if (!permResult.allowed) return permResult;
378
+ return {
379
+ allowed: true,
380
+ reason: "Access granted."
381
+ };
382
+ }
383
+
384
+ export { checkAccess, hasPermission, hasRole };
385
+ //# sourceMappingURL=utils.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.esm.js","sources":["../src/utils/checkPermission.js"],"sourcesContent":["/**\n * @fileoverview Core permission-checking utility functions.\n *\n * These are pure, framework-agnostic functions that form the heart of\n * the role-permission-engine. They have zero side effects and can be\n * used independently of any React component.\n *\n * @module utils/checkPermission\n */\n\n// ─── Type Definitions ────────────────────────────────────────────────────────\n\n/**\n * The logical operator used when evaluating multiple roles or permissions.\n *\n * - `\"any\"` – The user must satisfy **at least one** of the required items (OR logic).\n * - `\"all\"` – The user must satisfy **every** required item (AND logic).\n *\n * @typedef {\"any\" | \"all\"} LogicOperator\n */\n\n/**\n * A single role string, e.g. `\"admin\"`, `\"editor\"`, `\"viewer\"`.\n *\n * @typedef {string} Role\n */\n\n/**\n * A single permission string, typically in `\"action:resource\"` format,\n * e.g. `\"read:users\"`, `\"write:posts\"`, `\"delete:*\"`.\n *\n * @typedef {string} Permission\n */\n\n/**\n * The result returned by permission-check functions.\n *\n * @typedef {Object} PermissionResult\n * @property {boolean} allowed - Whether the user is allowed access.\n * @property {string} reason - A human-readable explanation of the result.\n */\n\n// ─── Role Checking ────────────────────────────────────────────────────────────\n\n/**\n * Checks whether a user's roles satisfy the required roles using\n * the specified logic operator.\n *\n * This is a **pure utility** — it accepts any string as a role.\n * You define what roles mean in your own application. Examples:\n * `\"admin\"`, `\"manager\"`, `\"superuser\"`, `\"read-only\"`, `\"tenant-owner\"`, etc.\n *\n * @param {string[]} userRoles\n * Array of role strings currently assigned to the user.\n * - Comparison is **case-insensitive** and **trims whitespace**.\n * - Example: `['Admin', ' editor ']` is treated as `['admin', 'editor']`.\n * - Pass an empty array `[]` when the user has no roles.\n *\n * @param {string[]} requiredRoles\n * Array of role strings required to pass this check.\n * - If this array is **empty** (`[]`) or not provided, the check\n * is skipped and `allowed: true` is returned immediately.\n * - Example: `['admin', 'superuser']`\n *\n * @param {\"any\" | \"all\"} [logic=\"any\"]\n * Controls how multiple `requiredRoles` are evaluated.\n *\n * | Value | Alias | Behaviour | Example |\n * |---------|-------|-----------------------------------------------------|----------------------------------------------|\n * | `\"any\"` | OR | User needs **at least one** of the required roles | `required: ['admin','editor']` → admin **or** editor is enough |\n * | `\"all\"` | AND | User needs **every single** required role | `required: ['admin','editor']` → must have **both** |\n *\n * **Default:** `\"any\"` (OR logic).\n *\n * ```js\n * // Default — no third argument needed for OR logic\n * hasRole(['editor'], ['admin', 'editor']);\n * // same as:\n * hasRole(['editor'], ['admin', 'editor'], 'any');\n * ```\n *\n * @returns {PermissionResult}\n * `{ allowed: boolean, reason: string }`\n *\n * @example <caption>OR logic (default) — user has at least one required role</caption>\n * hasRole(['editor'], ['admin', 'editor']);\n * // => { allowed: true, reason: 'User has at least one required role.' }\n *\n * @example <caption>OR logic — user has none of the required roles</caption>\n * hasRole(['viewer'], ['admin', 'editor'], 'any');\n * // => { allowed: false, reason: 'User does not have any of the required roles: admin, editor' }\n *\n * @example <caption>AND logic — user must have every required role</caption>\n * hasRole(['admin', 'editor'], ['admin', 'editor'], 'all');\n * // => { allowed: true, reason: 'User has all required roles.' }\n *\n * @example <caption>AND logic — user is missing one role</caption>\n * hasRole(['editor'], ['admin', 'editor'], 'all');\n * // => { allowed: false, reason: 'User is missing required roles: admin' }\n *\n * @example <caption>Empty required roles — always allowed</caption>\n * hasRole([], []);\n * // => { allowed: true, reason: 'No roles required — access granted.' }\n */\nexport function hasRole(userRoles, requiredRoles, logic = \"any\") {\n if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {\n return { allowed: true, reason: \"No roles required — access granted.\" };\n }\n\n if (!Array.isArray(userRoles) || userRoles.length === 0) {\n return { allowed: false, reason: \"User has no roles assigned.\" };\n }\n\n const normalizedUser = userRoles.map((r) => r.toLowerCase().trim());\n const normalizedRequired = requiredRoles.map((r) => r.toLowerCase().trim());\n\n if (logic === \"all\") {\n const missing = normalizedRequired.filter(\n (r) => !normalizedUser.includes(r),\n );\n if (missing.length > 0) {\n return {\n allowed: false,\n reason: `User is missing required roles: ${missing.join(\", \")}`,\n };\n }\n return { allowed: true, reason: \"User has all required roles.\" };\n }\n\n // Default: \"any\" (OR logic)\n const matched = normalizedRequired.filter((r) => normalizedUser.includes(r));\n if (matched.length === 0) {\n return {\n allowed: false,\n reason: `User does not have any of the required roles: ${normalizedRequired.join(\", \")}`,\n };\n }\n return { allowed: true, reason: \"User has at least one required role.\" };\n}\n\n// ─── Permission Checking ──────────────────────────────────────────────────────\n\n/**\n * Checks whether a user's permissions satisfy the required permissions\n * using the specified logic operator. Supports wildcard `\"*\"` permissions.\n *\n * This is a **pure utility** — it accepts any string as a permission.\n * You define what permissions mean in your own application. A common\n * convention is `\"action:resource\"` format (e.g. `\"read:invoices\"`,\n * `\"export:reports\"`, `\"delete:accounts\"`), but any string works.\n *\n * **Wildcard rules:**\n * | User permission | Matches |\n * |-----------------|----------------------------------------------|\n * | `\"*\"` | Every possible permission — full super-access |\n * | `\"read:*\"` | Any permission starting with `\"read:\"` |\n * | `\"write:posts\"` | Only `\"write:posts\"` exactly |\n *\n * @param {string[]} userPermissions\n * Array of permission strings the user currently holds.\n * - Comparison is **case-insensitive** and **trims whitespace**.\n * - Pass `['*']` to grant superuser access to all checks.\n * - Pass an empty array `[]` when the user has no permissions.\n *\n * @param {string[]} requiredPermissions\n * Array of permission strings required to pass this check.\n * - If this array is **empty** (`[]`) or not provided, the check\n * is skipped and `allowed: true` is returned immediately.\n *\n * @param {\"any\" | \"all\"} [logic=\"any\"]\n * Controls how multiple `requiredPermissions` are evaluated.\n *\n * | Value | Alias | Behaviour | Example |\n * |---------|-------|-----------------------------------------------------------|------------------------------------------------------|\n * | `\"any\"` | OR | User needs **at least one** of the required permissions | `required: ['read:x','write:x']` → read **or** write is enough |\n * | `\"all\"` | AND | User needs **every single** required permission | `required: ['read:x','write:x']` → must have **both** |\n *\n * **Default:** `\"any\"` (OR logic).\n *\n * ```js\n * // Default — OR logic, no third argument needed\n * hasPermission(['read:reports'], ['read:reports', 'write:reports']);\n * // same as:\n * hasPermission(['read:reports'], ['read:reports', 'write:reports'], 'any');\n * ```\n *\n * @returns {PermissionResult}\n * `{ allowed: boolean, reason: string }`\n *\n * @example <caption>OR logic (default) — user has one of the required permissions</caption>\n * hasPermission(['read:invoices'], ['read:invoices', 'write:invoices']);\n * // => { allowed: true, reason: 'User has at least one required permission.' }\n *\n * @example <caption>OR logic — user has none</caption>\n * hasPermission(['read:posts'], ['write:posts', 'delete:posts'], 'any');\n * // => { allowed: false, reason: 'User does not have any of the required permissions: ...' }\n *\n * @example <caption>AND logic — must have all required permissions</caption>\n * hasPermission(['read:users', 'write:users'], ['read:users', 'write:users'], 'all');\n * // => { allowed: true, reason: 'User has all required permissions.' }\n *\n * @example <caption>Full wildcard — grants everything</caption>\n * hasPermission(['*'], ['read:users', 'delete:posts'], 'all');\n * // => { allowed: true, reason: 'User has wildcard permission (*) — full access granted.' }\n *\n * @example <caption>Namespace wildcard — \"read:*\" satisfies any \"read:\" permission</caption>\n * hasPermission(['read:*'], ['read:invoices', 'read:reports'], 'all');\n * // => { allowed: true, reason: 'User has all required permissions.' }\n */\nexport function hasPermission(\n userPermissions,\n requiredPermissions,\n logic = \"any\",\n) {\n if (!Array.isArray(requiredPermissions) || requiredPermissions.length === 0) {\n return {\n allowed: true,\n reason: 'No permissions required — access granted.',\n };\n }\n\n if (!Array.isArray(userPermissions) || userPermissions.length === 0) {\n return { allowed: false, reason: 'User has no permissions assigned.' };\n }\n\n\n const normalizedUser = userPermissions.map((p) => p.toLowerCase().trim());\n const normalizedRequired = requiredPermissions.map((p) =>\n p.toLowerCase().trim(),\n );\n\n // Full wildcard check\n if (normalizedUser.includes(\"*\")) {\n return {\n allowed: true,\n reason: \"User has wildcard permission (*) — full access granted.\",\n };\n }\n\n /**\n * Checks if a single required permission is satisfied by the user's permission set,\n * including namespace wildcard matching (e.g. \"read:*\" matches \"read:users\").\n *\n * @param {string} required - The required permission to satisfy.\n * @returns {boolean} Whether the user satisfies this specific permission.\n */\n function isSatisfied(required) {\n if (normalizedUser.includes(required)) return true;\n\n // Namespace wildcard: \"read:*\" should satisfy \"read:users\"\n const [requiredNamespace] = required.split(\":\");\n return normalizedUser.includes(`${requiredNamespace}:*`);\n }\n\n if (logic === \"all\") {\n const missing = normalizedRequired.filter((p) => !isSatisfied(p));\n if (missing.length > 0) {\n return {\n allowed: false,\n reason: `User is missing required permissions: ${missing.join(\", \")}`,\n };\n }\n return { allowed: true, reason: \"User has all required permissions.\" };\n }\n\n // Default: \"any\" (OR logic)\n const matched = normalizedRequired.filter((p) => isSatisfied(p));\n if (matched.length === 0) {\n return {\n allowed: false,\n reason: `User does not have any of the required permissions: ${normalizedRequired.join(\", \")}`,\n };\n }\n return {\n allowed: true,\n reason: \"User has at least one required permission.\",\n };\n}\n\n// ─── Combined Check ───────────────────────────────────────────────────────────\n\n/**\n * Performs a **combined** role AND permission check in a single call.\n *\n * Both the role check and the permission check must independently pass\n * for `allowed` to be `true`. If a constraint array is empty or not\n * provided, that constraint is automatically satisfied (open access).\n *\n * @param {Object} options\n *\n * @param {string[]} [options.userRoles=[]]\n * The roles currently held by the user. Any strings are accepted;\n * you define what roles mean in your app.\n *\n * @param {string[]} [options.userPermissions=[]]\n * The permissions currently held by the user. Supports wildcards:\n * `\"*\"` (all access) and `\"namespace:*\"` (all in namespace).\n *\n * @param {string[]} [options.requiredRoles=[]]\n * Roles the user must have to pass the role check.\n * Pass `[]` (default) to skip the role check entirely.\n *\n * @param {string[]} [options.requiredPermissions=[]]\n * Permissions the user must have to pass the permission check.\n * Pass `[]` (default) to skip the permission check entirely.\n *\n * @param {\"any\" | \"all\"} [options.roleLogic=\"any\"]\n * Controls how `requiredRoles` are evaluated.\n *\n * | Value | Behaviour | Default? |\n * |---------|--------------------------------------------------|----------|\n * | `\"any\"` | User needs **at least one** required role (OR) | ✅ Yes |\n * | `\"all\"` | User needs **every** required role (AND) | No |\n *\n * ```js\n * // 'any' is the default — no need to specify it explicitly for OR logic\n * checkAccess({ requiredRoles: ['admin', 'editor'], roleLogic: 'any' });\n * ```\n *\n * @param {\"any\" | \"all\"} [options.permissionLogic=\"any\"]\n * Controls how `requiredPermissions` are evaluated.\n *\n * | Value | Behaviour | Default? |\n * |---------|-------------------------------------------------------|----------|\n * | `\"any\"` | User needs **at least one** required permission (OR) | ✅ Yes |\n * | `\"all\"` | User needs **every** required permission (AND) | No |\n *\n * ```js\n * // Require the user to have ALL listed permissions (AND logic)\n * checkAccess({\n * requiredPermissions: ['read:report', 'export:report'],\n * permissionLogic: 'all',\n * });\n * ```\n *\n * @returns {PermissionResult}\n * `{ allowed: boolean, reason: string }` — `allowed` is `true` only\n * when **both** role and permission checks pass.\n *\n * @example <caption>Combined check — OR role logic + OR permission logic (both defaults)</caption>\n * checkAccess({\n * userRoles: ['editor'],\n * userPermissions: ['write:posts'],\n * requiredRoles: ['editor', 'admin'], // editor OR admin\n * requiredPermissions: ['write:posts'], // write:posts required\n * });\n * // => { allowed: true, reason: 'Access granted.' }\n *\n * @example <caption>AND logic for permissions — user must have ALL listed permissions</caption>\n * checkAccess({\n * userRoles: ['manager'],\n * userPermissions: ['approve:leaves'],\n * requiredRoles: ['manager'],\n * requiredPermissions: ['approve:leaves', 'view:team'],\n * permissionLogic: 'all', // must have BOTH permissions\n * });\n * // => { allowed: false, reason: 'User is missing required permissions: view:team' }\n *\n * @example <caption>No constraints — always allowed</caption>\n * checkAccess({ userRoles: ['guest'], userPermissions: [] });\n * // => { allowed: true, reason: 'Access granted.' }\n */\nexport function checkAccess({\n userRoles = [],\n userPermissions = [],\n requiredRoles = [],\n requiredPermissions = [],\n roleLogic = \"any\",\n permissionLogic = \"any\",\n}) {\n const roleResult = hasRole(userRoles, requiredRoles, roleLogic);\n if (!roleResult.allowed) return roleResult;\n\n const permResult = hasPermission(\n userPermissions,\n requiredPermissions,\n permissionLogic,\n );\n if (!permResult.allowed) return permResult;\n\n return { allowed: true, reason: \"Access granted.\" };\n}\n"],"names":["hasRole","userRoles","requiredRoles","logic","Array","isArray","length","allowed","reason","normalizedUser","map","r","toLowerCase","trim","normalizedRequired","missing","filter","includes","join","matched","hasPermission","userPermissions","requiredPermissions","p","isSatisfied","required","requiredNamespace","split","checkAccess","roleLogic","permissionLogic","roleResult","permResult"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,OAAOA,CAACC,SAAS,EAAEC,aAAa,EAAEC,KAAK,GAAG,KAAK,EAAE;AAC/D,EAAA,IAAI,CAACC,KAAK,CAACC,OAAO,CAACH,aAAa,CAAC,IAAIA,aAAa,CAACI,MAAM,KAAK,CAAC,EAAE;IAC/D,OAAO;AAAEC,MAAAA,OAAO,EAAE,IAAI;AAAEC,MAAAA,MAAM,EAAE;KAAuC;AACzE,EAAA;AAEA,EAAA,IAAI,CAACJ,KAAK,CAACC,OAAO,CAACJ,SAAS,CAAC,IAAIA,SAAS,CAACK,MAAM,KAAK,CAAC,EAAE;IACvD,OAAO;AAAEC,MAAAA,OAAO,EAAE,KAAK;AAAEC,MAAAA,MAAM,EAAE;KAA+B;AAClE,EAAA;AAEA,EAAA,MAAMC,cAAc,GAAGR,SAAS,CAACS,GAAG,CAAEC,CAAC,IAAKA,CAAC,CAACC,WAAW,EAAE,CAACC,IAAI,EAAE,CAAC;AACnE,EAAA,MAAMC,kBAAkB,GAAGZ,aAAa,CAACQ,GAAG,CAAEC,CAAC,IAAKA,CAAC,CAACC,WAAW,EAAE,CAACC,IAAI,EAAE,CAAC;EAE3E,IAAIV,KAAK,KAAK,KAAK,EAAE;AACnB,IAAA,MAAMY,OAAO,GAAGD,kBAAkB,CAACE,MAAM,CACtCL,CAAC,IAAK,CAACF,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"}
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "role-permission-engine",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "A lightweight, flexible role and permission-based route guard and UI gate for React applications supporting React Router v5 and v6.",
6
+ "main": "dist/index.cjs.js",
7
+ "module": "dist/index.esm.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./types/index.d.ts",
11
+ "require": "./dist/index.cjs.js",
12
+ "import": "./dist/index.esm.js"
13
+ },
14
+ "./utils": {
15
+ "types": "./types/utils.d.ts",
16
+ "require": "./dist/utils.cjs.js",
17
+ "import": "./dist/utils.esm.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "types",
23
+ "README.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "rollup -c",
28
+ "test": "jest --coverage",
29
+ "test:watch": "jest --watch",
30
+ "docs": "jsdoc -c jsdoc.json",
31
+ "prepublishOnly": "npm run build && npm test"
32
+ },
33
+ "keywords": [
34
+ "react",
35
+ "role",
36
+ "permission",
37
+ "rbac",
38
+ "route-guard",
39
+ "authorization",
40
+ "access-control",
41
+ "react-router"
42
+ ],
43
+ "author": "Darshan Raghvani",
44
+ "license": "MIT",
45
+ "homepage": "https://role-permission-engine.pages.dev/",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/DarshanR1406/role-permission-engine.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/DarshanR1406/role-permission-engine/issues"
52
+ },
53
+ "engines": {
54
+ "node": ">=14.0.0"
55
+ },
56
+ "peerDependencies": {
57
+ "react": ">=16.8.0",
58
+ "react-dom": ">=16.8.0",
59
+ "react-router-dom": ">=5.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "react": {
63
+ "optional": true
64
+ },
65
+ "react-dom": {
66
+ "optional": true
67
+ },
68
+ "react-router-dom": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "@babel/core": "^7.23.0",
74
+ "@babel/preset-env": "^7.23.0",
75
+ "@babel/preset-react": "^7.22.0",
76
+ "@rollup/plugin-babel": "^6.0.4",
77
+ "@rollup/plugin-commonjs": "^25.0.7",
78
+ "@rollup/plugin-node-resolve": "^15.2.3",
79
+ "@testing-library/jest-dom": "^6.1.4",
80
+ "@testing-library/react": "^14.0.0",
81
+ "@testing-library/user-event": "^14.5.1",
82
+ "babel-jest": "^29.7.0",
83
+ "jest": "^29.7.0",
84
+ "jest-environment-jsdom": "^29.7.0",
85
+ "jsdoc": "^4.0.2",
86
+ "react": "^18.2.0",
87
+ "react-dom": "^18.2.0",
88
+ "react-router-dom": "^6.18.0",
89
+ "rollup": "^4.1.4"
90
+ },
91
+ "jest": {
92
+ "testEnvironment": "jsdom",
93
+ "setupFilesAfterEnv": ["./jest.setup.js"],
94
+ "transform": {
95
+ "^.+\\.(js|jsx)$": "babel-jest"
96
+ },
97
+ "moduleFileExtensions": [
98
+ "js",
99
+ "jsx"
100
+ ],
101
+ "collectCoverageFrom": [
102
+ "src/**/*.{js,jsx}",
103
+ "!src/index.js"
104
+ ]
105
+ }
106
+ }