access-control-js 0.1.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.md +21 -0
- package/README.md +327 -0
- package/dist/index.d.mts +147 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +50 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aashish Rai
|
|
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
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Access Control JS
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe access control library for TypeScript applications. Supports both server-side (stateless) and client-side (reactive) environments.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install access-control-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Type-Safe**: Fully typed resources and actions based on your configuration.
|
|
14
|
+
- **Isomorphic**: Works on both server (Node.js/Next.js) and client (React/Vanilla JS).
|
|
15
|
+
- **Reactive**: Built-in subscription store for UI updates.
|
|
16
|
+
- **Flexible**: Supports Role-Based (RBAC) and Attribute-Based (ABAC) access control.
|
|
17
|
+
|
|
18
|
+
## Recommended Folder Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
@/lib/access-control/
|
|
22
|
+
resources.ts ← your resource & action config
|
|
23
|
+
policy.ts ← policy builder
|
|
24
|
+
factory.ts ← access control instance (client or server)
|
|
25
|
+
index.ts ← barrel export
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Step 1 — Define Resources & Actions
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// @/lib/access-control/resources.ts
|
|
36
|
+
export const config = {
|
|
37
|
+
posts: ['read', 'create', 'update', 'delete'],
|
|
38
|
+
comments: ['create', 'delete'],
|
|
39
|
+
admin: ['manage_users', 'view_logs'],
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export type AppConfig = typeof config;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### Step 2 — Define Your Policy
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// @/lib/access-control/policy.ts
|
|
51
|
+
import { definePolicy } from 'access-control-js';
|
|
52
|
+
import { type AppConfig } from './resources';
|
|
53
|
+
|
|
54
|
+
export const policy = definePolicy<AppConfig>()
|
|
55
|
+
.allow('posts', ['read', 'create'])
|
|
56
|
+
.deny('posts', ['delete'])
|
|
57
|
+
.allow('comments', ['*'])
|
|
58
|
+
.build();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Policy Builder API
|
|
62
|
+
|
|
63
|
+
| Method | Signature | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `definePolicy` | `definePolicy<T>()` | Creates a new typed `PolicyBuilder` |
|
|
66
|
+
| `.allow` | `.allow(resource, actions, contexts?)` | Adds an allow statement |
|
|
67
|
+
| `.deny` | `.deny(resource, actions, contexts?)` | Adds a deny statement |
|
|
68
|
+
| `.build` | `.build()` | Returns the final `TAccessControlPolicy<T>` array |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Step 3 — Create the Factory
|
|
73
|
+
|
|
74
|
+
Pick **one** depending on your environment.
|
|
75
|
+
|
|
76
|
+
#### Client-Side (Vanilla JS / React)
|
|
77
|
+
|
|
78
|
+
Use `createAccessControl` to create a reactive store that can be updated after login.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// @/lib/access-control/factory.ts
|
|
82
|
+
import { createAccessControl } from 'access-control-js';
|
|
83
|
+
import { type AppConfig } from './resources';
|
|
84
|
+
import { policy } from './policy';
|
|
85
|
+
|
|
86
|
+
export const authStore = createAccessControl<AppConfig>(policy);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Check permissions:**
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { authStore } from '@/lib/access-control/factory';
|
|
93
|
+
|
|
94
|
+
const { can } = authStore.getSnapshot();
|
|
95
|
+
|
|
96
|
+
can('posts', 'create'); // true | false
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Update policy after login:**
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { authStore } from '@/lib/access-control/factory';
|
|
103
|
+
|
|
104
|
+
async function login() {
|
|
105
|
+
const user = await api.login();
|
|
106
|
+
authStore.updatePolicy(user.policy);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Subscribe to policy changes:**
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { authStore } from '@/lib/access-control/factory';
|
|
114
|
+
|
|
115
|
+
const updateUI = () => {
|
|
116
|
+
const { can } = authStore.getSnapshot();
|
|
117
|
+
const btn = document.getElementById('delete-btn');
|
|
118
|
+
btn.style.display = can('posts', 'delete') ? 'block' : 'none';
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
updateUI();
|
|
122
|
+
authStore.subscribe(updateUI);
|
|
123
|
+
|
|
124
|
+
// Later, when policy updates...
|
|
125
|
+
authStore.updatePolicy(newPolicy); // UI updates automatically
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### `createAccessControl` Store API
|
|
129
|
+
|
|
130
|
+
| Method | Signature | Description |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `updatePolicy` | `updatePolicy(policy, defaultContext?, options?)` | Replaces the policy, optionally updating default context and loading state |
|
|
133
|
+
| `setLoading` | `setLoading(boolean)` | Sets `isLoading` state and notifies subscribers |
|
|
134
|
+
| `subscribe` | `subscribe(listener)` | Registers a change listener; returns an unsubscribe function |
|
|
135
|
+
| `getSnapshot` | `getSnapshot()` | Returns a stable snapshot with `can`, `canAll`, `canAny`, `canThese`, `policy`, `isLoading` |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
#### Server-Side (API Routes / Server Components)
|
|
140
|
+
|
|
141
|
+
Use `getAccessControl` for stateless, per-request environments.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// @/lib/access-control/factory.ts
|
|
145
|
+
import { getAccessControl } from 'access-control-js';
|
|
146
|
+
import { type AppConfig } from './resources';
|
|
147
|
+
import { policy } from './policy';
|
|
148
|
+
|
|
149
|
+
export const ac = getAccessControl<AppConfig>(policy);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Use in an API route or Server Component:**
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { ac } from '@/lib/access-control/factory';
|
|
156
|
+
|
|
157
|
+
export async function POST(req: Request) {
|
|
158
|
+
if (!ac.can('posts', 'create')) {
|
|
159
|
+
return new Response('Forbidden', { status: 403 });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// perform action...
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `getAccessControl` API
|
|
167
|
+
|
|
168
|
+
| Method | Signature | Description |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `can` | `can(resource, action, context?)` | Returns `true` if the action is allowed |
|
|
171
|
+
| `canAll` | `canAll(resource, actions[], context?)` | Returns `true` if **all** actions are allowed |
|
|
172
|
+
| `canAny` | `canAny(resource, actions[], context?)` | Returns `true` if **any** action is allowed |
|
|
173
|
+
| `canThese` | `canThese(checks[])` | Returns a `Record<action, boolean>` for each check |
|
|
174
|
+
| `policy` | `policy` | The policy array the instance was created with |
|
|
175
|
+
| `isLoading` | `isLoading` | Always `false` for stateless instances |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Step 4 — Merging Policies
|
|
180
|
+
|
|
181
|
+
Combine a local static policy with a remote one fetched from your backend.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// @/lib/access-control/policy.ts
|
|
185
|
+
import { definePolicy, mergePolicies, type TAccessControlPolicy } from 'access-control-js';
|
|
186
|
+
import { type AppConfig } from './resources';
|
|
187
|
+
|
|
188
|
+
// 1. Local base policy
|
|
189
|
+
const basePolicy = definePolicy<AppConfig>()
|
|
190
|
+
.allow('posts', ['read'])
|
|
191
|
+
.build();
|
|
192
|
+
|
|
193
|
+
// 2. Fetch remote policy (e.g., from DB or API)
|
|
194
|
+
const remotePolicy: TAccessControlPolicy<AppConfig> = await api.getPolicy();
|
|
195
|
+
|
|
196
|
+
// 3. Merge — last policy takes precedence on overlaps
|
|
197
|
+
export const policy = mergePolicies(basePolicy, remotePolicy);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
| Function | Signature | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `mergePolicies` | `mergePolicies(...policies)` | Flattens multiple policy arrays into one |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Advanced
|
|
207
|
+
|
|
208
|
+
### Default Context (ABAC)
|
|
209
|
+
|
|
210
|
+
Pass a default context that is automatically merged into every permission check. Useful for multi-tenant apps.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Server-side
|
|
214
|
+
const ac = getAccessControl(policy, { defaultContext: { churchId: '123' } });
|
|
215
|
+
ac.can('posts', 'read'); // uses { churchId: '123' }
|
|
216
|
+
ac.can('posts', 'read', { role: 'admin' }); // uses { churchId: '123', role: 'admin' }
|
|
217
|
+
|
|
218
|
+
// Client-side — set at creation
|
|
219
|
+
const authStore = createAccessControl<AppConfig>(policy, { defaultContext: { churchId: '123' } });
|
|
220
|
+
|
|
221
|
+
// Update context alongside policy
|
|
222
|
+
authStore.updatePolicy(newPolicy, { churchId: '456' });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Loading State (UI)
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const store = createAccessControl([]);
|
|
229
|
+
|
|
230
|
+
store.setLoading(true);
|
|
231
|
+
|
|
232
|
+
// In React
|
|
233
|
+
const { isLoading, can } = useAccessControl();
|
|
234
|
+
if (isLoading) return <Spinner />;
|
|
235
|
+
|
|
236
|
+
// Update policy and turn off loading in one go
|
|
237
|
+
store.updatePolicy(newPolicy, undefined, { isLoading: false });
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Conflict Resolution
|
|
241
|
+
|
|
242
|
+
By default, any deny rule at the highest specificity blocks access (`denyWins`). You can change this:
|
|
243
|
+
|
|
244
|
+
| Strategy | Description |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `denyWins` (default) | Any deny rule at the highest specificity blocks access |
|
|
247
|
+
| `firstWins` | The first matching rule in the policy array determines the result |
|
|
248
|
+
| `lastWins` | The last matching rule in the policy array determines the result |
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const ac = getAccessControl(policy, { conflictResolution: 'lastWins' });
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `AccessControlOptions`
|
|
255
|
+
|
|
256
|
+
| Option | Type | Default | Description |
|
|
257
|
+
|---|---|---|---|
|
|
258
|
+
| `defaultContext` | `Record<string, any>` | `undefined` | Merged into every `can()` call automatically |
|
|
259
|
+
| `conflictResolution` | `'denyWins' \| 'firstWins' \| 'lastWins'` | `'denyWins'` | Strategy for resolving conflicting allow/deny rules |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Usage with Frameworks
|
|
264
|
+
|
|
265
|
+
The examples below assume `authStore` is already exported from `@/lib/access-control/factory.ts`.
|
|
266
|
+
|
|
267
|
+
### React
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// @/lib/access-control/factory.ts (add to existing file)
|
|
271
|
+
import { useSyncExternalStore } from 'react';
|
|
272
|
+
|
|
273
|
+
export const useAccessControl = () =>
|
|
274
|
+
useSyncExternalStore(authStore.subscribe, authStore.getSnapshot);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
import { useAccessControl } from '@/lib/access-control/factory';
|
|
279
|
+
|
|
280
|
+
export const CreatePostButton = () => {
|
|
281
|
+
const { can, isLoading } = useAccessControl();
|
|
282
|
+
|
|
283
|
+
if (isLoading) return <Spinner />;
|
|
284
|
+
if (!can('posts', 'create')) return null;
|
|
285
|
+
|
|
286
|
+
return <button>Create Post</button>;
|
|
287
|
+
};
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### Vue
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// composables/useAccessControl.ts
|
|
296
|
+
import { shallowRef, onUnmounted } from 'vue';
|
|
297
|
+
import { authStore } from '@/lib/access-control/factory';
|
|
298
|
+
|
|
299
|
+
export const useAccessControl = () => {
|
|
300
|
+
const snapshot = shallowRef(authStore.getSnapshot());
|
|
301
|
+
|
|
302
|
+
const unsubscribe = authStore.subscribe(() => {
|
|
303
|
+
snapshot.value = authStore.getSnapshot();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
onUnmounted(unsubscribe);
|
|
307
|
+
|
|
308
|
+
return snapshot;
|
|
309
|
+
};
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
```vue
|
|
313
|
+
<!-- CreatePostButton.vue -->
|
|
314
|
+
<script setup lang="ts">
|
|
315
|
+
import { useAccessControl } from '@/composables/useAccessControl';
|
|
316
|
+
|
|
317
|
+
const ac = useAccessControl();
|
|
318
|
+
</script>
|
|
319
|
+
|
|
320
|
+
<template>
|
|
321
|
+
<span v-if="ac.isLoading">Loading...</span>
|
|
322
|
+
<button v-else-if="ac.can('posts', 'create')">Create Post</button>
|
|
323
|
+
</template>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration object defining resources and their available actions.
|
|
3
|
+
* Keys are resource names, and values are arrays of action strings.
|
|
4
|
+
* Use `as const` to ensure literal types are preserved.
|
|
5
|
+
*/
|
|
6
|
+
type AccessControlConfig = Record<string, readonly string[]>;
|
|
7
|
+
/**
|
|
8
|
+
* A single statement in an access control policy.
|
|
9
|
+
* Defines a permission for a specific resource and actions.
|
|
10
|
+
*/
|
|
11
|
+
type TAccessControlStatement<T extends AccessControlConfig> = {
|
|
12
|
+
[R in keyof T]: {
|
|
13
|
+
/** The resource this statement applies to. */
|
|
14
|
+
resource: R;
|
|
15
|
+
/** The actions allowed or denied. Can include '*' for all actions. */
|
|
16
|
+
actions: readonly (T[R][number] | "*" | "")[];
|
|
17
|
+
/** The effect of the statement: 'allow' grants access, 'deny' blocks it. */
|
|
18
|
+
effect: "allow" | "deny";
|
|
19
|
+
/** Optional contexts for Attribute-Based Access Control (ABAC). Access is granted if ANY context object matches (OR logic). */
|
|
20
|
+
contexts?: readonly Record<string, any>[];
|
|
21
|
+
};
|
|
22
|
+
}[keyof T];
|
|
23
|
+
/**
|
|
24
|
+
* An access control policy consisting of an array of statements.
|
|
25
|
+
*/
|
|
26
|
+
type TAccessControlPolicy<T extends AccessControlConfig> = readonly TAccessControlStatement<T>[];
|
|
27
|
+
/**
|
|
28
|
+
* Strategy for resolving conflicting permissions (e.g., when one rule allows and another denies).
|
|
29
|
+
* - `denyWins`: (Default) If any matching rule denies, access is denied.
|
|
30
|
+
* - `firstWins`: The first matching rule in the policy array wins.
|
|
31
|
+
* - `lastWins`: The last matching rule in the policy array wins.
|
|
32
|
+
*/
|
|
33
|
+
type ConflictResolutionStrategy = "denyWins" | "firstWins" | "lastWins";
|
|
34
|
+
interface AccessControlOptions {
|
|
35
|
+
/** Optional default context merged into all permission checks. */
|
|
36
|
+
defaultContext?: Record<string, any>;
|
|
37
|
+
/** Strategy for resolving conflicting permissions. Defaults to 'denyWins'. */
|
|
38
|
+
conflictResolution?: ConflictResolutionStrategy;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The core access control interface returned by getAccessControl.
|
|
42
|
+
* Contains the policy and helper functions for checking permissions.
|
|
43
|
+
*/
|
|
44
|
+
interface CoreAccessControlType<T extends AccessControlConfig> {
|
|
45
|
+
/** The current access control policy. */
|
|
46
|
+
policy: TAccessControlPolicy<T>;
|
|
47
|
+
/** Indicates if the policy is currently loading. */
|
|
48
|
+
isLoading: boolean;
|
|
49
|
+
/** Checks if a specific action on a resource is allowed. */
|
|
50
|
+
can: <R extends keyof T>(resource: R, action: T[R][number], context?: Record<string, any> | Record<string, any>[]) => boolean;
|
|
51
|
+
/** Checks if ALL specified actions on a resource are allowed. */
|
|
52
|
+
canAll: <R extends keyof T>(resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[]) => boolean;
|
|
53
|
+
/** Checks if ANY of the specified actions on a resource are allowed. */
|
|
54
|
+
canAny: <R extends keyof T>(resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[]) => boolean;
|
|
55
|
+
/** Checks multiple actions on a resource at once. Returns an object mapping each action to its allow/deny status. */
|
|
56
|
+
canThese: <R extends keyof T>(resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[]) => Record<T[R][number], boolean>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The store interface returned by createAccessControl.
|
|
60
|
+
* Provides state management and a snapshot with all check methods.
|
|
61
|
+
*/
|
|
62
|
+
interface AccessControlStore<T extends AccessControlConfig> {
|
|
63
|
+
/** Updates the current policy and optionally the default context, then notifies listeners. */
|
|
64
|
+
updatePolicy: (newPolicy: TAccessControlPolicy<T>, defaultContext?: Record<string, any>, options?: {
|
|
65
|
+
isLoading?: boolean;
|
|
66
|
+
}) => void;
|
|
67
|
+
/** Manually sets the loading state and notifies listeners. */
|
|
68
|
+
setLoading: (isLoading: boolean) => void;
|
|
69
|
+
/** Subscribes to policy changes. Returns a cleanup function. */
|
|
70
|
+
subscribe: (listener: () => void) => () => void;
|
|
71
|
+
/** Returns a cached snapshot of the current access control state with all check methods. */
|
|
72
|
+
getSnapshot: () => CoreAccessControlType<T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pure function to evaluate access against a specific policy state.
|
|
77
|
+
* This helper ensures logic is consistent across both static and dynamic implementations.
|
|
78
|
+
*/
|
|
79
|
+
declare const evaluateAccess: <T extends AccessControlConfig, R extends keyof T>(policy: TAccessControlPolicy<T>, resource: R, action: T[R][number], context?: Record<string, any> | Record<string, any>[], options?: AccessControlOptions) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Bulk version of evaluateAccess to check multiple actions on the same resource
|
|
82
|
+
* with the same context. Minimizes redundant policy filtering and context processing.
|
|
83
|
+
*/
|
|
84
|
+
declare const evaluateAccessBulk: <T extends AccessControlConfig, R extends keyof T>(policy: TAccessControlPolicy<T>, resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[], options?: AccessControlOptions) => Record<T[R][number], boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Creates a static access control interface.
|
|
87
|
+
* Ideal for server-side use (e.g., API routes, Server Components) where the policy is fixed per request.
|
|
88
|
+
*
|
|
89
|
+
* @param accessControlPolicy - The policy to evaluate.
|
|
90
|
+
* @param options - Optional configuration options (default context, conflict resolution).
|
|
91
|
+
* @returns An object containing `can`, `canAll`, and `canAny` functions.
|
|
92
|
+
*/
|
|
93
|
+
declare const getAccessControl: <T extends AccessControlConfig>(accessControlPolicy: TAccessControlPolicy<T>, optionsOrContext?: AccessControlOptions | Record<string, any>) => CoreAccessControlType<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Creates an updatable access control store with subscription capabilities.
|
|
96
|
+
* Ideal for client-side use where the policy may load asynchronously or change over time.
|
|
97
|
+
*
|
|
98
|
+
* @param initialPolicy - The initial policy to use.
|
|
99
|
+
* @param options - Optional configuration options (default context, conflict resolution).
|
|
100
|
+
* @returns An object containing policy updater, subscription method, and snapshot.
|
|
101
|
+
*/
|
|
102
|
+
declare const createAccessControl: <T extends AccessControlConfig>(initialPolicy: TAccessControlPolicy<T>, optionsOrContext?: AccessControlOptions | Record<string, any>) => AccessControlStore<T>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A fluent builder class to help create access control policies.
|
|
106
|
+
* Use `definePolicy()` to start a chain.
|
|
107
|
+
*/
|
|
108
|
+
declare class PolicyBuilder<T extends AccessControlConfig> {
|
|
109
|
+
private statements;
|
|
110
|
+
/**
|
|
111
|
+
* Allows one or more actions on a specific resource.
|
|
112
|
+
* @param resource The resource to grant access to.
|
|
113
|
+
* @param actions The actions to allow (or "*" for all).
|
|
114
|
+
* @param options Optional configuration, like ABAC contexts.
|
|
115
|
+
*/
|
|
116
|
+
allow<R extends keyof T>(resource: R, actions: readonly (T[R][number] | "*" | "")[], options?: {
|
|
117
|
+
contexts?: readonly Record<string, any>[];
|
|
118
|
+
}): this;
|
|
119
|
+
/**
|
|
120
|
+
* Denies one or more actions on a specific resource.
|
|
121
|
+
* @param resource The resource to deny access to.
|
|
122
|
+
* @param actions The actions to deny (or "*" for all).
|
|
123
|
+
* @param options Optional configuration, like ABAC contexts.
|
|
124
|
+
*/
|
|
125
|
+
deny<R extends keyof T>(resource: R, actions: readonly (T[R][number] | "*" | "")[], options?: {
|
|
126
|
+
contexts?: readonly Record<string, any>[];
|
|
127
|
+
}): this;
|
|
128
|
+
/**
|
|
129
|
+
* Returns the constructed policy as a plain array.
|
|
130
|
+
* This array is fully serializable and can be merged with other policies.
|
|
131
|
+
*/
|
|
132
|
+
build(): TAccessControlPolicy<T>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Helper to start building a policy using the fluent API.
|
|
136
|
+
* @returns A new PolicyBuilder instance.
|
|
137
|
+
*/
|
|
138
|
+
declare const definePolicy: <T extends AccessControlConfig>() => PolicyBuilder<T>;
|
|
139
|
+
/**
|
|
140
|
+
* Merges multiple policy arrays into a single policy array.
|
|
141
|
+
* Useful for combining static policies with dynamic ones fetched from a backend.
|
|
142
|
+
* @param policies The list of policy arrays to merge.
|
|
143
|
+
* @returns A single flattened policy array.
|
|
144
|
+
*/
|
|
145
|
+
declare const mergePolicies: <T extends AccessControlConfig>(...policies: TAccessControlPolicy<T>[]) => TAccessControlPolicy<T>;
|
|
146
|
+
|
|
147
|
+
export { type AccessControlConfig, type AccessControlOptions, type AccessControlStore, type ConflictResolutionStrategy, type CoreAccessControlType, PolicyBuilder, type TAccessControlPolicy, type TAccessControlStatement, createAccessControl, definePolicy, evaluateAccess, evaluateAccessBulk, getAccessControl, mergePolicies };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration object defining resources and their available actions.
|
|
3
|
+
* Keys are resource names, and values are arrays of action strings.
|
|
4
|
+
* Use `as const` to ensure literal types are preserved.
|
|
5
|
+
*/
|
|
6
|
+
type AccessControlConfig = Record<string, readonly string[]>;
|
|
7
|
+
/**
|
|
8
|
+
* A single statement in an access control policy.
|
|
9
|
+
* Defines a permission for a specific resource and actions.
|
|
10
|
+
*/
|
|
11
|
+
type TAccessControlStatement<T extends AccessControlConfig> = {
|
|
12
|
+
[R in keyof T]: {
|
|
13
|
+
/** The resource this statement applies to. */
|
|
14
|
+
resource: R;
|
|
15
|
+
/** The actions allowed or denied. Can include '*' for all actions. */
|
|
16
|
+
actions: readonly (T[R][number] | "*" | "")[];
|
|
17
|
+
/** The effect of the statement: 'allow' grants access, 'deny' blocks it. */
|
|
18
|
+
effect: "allow" | "deny";
|
|
19
|
+
/** Optional contexts for Attribute-Based Access Control (ABAC). Access is granted if ANY context object matches (OR logic). */
|
|
20
|
+
contexts?: readonly Record<string, any>[];
|
|
21
|
+
};
|
|
22
|
+
}[keyof T];
|
|
23
|
+
/**
|
|
24
|
+
* An access control policy consisting of an array of statements.
|
|
25
|
+
*/
|
|
26
|
+
type TAccessControlPolicy<T extends AccessControlConfig> = readonly TAccessControlStatement<T>[];
|
|
27
|
+
/**
|
|
28
|
+
* Strategy for resolving conflicting permissions (e.g., when one rule allows and another denies).
|
|
29
|
+
* - `denyWins`: (Default) If any matching rule denies, access is denied.
|
|
30
|
+
* - `firstWins`: The first matching rule in the policy array wins.
|
|
31
|
+
* - `lastWins`: The last matching rule in the policy array wins.
|
|
32
|
+
*/
|
|
33
|
+
type ConflictResolutionStrategy = "denyWins" | "firstWins" | "lastWins";
|
|
34
|
+
interface AccessControlOptions {
|
|
35
|
+
/** Optional default context merged into all permission checks. */
|
|
36
|
+
defaultContext?: Record<string, any>;
|
|
37
|
+
/** Strategy for resolving conflicting permissions. Defaults to 'denyWins'. */
|
|
38
|
+
conflictResolution?: ConflictResolutionStrategy;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The core access control interface returned by getAccessControl.
|
|
42
|
+
* Contains the policy and helper functions for checking permissions.
|
|
43
|
+
*/
|
|
44
|
+
interface CoreAccessControlType<T extends AccessControlConfig> {
|
|
45
|
+
/** The current access control policy. */
|
|
46
|
+
policy: TAccessControlPolicy<T>;
|
|
47
|
+
/** Indicates if the policy is currently loading. */
|
|
48
|
+
isLoading: boolean;
|
|
49
|
+
/** Checks if a specific action on a resource is allowed. */
|
|
50
|
+
can: <R extends keyof T>(resource: R, action: T[R][number], context?: Record<string, any> | Record<string, any>[]) => boolean;
|
|
51
|
+
/** Checks if ALL specified actions on a resource are allowed. */
|
|
52
|
+
canAll: <R extends keyof T>(resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[]) => boolean;
|
|
53
|
+
/** Checks if ANY of the specified actions on a resource are allowed. */
|
|
54
|
+
canAny: <R extends keyof T>(resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[]) => boolean;
|
|
55
|
+
/** Checks multiple actions on a resource at once. Returns an object mapping each action to its allow/deny status. */
|
|
56
|
+
canThese: <R extends keyof T>(resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[]) => Record<T[R][number], boolean>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The store interface returned by createAccessControl.
|
|
60
|
+
* Provides state management and a snapshot with all check methods.
|
|
61
|
+
*/
|
|
62
|
+
interface AccessControlStore<T extends AccessControlConfig> {
|
|
63
|
+
/** Updates the current policy and optionally the default context, then notifies listeners. */
|
|
64
|
+
updatePolicy: (newPolicy: TAccessControlPolicy<T>, defaultContext?: Record<string, any>, options?: {
|
|
65
|
+
isLoading?: boolean;
|
|
66
|
+
}) => void;
|
|
67
|
+
/** Manually sets the loading state and notifies listeners. */
|
|
68
|
+
setLoading: (isLoading: boolean) => void;
|
|
69
|
+
/** Subscribes to policy changes. Returns a cleanup function. */
|
|
70
|
+
subscribe: (listener: () => void) => () => void;
|
|
71
|
+
/** Returns a cached snapshot of the current access control state with all check methods. */
|
|
72
|
+
getSnapshot: () => CoreAccessControlType<T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pure function to evaluate access against a specific policy state.
|
|
77
|
+
* This helper ensures logic is consistent across both static and dynamic implementations.
|
|
78
|
+
*/
|
|
79
|
+
declare const evaluateAccess: <T extends AccessControlConfig, R extends keyof T>(policy: TAccessControlPolicy<T>, resource: R, action: T[R][number], context?: Record<string, any> | Record<string, any>[], options?: AccessControlOptions) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Bulk version of evaluateAccess to check multiple actions on the same resource
|
|
82
|
+
* with the same context. Minimizes redundant policy filtering and context processing.
|
|
83
|
+
*/
|
|
84
|
+
declare const evaluateAccessBulk: <T extends AccessControlConfig, R extends keyof T>(policy: TAccessControlPolicy<T>, resource: R, actions: T[R][number][], context?: Record<string, any> | Record<string, any>[], options?: AccessControlOptions) => Record<T[R][number], boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Creates a static access control interface.
|
|
87
|
+
* Ideal for server-side use (e.g., API routes, Server Components) where the policy is fixed per request.
|
|
88
|
+
*
|
|
89
|
+
* @param accessControlPolicy - The policy to evaluate.
|
|
90
|
+
* @param options - Optional configuration options (default context, conflict resolution).
|
|
91
|
+
* @returns An object containing `can`, `canAll`, and `canAny` functions.
|
|
92
|
+
*/
|
|
93
|
+
declare const getAccessControl: <T extends AccessControlConfig>(accessControlPolicy: TAccessControlPolicy<T>, optionsOrContext?: AccessControlOptions | Record<string, any>) => CoreAccessControlType<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Creates an updatable access control store with subscription capabilities.
|
|
96
|
+
* Ideal for client-side use where the policy may load asynchronously or change over time.
|
|
97
|
+
*
|
|
98
|
+
* @param initialPolicy - The initial policy to use.
|
|
99
|
+
* @param options - Optional configuration options (default context, conflict resolution).
|
|
100
|
+
* @returns An object containing policy updater, subscription method, and snapshot.
|
|
101
|
+
*/
|
|
102
|
+
declare const createAccessControl: <T extends AccessControlConfig>(initialPolicy: TAccessControlPolicy<T>, optionsOrContext?: AccessControlOptions | Record<string, any>) => AccessControlStore<T>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A fluent builder class to help create access control policies.
|
|
106
|
+
* Use `definePolicy()` to start a chain.
|
|
107
|
+
*/
|
|
108
|
+
declare class PolicyBuilder<T extends AccessControlConfig> {
|
|
109
|
+
private statements;
|
|
110
|
+
/**
|
|
111
|
+
* Allows one or more actions on a specific resource.
|
|
112
|
+
* @param resource The resource to grant access to.
|
|
113
|
+
* @param actions The actions to allow (or "*" for all).
|
|
114
|
+
* @param options Optional configuration, like ABAC contexts.
|
|
115
|
+
*/
|
|
116
|
+
allow<R extends keyof T>(resource: R, actions: readonly (T[R][number] | "*" | "")[], options?: {
|
|
117
|
+
contexts?: readonly Record<string, any>[];
|
|
118
|
+
}): this;
|
|
119
|
+
/**
|
|
120
|
+
* Denies one or more actions on a specific resource.
|
|
121
|
+
* @param resource The resource to deny access to.
|
|
122
|
+
* @param actions The actions to deny (or "*" for all).
|
|
123
|
+
* @param options Optional configuration, like ABAC contexts.
|
|
124
|
+
*/
|
|
125
|
+
deny<R extends keyof T>(resource: R, actions: readonly (T[R][number] | "*" | "")[], options?: {
|
|
126
|
+
contexts?: readonly Record<string, any>[];
|
|
127
|
+
}): this;
|
|
128
|
+
/**
|
|
129
|
+
* Returns the constructed policy as a plain array.
|
|
130
|
+
* This array is fully serializable and can be merged with other policies.
|
|
131
|
+
*/
|
|
132
|
+
build(): TAccessControlPolicy<T>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Helper to start building a policy using the fluent API.
|
|
136
|
+
* @returns A new PolicyBuilder instance.
|
|
137
|
+
*/
|
|
138
|
+
declare const definePolicy: <T extends AccessControlConfig>() => PolicyBuilder<T>;
|
|
139
|
+
/**
|
|
140
|
+
* Merges multiple policy arrays into a single policy array.
|
|
141
|
+
* Useful for combining static policies with dynamic ones fetched from a backend.
|
|
142
|
+
* @param policies The list of policy arrays to merge.
|
|
143
|
+
* @returns A single flattened policy array.
|
|
144
|
+
*/
|
|
145
|
+
declare const mergePolicies: <T extends AccessControlConfig>(...policies: TAccessControlPolicy<T>[]) => TAccessControlPolicy<T>;
|
|
146
|
+
|
|
147
|
+
export { type AccessControlConfig, type AccessControlOptions, type AccessControlStore, type ConflictResolutionStrategy, type CoreAccessControlType, PolicyBuilder, type TAccessControlPolicy, type TAccessControlStatement, createAccessControl, definePolicy, evaluateAccess, evaluateAccessBulk, getAccessControl, mergePolicies };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var m=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var L=(n,e)=>{for(var o in e)m(n,o,{get:e[o],enumerable:!0})},j=(n,e,o,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of w(e))!M.call(n,t)&&t!==o&&m(n,t,{get:()=>e[t],enumerable:!(s=S(e,t))||s.enumerable});return n};var K=n=>j(m({},"__esModule",{value:!0}),n);var E={};L(E,{PolicyBuilder:()=>A,createAccessControl:()=>$,definePolicy:()=>B,evaluateAccess:()=>k,evaluateAccessBulk:()=>x,getAccessControl:()=>W,mergePolicies:()=>D});module.exports=K(E);var p=(n,e,o)=>{let s=[],t=new Set;return n.forEach((r,f)=>{if(!(r.actions.includes("*")||r.actions.includes(e)))return;let l=r.contexts||[];if(l.length===0){s.push({effect:r.effect,specificity:0,index:f});return}if(o.length!==0)for(let i of l){let c=Object.keys(i),d=c.length,y=`${f}:${d}`;if(!t.has(y)){for(let g of o)if(c.every(u=>g[u]===i[u])){t.add(y),s.push({effect:r.effect,specificity:d,index:f});break}}}}),s},P=(n,e)=>{if(e==="firstWins")return n.sort((t,r)=>t.index-r.index),n[0].effect==="allow";if(e==="lastWins")return n.sort((t,r)=>r.index-t.index),n[0].effect==="allow";n.sort((t,r)=>r.specificity-t.specificity);let o=n[0].specificity;return!n.filter(t=>t.specificity===o).some(t=>t.effect==="deny")},k=(n,e,o,s,t)=>{let r=Array.isArray(s)?s:s?[s]:[],f=n.filter(l=>l.resource===e),a=p(f,o,r);return a.length===0?!1:P(a,(t==null?void 0:t.conflictResolution)??"denyWins")},x=(n,e,o,s,t)=>{let r={};if(o.length===0)return r;let f=Array.isArray(s)?s:s?[s]:[],a=n.filter(i=>i.resource===e);if(a.length===0){for(let i of o)r[i]=!1;return r}let l=(t==null?void 0:t.conflictResolution)??"denyWins";for(let i of o){let c=p(a,i,f);r[i]=c.length===0?!1:P(c,l)}return r},h=(n,e)=>n?e?Array.isArray(e)?e.map(o=>({...n,...o})):{...n,...e}:n:e,W=(n,e)=>{let o={};e&&("conflictResolution"in e||"defaultContext"in e?o=e:o={defaultContext:e});let{defaultContext:s}=o,t=(l,i,c)=>k(n,l,i,h(s,c),o),r=(l,i,c)=>{let d=a(l,i,c);return Object.values(d).every(y=>y===!0)},f=(l,i,c)=>{let d=a(l,i,c);return Object.values(d).some(y=>y===!0)},a=(l,i,c)=>x(n,l,i,h(s,c),o);return{policy:n,isLoading:!1,can:t,canAll:r,canAny:f,canThese:a}},$=(n,e)=>{let o={};e&&("conflictResolution"in e||"defaultContext"in e?o=e:o={defaultContext:e});let s=n,t=o.defaultContext,r=!1,f=new Set,a=(c,d,y)=>{let g=(u,R,T)=>x(c,u,R,h(d,T),o),C=(u,R,T)=>g(u,[R],T)[R];return{policy:c,isLoading:y??!1,can:C,canAll:(u,R,T)=>R.every(b=>C(u,b,T)),canAny:(u,R,T)=>{let b=g(u,R,T);return Object.values(b).some(v=>v===!0)},canThese:g}},l=a(s,t,r),i=()=>{for(let c of f)c()};return{updatePolicy:(c,d,y)=>{s=c,d!==void 0&&(t=d),(y==null?void 0:y.isLoading)!==void 0&&(r=y.isLoading),l=a(s,t,r),i()},setLoading:c=>{r!==c&&(r=c,l=a(s,t,r),i())},subscribe:c=>(f.add(c),()=>f.delete(c)),getSnapshot:()=>l}};var A=class{statements=[];allow(e,o,s){return this.statements=[...this.statements,{resource:e,actions:o,effect:"allow",contexts:s==null?void 0:s.contexts}],this}deny(e,o,s){return this.statements=[...this.statements,{resource:e,actions:o,effect:"deny",contexts:s==null?void 0:s.contexts}],this}build(){return[...this.statements]}},B=()=>new A,D=(...n)=>n.flat();0&&(module.exports={PolicyBuilder,createAccessControl,definePolicy,evaluateAccess,evaluateAccessBulk,getAccessControl,mergePolicies});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/policy.ts","../src/core/policy-builder.ts"],"sourcesContent":["export * from \"./core/index\";\n","import type {\n AccessControlConfig,\n AccessControlOptions,\n AccessControlStore,\n CoreAccessControlType,\n TAccessControlPolicy,\n} from \"./types\";\n\ntype MatchedStatement = {\n effect: \"allow\" | \"deny\";\n specificity: number;\n index: number;\n};\n\n/**\n * Collects matching statements for a single action against pre-filtered relevant statements.\n * Deduplicates by (index, specificity) — a statement can only contribute once per specificity\n * level regardless of how many context combinations match it.\n * Breaks early on the first input context that satisfies a policy condition.\n */\nconst collectMatchedStatements = <T extends AccessControlConfig>(\n relevantStatements: TAccessControlPolicy<T>,\n action: string,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n inputContexts: Record<string, any>[],\n): MatchedStatement[] => {\n const matched: MatchedStatement[] = [];\n const seen = new Set<string>();\n\n relevantStatements.forEach((stmt, index) => {\n // biome-ignore lint/suspicious/noExplicitAny: action is a string subtype\n const actionMatches = stmt.actions.includes(\"*\") || stmt.actions.includes(action as any);\n if (!actionMatches) return;\n\n const policyConditions = stmt.contexts || [];\n\n // No conditions — matches everything at specificity 0\n if (policyConditions.length === 0) {\n matched.push({ effect: stmt.effect, specificity: 0, index });\n return;\n }\n\n if (inputContexts.length === 0) return;\n\n // OR logic: any policy condition matching any input context is a match\n for (const policyCondition of policyConditions) {\n const conditionKeys = Object.keys(policyCondition);\n const specificity = conditionKeys.length;\n const dedupeKey = `${index}:${specificity}`;\n if (seen.has(dedupeKey)) continue;\n\n for (const inputContext of inputContexts) {\n const allKeysMatch = conditionKeys.every(\n (k) => inputContext[k] === policyCondition[k],\n );\n if (allKeysMatch) {\n seen.add(dedupeKey);\n matched.push({ effect: stmt.effect, specificity, index });\n break; // No need to check further input contexts for this condition\n }\n }\n }\n });\n\n return matched;\n};\n\n/**\n * Resolves a non-empty list of matched statements to allow/deny using the given strategy.\n */\nconst resolveConflict = (\n matchedStatements: MatchedStatement[],\n strategy: string,\n): boolean => {\n if (strategy === \"firstWins\") {\n matchedStatements.sort((a, b) => a.index - b.index);\n return matchedStatements[0].effect === \"allow\";\n }\n\n if (strategy === \"lastWins\") {\n matchedStatements.sort((a, b) => b.index - a.index);\n return matchedStatements[0].effect === \"allow\";\n }\n\n // Default: \"denyWins\" — most specific statements take precedence; deny wins among ties\n matchedStatements.sort((a, b) => b.specificity - a.specificity);\n const maxSpecificity = matchedStatements[0].specificity;\n const mostSpecific = matchedStatements.filter((s) => s.specificity === maxSpecificity);\n return !mostSpecific.some((s) => s.effect === \"deny\");\n};\n\n/**\n * Pure function to evaluate access against a specific policy state.\n * This helper ensures logic is consistent across both static and dynamic implementations.\n */\nexport const evaluateAccess = <T extends AccessControlConfig, R extends keyof T>(\n policy: TAccessControlPolicy<T>,\n resource: R,\n action: T[R][number],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n options?: AccessControlOptions,\n): boolean => {\n const inputContexts = Array.isArray(context) ? context : context ? [context] : [];\n const relevantStatements = policy.filter((stmt) => stmt.resource === resource);\n const matchedStatements = collectMatchedStatements(relevantStatements, action, inputContexts);\n\n if (matchedStatements.length === 0) return false;\n\n return resolveConflict(matchedStatements, options?.conflictResolution ?? \"denyWins\");\n};\n\n/**\n * Bulk version of evaluateAccess to check multiple actions on the same resource\n * with the same context. Minimizes redundant policy filtering and context processing.\n */\nexport const evaluateAccessBulk = <T extends AccessControlConfig, R extends keyof T>(\n policy: TAccessControlPolicy<T>,\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n options?: AccessControlOptions,\n): Record<T[R][number], boolean> => {\n const results = {} as Record<T[R][number], boolean>;\n\n if (actions.length === 0) return results;\n\n // Normalize context and filter statements ONCE for all actions\n const inputContexts = Array.isArray(context) ? context : context ? [context] : [];\n const relevantStatements = policy.filter((stmt) => stmt.resource === resource);\n\n if (relevantStatements.length === 0) {\n for (const action of actions) results[action] = false;\n return results;\n }\n\n const strategy = options?.conflictResolution ?? \"denyWins\";\n\n for (const action of actions) {\n const matchedStatements = collectMatchedStatements(relevantStatements, action, inputContexts);\n results[action] = matchedStatements.length === 0\n ? false\n : resolveConflict(matchedStatements, strategy);\n }\n\n return results;\n};\n\n/**\n * Merges a default context into an explicit context.\n * Default context acts as a base — explicit context keys override default ones.\n */\nconst mergeContext = (\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n defaultContext?: Record<string, any>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n explicitContext?: Record<string, any> | Record<string, any>[],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n): Record<string, any> | Record<string, any>[] | undefined => {\n if (!defaultContext) return explicitContext;\n if (!explicitContext) return defaultContext;\n if (Array.isArray(explicitContext)) {\n return explicitContext.map((c) => ({ ...defaultContext, ...c }));\n }\n return { ...defaultContext, ...explicitContext };\n};\n\n/**\n * Creates a static access control interface.\n * Ideal for server-side use (e.g., API routes, Server Components) where the policy is fixed per request.\n *\n * @param accessControlPolicy - The policy to evaluate.\n * @param options - Optional configuration options (default context, conflict resolution).\n * @returns An object containing `can`, `canAll`, and `canAny` functions.\n */\nexport const getAccessControl = <T extends AccessControlConfig>(\n accessControlPolicy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n optionsOrContext?: AccessControlOptions | Record<string, any>,\n): CoreAccessControlType<T> => {\n // Backward compatibility: if second arg is a plain object without known option keys, treat as context\n let options: AccessControlOptions = {};\n if (optionsOrContext) {\n if (\"conflictResolution\" in optionsOrContext || \"defaultContext\" in optionsOrContext) {\n options = optionsOrContext as AccessControlOptions;\n } else {\n options = { defaultContext: optionsOrContext };\n }\n }\n\n const { defaultContext } = options;\n\n const can = <R extends keyof T>(\n resource: R,\n action: T[R][number],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n return evaluateAccess(\n accessControlPolicy,\n resource,\n action,\n mergeContext(defaultContext, context),\n options,\n );\n };\n\n const canAll = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n const results = canThese(resource, actions, context);\n return Object.values(results).every((v) => v === true);\n };\n\n const canAny = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n const results = canThese(resource, actions, context);\n return Object.values(results).some((v) => v === true);\n };\n\n const canThese = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): Record<T[R][number], boolean> => {\n return evaluateAccessBulk(\n accessControlPolicy,\n resource,\n actions,\n mergeContext(defaultContext, context),\n options,\n );\n };\n\n return {\n policy: accessControlPolicy,\n isLoading: false, // Static policies are never loading\n can,\n canAll,\n canAny,\n canThese,\n };\n};\n\n/**\n * Creates an updatable access control store with subscription capabilities.\n * Ideal for client-side use where the policy may load asynchronously or change over time.\n *\n * @param initialPolicy - The initial policy to use.\n * @param options - Optional configuration options (default context, conflict resolution).\n * @returns An object containing policy updater, subscription method, and snapshot.\n */\nexport const createAccessControl = <T extends AccessControlConfig>(\n initialPolicy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n optionsOrContext?: AccessControlOptions | Record<string, any>,\n): AccessControlStore<T> => {\n // Backward compatibility handling\n let options: AccessControlOptions = {};\n if (optionsOrContext) {\n if (\"conflictResolution\" in optionsOrContext || \"defaultContext\" in optionsOrContext) {\n options = optionsOrContext as AccessControlOptions;\n } else {\n options = { defaultContext: optionsOrContext };\n }\n }\n\n let currentPolicy = initialPolicy;\n let currentDefaultContext = options.defaultContext;\n let currentIsLoading = false;\n \n const listeners = new Set<() => void>();\n\n // Build a snapshot with all check methods bound to a specific policy.\n // Cached and only rebuilt on updatePolicy/setLoading calls.\n const buildSnapshot = (\n policy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n defCtx?: Record<string, any>,\n loading?: boolean,\n ): CoreAccessControlType<T> => {\n const snapshotCanThese = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): Record<T[R][number], boolean> =>\n evaluateAccessBulk(policy, resource, actions, mergeContext(defCtx, context), options);\n\n const snapshotCan = <R extends keyof T>(\n resource: R,\n action: T[R][number],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => snapshotCanThese(resource, [action], context)[action];\n\n return {\n policy,\n isLoading: loading ?? false,\n can: snapshotCan,\n canAll: <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => actions.every((a) => snapshotCan(resource, a, context)),\n canAny: <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n const results = snapshotCanThese(resource, actions, context);\n return Object.values(results).some((a) => a === true);\n },\n canThese: snapshotCanThese,\n };\n };\n\n let snapshot = buildSnapshot(currentPolicy, currentDefaultContext, currentIsLoading);\n\n const notifyListeners = () => {\n for (const listener of listeners) {\n listener();\n }\n };\n\n return {\n updatePolicy: (\n newPolicy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n defaultContext?: Record<string, any>,\n updateOptions?: { isLoading?: boolean }\n ) => {\n currentPolicy = newPolicy;\n if (defaultContext !== undefined) {\n currentDefaultContext = defaultContext;\n }\n if (updateOptions?.isLoading !== undefined) {\n currentIsLoading = updateOptions.isLoading;\n }\n snapshot = buildSnapshot(currentPolicy, currentDefaultContext, currentIsLoading);\n notifyListeners();\n },\n setLoading: (isLoading: boolean) => {\n if (currentIsLoading === isLoading) return;\n currentIsLoading = isLoading;\n snapshot = buildSnapshot(currentPolicy, currentDefaultContext, currentIsLoading);\n notifyListeners();\n },\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n getSnapshot: () => snapshot,\n };\n};","import type {\n\tAccessControlConfig,\n\tTAccessControlPolicy,\n} from \"./types\";\n\n/**\n * A fluent builder class to help create access control policies.\n * Use `definePolicy()` to start a chain.\n */\nexport class PolicyBuilder<T extends AccessControlConfig> {\n\tprivate statements: TAccessControlPolicy<T> = [];\n\n\t/**\n\t * Allows one or more actions on a specific resource.\n\t * @param resource The resource to grant access to.\n\t * @param actions The actions to allow (or \"*\" for all).\n\t * @param options Optional configuration, like ABAC contexts.\n\t */\n\tallow<R extends keyof T>(\n\t\tresource: R,\n\t\tactions: readonly (T[R][number] | \"*\" | \"\")[],\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n\t\toptions?: { contexts?: readonly Record<string, any>[] },\n\t): this {\n\t\tthis.statements = [\n\t\t\t...this.statements,\n\t\t\t{\n\t\t\t\tresource,\n\t\t\t\tactions,\n\t\t\t\teffect: \"allow\",\n\t\t\t\tcontexts: options?.contexts,\n\t\t\t},\n\t\t] as unknown as TAccessControlPolicy<T>;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Denies one or more actions on a specific resource.\n\t * @param resource The resource to deny access to.\n\t * @param actions The actions to deny (or \"*\" for all).\n\t * @param options Optional configuration, like ABAC contexts.\n\t */\n\tdeny<R extends keyof T>(\n\t\tresource: R,\n\t\tactions: readonly (T[R][number] | \"*\" | \"\")[],\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n\t\toptions?: { contexts?: readonly Record<string, any>[] },\n\t): this {\n\t\tthis.statements = [\n\t\t\t...this.statements,\n\t\t\t{\n\t\t\t\tresource,\n\t\t\t\tactions,\n\t\t\t\teffect: \"deny\",\n\t\t\t\tcontexts: options?.contexts,\n\t\t\t},\n\t\t] as unknown as TAccessControlPolicy<T>;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the constructed policy as a plain array.\n\t * This array is fully serializable and can be merged with other policies.\n\t */\n\tbuild(): TAccessControlPolicy<T> {\n\t\treturn [...this.statements];\n\t}\n}\n\n/**\n * Helper to start building a policy using the fluent API.\n * @returns A new PolicyBuilder instance.\n */\nexport const definePolicy = <T extends AccessControlConfig>(): PolicyBuilder<T> => {\n\treturn new PolicyBuilder<T>();\n};\n\n/**\n * Merges multiple policy arrays into a single policy array.\n * Useful for combining static policies with dynamic ones fetched from a backend.\n * @param policies The list of policy arrays to merge.\n * @returns A single flattened policy array.\n */\nexport const mergePolicies = <T extends AccessControlConfig>(\n\t...policies: TAccessControlPolicy<T>[]\n): TAccessControlPolicy<T> => {\n\treturn policies.flat() as unknown as TAccessControlPolicy<T>;\n};\n"],"mappings":"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","names":["index_exports","__export","PolicyBuilder","createAccessControl","definePolicy","evaluateAccess","evaluateAccessBulk","getAccessControl","mergePolicies","__toCommonJS","collectMatchedStatements","relevantStatements","action","inputContexts","matched","seen","stmt","index","policyConditions","policyCondition","conditionKeys","specificity","dedupeKey","inputContext","k","resolveConflict","matchedStatements","strategy","a","b","maxSpecificity","s","evaluateAccess","policy","resource","context","options","evaluateAccessBulk","actions","results","mergeContext","defaultContext","explicitContext","c","getAccessControl","accessControlPolicy","optionsOrContext","can","canAll","canThese","v","canAny","createAccessControl","initialPolicy","currentPolicy","currentDefaultContext","currentIsLoading","listeners","buildSnapshot","defCtx","loading","snapshotCanThese","snapshotCan","snapshot","notifyListeners","listener","newPolicy","updateOptions","isLoading","PolicyBuilder","resource","actions","options","definePolicy","mergePolicies","policies"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var h=(t,e,o)=>{let n=[],r=new Set;return t.forEach((s,f)=>{if(!(s.actions.includes("*")||s.actions.includes(e)))return;let l=s.contexts||[];if(l.length===0){n.push({effect:s.effect,specificity:0,index:f});return}if(o.length!==0)for(let i of l){let c=Object.keys(i),d=c.length,y=`${f}:${d}`;if(!r.has(y)){for(let g of o)if(c.every(u=>g[u]===i[u])){r.add(y),n.push({effect:s.effect,specificity:d,index:f});break}}}}),n},x=(t,e)=>{if(e==="firstWins")return t.sort((r,s)=>r.index-s.index),t[0].effect==="allow";if(e==="lastWins")return t.sort((r,s)=>s.index-r.index),t[0].effect==="allow";t.sort((r,s)=>s.specificity-r.specificity);let o=t[0].specificity;return!t.filter(r=>r.specificity===o).some(r=>r.effect==="deny")},k=(t,e,o,n,r)=>{let s=Array.isArray(n)?n:n?[n]:[],f=t.filter(l=>l.resource===e),a=h(f,o,s);return a.length===0?!1:x(a,(r==null?void 0:r.conflictResolution)??"denyWins")},p=(t,e,o,n,r)=>{let s={};if(o.length===0)return s;let f=Array.isArray(n)?n:n?[n]:[],a=t.filter(i=>i.resource===e);if(a.length===0){for(let i of o)s[i]=!1;return s}let l=(r==null?void 0:r.conflictResolution)??"denyWins";for(let i of o){let c=h(a,i,f);s[i]=c.length===0?!1:x(c,l)}return s},b=(t,e)=>t?e?Array.isArray(e)?e.map(o=>({...t,...o})):{...t,...e}:t:e,v=(t,e)=>{let o={};e&&("conflictResolution"in e||"defaultContext"in e?o=e:o={defaultContext:e});let{defaultContext:n}=o,r=(l,i,c)=>k(t,l,i,b(n,c),o),s=(l,i,c)=>{let d=a(l,i,c);return Object.values(d).every(y=>y===!0)},f=(l,i,c)=>{let d=a(l,i,c);return Object.values(d).some(y=>y===!0)},a=(l,i,c)=>p(t,l,i,b(n,c),o);return{policy:t,isLoading:!1,can:r,canAll:s,canAny:f,canThese:a}},S=(t,e)=>{let o={};e&&("conflictResolution"in e||"defaultContext"in e?o=e:o={defaultContext:e});let n=t,r=o.defaultContext,s=!1,f=new Set,a=(c,d,y)=>{let g=(u,R,T)=>p(c,u,R,b(d,T),o),A=(u,R,T)=>g(u,[R],T)[R];return{policy:c,isLoading:y??!1,can:A,canAll:(u,R,T)=>R.every(C=>A(u,C,T)),canAny:(u,R,T)=>{let C=g(u,R,T);return Object.values(C).some(P=>P===!0)},canThese:g}},l=a(n,r,s),i=()=>{for(let c of f)c()};return{updatePolicy:(c,d,y)=>{n=c,d!==void 0&&(r=d),(y==null?void 0:y.isLoading)!==void 0&&(s=y.isLoading),l=a(n,r,s),i()},setLoading:c=>{s!==c&&(s=c,l=a(n,r,s),i())},subscribe:c=>(f.add(c),()=>f.delete(c)),getSnapshot:()=>l}};var m=class{statements=[];allow(e,o,n){return this.statements=[...this.statements,{resource:e,actions:o,effect:"allow",contexts:n==null?void 0:n.contexts}],this}deny(e,o,n){return this.statements=[...this.statements,{resource:e,actions:o,effect:"deny",contexts:n==null?void 0:n.contexts}],this}build(){return[...this.statements]}},M=()=>new m,L=(...t)=>t.flat();export{m as PolicyBuilder,S as createAccessControl,M as definePolicy,k as evaluateAccess,p as evaluateAccessBulk,v as getAccessControl,L as mergePolicies};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/policy.ts","../src/core/policy-builder.ts"],"sourcesContent":["import type {\n AccessControlConfig,\n AccessControlOptions,\n AccessControlStore,\n CoreAccessControlType,\n TAccessControlPolicy,\n} from \"./types\";\n\ntype MatchedStatement = {\n effect: \"allow\" | \"deny\";\n specificity: number;\n index: number;\n};\n\n/**\n * Collects matching statements for a single action against pre-filtered relevant statements.\n * Deduplicates by (index, specificity) — a statement can only contribute once per specificity\n * level regardless of how many context combinations match it.\n * Breaks early on the first input context that satisfies a policy condition.\n */\nconst collectMatchedStatements = <T extends AccessControlConfig>(\n relevantStatements: TAccessControlPolicy<T>,\n action: string,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n inputContexts: Record<string, any>[],\n): MatchedStatement[] => {\n const matched: MatchedStatement[] = [];\n const seen = new Set<string>();\n\n relevantStatements.forEach((stmt, index) => {\n // biome-ignore lint/suspicious/noExplicitAny: action is a string subtype\n const actionMatches = stmt.actions.includes(\"*\") || stmt.actions.includes(action as any);\n if (!actionMatches) return;\n\n const policyConditions = stmt.contexts || [];\n\n // No conditions — matches everything at specificity 0\n if (policyConditions.length === 0) {\n matched.push({ effect: stmt.effect, specificity: 0, index });\n return;\n }\n\n if (inputContexts.length === 0) return;\n\n // OR logic: any policy condition matching any input context is a match\n for (const policyCondition of policyConditions) {\n const conditionKeys = Object.keys(policyCondition);\n const specificity = conditionKeys.length;\n const dedupeKey = `${index}:${specificity}`;\n if (seen.has(dedupeKey)) continue;\n\n for (const inputContext of inputContexts) {\n const allKeysMatch = conditionKeys.every(\n (k) => inputContext[k] === policyCondition[k],\n );\n if (allKeysMatch) {\n seen.add(dedupeKey);\n matched.push({ effect: stmt.effect, specificity, index });\n break; // No need to check further input contexts for this condition\n }\n }\n }\n });\n\n return matched;\n};\n\n/**\n * Resolves a non-empty list of matched statements to allow/deny using the given strategy.\n */\nconst resolveConflict = (\n matchedStatements: MatchedStatement[],\n strategy: string,\n): boolean => {\n if (strategy === \"firstWins\") {\n matchedStatements.sort((a, b) => a.index - b.index);\n return matchedStatements[0].effect === \"allow\";\n }\n\n if (strategy === \"lastWins\") {\n matchedStatements.sort((a, b) => b.index - a.index);\n return matchedStatements[0].effect === \"allow\";\n }\n\n // Default: \"denyWins\" — most specific statements take precedence; deny wins among ties\n matchedStatements.sort((a, b) => b.specificity - a.specificity);\n const maxSpecificity = matchedStatements[0].specificity;\n const mostSpecific = matchedStatements.filter((s) => s.specificity === maxSpecificity);\n return !mostSpecific.some((s) => s.effect === \"deny\");\n};\n\n/**\n * Pure function to evaluate access against a specific policy state.\n * This helper ensures logic is consistent across both static and dynamic implementations.\n */\nexport const evaluateAccess = <T extends AccessControlConfig, R extends keyof T>(\n policy: TAccessControlPolicy<T>,\n resource: R,\n action: T[R][number],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n options?: AccessControlOptions,\n): boolean => {\n const inputContexts = Array.isArray(context) ? context : context ? [context] : [];\n const relevantStatements = policy.filter((stmt) => stmt.resource === resource);\n const matchedStatements = collectMatchedStatements(relevantStatements, action, inputContexts);\n\n if (matchedStatements.length === 0) return false;\n\n return resolveConflict(matchedStatements, options?.conflictResolution ?? \"denyWins\");\n};\n\n/**\n * Bulk version of evaluateAccess to check multiple actions on the same resource\n * with the same context. Minimizes redundant policy filtering and context processing.\n */\nexport const evaluateAccessBulk = <T extends AccessControlConfig, R extends keyof T>(\n policy: TAccessControlPolicy<T>,\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n options?: AccessControlOptions,\n): Record<T[R][number], boolean> => {\n const results = {} as Record<T[R][number], boolean>;\n\n if (actions.length === 0) return results;\n\n // Normalize context and filter statements ONCE for all actions\n const inputContexts = Array.isArray(context) ? context : context ? [context] : [];\n const relevantStatements = policy.filter((stmt) => stmt.resource === resource);\n\n if (relevantStatements.length === 0) {\n for (const action of actions) results[action] = false;\n return results;\n }\n\n const strategy = options?.conflictResolution ?? \"denyWins\";\n\n for (const action of actions) {\n const matchedStatements = collectMatchedStatements(relevantStatements, action, inputContexts);\n results[action] = matchedStatements.length === 0\n ? false\n : resolveConflict(matchedStatements, strategy);\n }\n\n return results;\n};\n\n/**\n * Merges a default context into an explicit context.\n * Default context acts as a base — explicit context keys override default ones.\n */\nconst mergeContext = (\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n defaultContext?: Record<string, any>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n explicitContext?: Record<string, any> | Record<string, any>[],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n): Record<string, any> | Record<string, any>[] | undefined => {\n if (!defaultContext) return explicitContext;\n if (!explicitContext) return defaultContext;\n if (Array.isArray(explicitContext)) {\n return explicitContext.map((c) => ({ ...defaultContext, ...c }));\n }\n return { ...defaultContext, ...explicitContext };\n};\n\n/**\n * Creates a static access control interface.\n * Ideal for server-side use (e.g., API routes, Server Components) where the policy is fixed per request.\n *\n * @param accessControlPolicy - The policy to evaluate.\n * @param options - Optional configuration options (default context, conflict resolution).\n * @returns An object containing `can`, `canAll`, and `canAny` functions.\n */\nexport const getAccessControl = <T extends AccessControlConfig>(\n accessControlPolicy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n optionsOrContext?: AccessControlOptions | Record<string, any>,\n): CoreAccessControlType<T> => {\n // Backward compatibility: if second arg is a plain object without known option keys, treat as context\n let options: AccessControlOptions = {};\n if (optionsOrContext) {\n if (\"conflictResolution\" in optionsOrContext || \"defaultContext\" in optionsOrContext) {\n options = optionsOrContext as AccessControlOptions;\n } else {\n options = { defaultContext: optionsOrContext };\n }\n }\n\n const { defaultContext } = options;\n\n const can = <R extends keyof T>(\n resource: R,\n action: T[R][number],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n return evaluateAccess(\n accessControlPolicy,\n resource,\n action,\n mergeContext(defaultContext, context),\n options,\n );\n };\n\n const canAll = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n const results = canThese(resource, actions, context);\n return Object.values(results).every((v) => v === true);\n };\n\n const canAny = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n const results = canThese(resource, actions, context);\n return Object.values(results).some((v) => v === true);\n };\n\n const canThese = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): Record<T[R][number], boolean> => {\n return evaluateAccessBulk(\n accessControlPolicy,\n resource,\n actions,\n mergeContext(defaultContext, context),\n options,\n );\n };\n\n return {\n policy: accessControlPolicy,\n isLoading: false, // Static policies are never loading\n can,\n canAll,\n canAny,\n canThese,\n };\n};\n\n/**\n * Creates an updatable access control store with subscription capabilities.\n * Ideal for client-side use where the policy may load asynchronously or change over time.\n *\n * @param initialPolicy - The initial policy to use.\n * @param options - Optional configuration options (default context, conflict resolution).\n * @returns An object containing policy updater, subscription method, and snapshot.\n */\nexport const createAccessControl = <T extends AccessControlConfig>(\n initialPolicy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n optionsOrContext?: AccessControlOptions | Record<string, any>,\n): AccessControlStore<T> => {\n // Backward compatibility handling\n let options: AccessControlOptions = {};\n if (optionsOrContext) {\n if (\"conflictResolution\" in optionsOrContext || \"defaultContext\" in optionsOrContext) {\n options = optionsOrContext as AccessControlOptions;\n } else {\n options = { defaultContext: optionsOrContext };\n }\n }\n\n let currentPolicy = initialPolicy;\n let currentDefaultContext = options.defaultContext;\n let currentIsLoading = false;\n \n const listeners = new Set<() => void>();\n\n // Build a snapshot with all check methods bound to a specific policy.\n // Cached and only rebuilt on updatePolicy/setLoading calls.\n const buildSnapshot = (\n policy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n defCtx?: Record<string, any>,\n loading?: boolean,\n ): CoreAccessControlType<T> => {\n const snapshotCanThese = <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): Record<T[R][number], boolean> =>\n evaluateAccessBulk(policy, resource, actions, mergeContext(defCtx, context), options);\n\n const snapshotCan = <R extends keyof T>(\n resource: R,\n action: T[R][number],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => snapshotCanThese(resource, [action], context)[action];\n\n return {\n policy,\n isLoading: loading ?? false,\n can: snapshotCan,\n canAll: <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => actions.every((a) => snapshotCan(resource, a, context)),\n canAny: <R extends keyof T>(\n resource: R,\n actions: T[R][number][],\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n context?: Record<string, any> | Record<string, any>[],\n ): boolean => {\n const results = snapshotCanThese(resource, actions, context);\n return Object.values(results).some((a) => a === true);\n },\n canThese: snapshotCanThese,\n };\n };\n\n let snapshot = buildSnapshot(currentPolicy, currentDefaultContext, currentIsLoading);\n\n const notifyListeners = () => {\n for (const listener of listeners) {\n listener();\n }\n };\n\n return {\n updatePolicy: (\n newPolicy: TAccessControlPolicy<T>,\n // biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n defaultContext?: Record<string, any>,\n updateOptions?: { isLoading?: boolean }\n ) => {\n currentPolicy = newPolicy;\n if (defaultContext !== undefined) {\n currentDefaultContext = defaultContext;\n }\n if (updateOptions?.isLoading !== undefined) {\n currentIsLoading = updateOptions.isLoading;\n }\n snapshot = buildSnapshot(currentPolicy, currentDefaultContext, currentIsLoading);\n notifyListeners();\n },\n setLoading: (isLoading: boolean) => {\n if (currentIsLoading === isLoading) return;\n currentIsLoading = isLoading;\n snapshot = buildSnapshot(currentPolicy, currentDefaultContext, currentIsLoading);\n notifyListeners();\n },\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n getSnapshot: () => snapshot,\n };\n};","import type {\n\tAccessControlConfig,\n\tTAccessControlPolicy,\n} from \"./types\";\n\n/**\n * A fluent builder class to help create access control policies.\n * Use `definePolicy()` to start a chain.\n */\nexport class PolicyBuilder<T extends AccessControlConfig> {\n\tprivate statements: TAccessControlPolicy<T> = [];\n\n\t/**\n\t * Allows one or more actions on a specific resource.\n\t * @param resource The resource to grant access to.\n\t * @param actions The actions to allow (or \"*\" for all).\n\t * @param options Optional configuration, like ABAC contexts.\n\t */\n\tallow<R extends keyof T>(\n\t\tresource: R,\n\t\tactions: readonly (T[R][number] | \"*\" | \"\")[],\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n\t\toptions?: { contexts?: readonly Record<string, any>[] },\n\t): this {\n\t\tthis.statements = [\n\t\t\t...this.statements,\n\t\t\t{\n\t\t\t\tresource,\n\t\t\t\tactions,\n\t\t\t\teffect: \"allow\",\n\t\t\t\tcontexts: options?.contexts,\n\t\t\t},\n\t\t] as unknown as TAccessControlPolicy<T>;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Denies one or more actions on a specific resource.\n\t * @param resource The resource to deny access to.\n\t * @param actions The actions to deny (or \"*\" for all).\n\t * @param options Optional configuration, like ABAC contexts.\n\t */\n\tdeny<R extends keyof T>(\n\t\tresource: R,\n\t\tactions: readonly (T[R][number] | \"*\" | \"\")[],\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Context can have any value type\n\t\toptions?: { contexts?: readonly Record<string, any>[] },\n\t): this {\n\t\tthis.statements = [\n\t\t\t...this.statements,\n\t\t\t{\n\t\t\t\tresource,\n\t\t\t\tactions,\n\t\t\t\teffect: \"deny\",\n\t\t\t\tcontexts: options?.contexts,\n\t\t\t},\n\t\t] as unknown as TAccessControlPolicy<T>;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Returns the constructed policy as a plain array.\n\t * This array is fully serializable and can be merged with other policies.\n\t */\n\tbuild(): TAccessControlPolicy<T> {\n\t\treturn [...this.statements];\n\t}\n}\n\n/**\n * Helper to start building a policy using the fluent API.\n * @returns A new PolicyBuilder instance.\n */\nexport const definePolicy = <T extends AccessControlConfig>(): PolicyBuilder<T> => {\n\treturn new PolicyBuilder<T>();\n};\n\n/**\n * Merges multiple policy arrays into a single policy array.\n * Useful for combining static policies with dynamic ones fetched from a backend.\n * @param policies The list of policy arrays to merge.\n * @returns A single flattened policy array.\n */\nexport const mergePolicies = <T extends AccessControlConfig>(\n\t...policies: TAccessControlPolicy<T>[]\n): TAccessControlPolicy<T> => {\n\treturn policies.flat() as unknown as TAccessControlPolicy<T>;\n};\n"],"mappings":"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","names":["collectMatchedStatements","relevantStatements","action","inputContexts","matched","seen","stmt","index","policyConditions","policyCondition","conditionKeys","specificity","dedupeKey","inputContext","k","resolveConflict","matchedStatements","strategy","a","b","maxSpecificity","s","evaluateAccess","policy","resource","context","options","evaluateAccessBulk","actions","results","mergeContext","defaultContext","explicitContext","c","getAccessControl","accessControlPolicy","optionsOrContext","can","canAll","canThese","v","canAny","createAccessControl","initialPolicy","currentPolicy","currentDefaultContext","currentIsLoading","listeners","buildSnapshot","defCtx","loading","snapshotCanThese","snapshotCan","snapshot","notifyListeners","listener","newPolicy","updateOptions","isLoading","PolicyBuilder","resource","actions","options","definePolicy","mergePolicies","policies"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "access-control-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, type-safe access control library for TypeScript applications.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"lint": "biome check .",
|
|
24
|
+
"format": "biome check --apply .",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"prepare": "husky"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"access-control",
|
|
30
|
+
"rbac",
|
|
31
|
+
"abac",
|
|
32
|
+
"authorization",
|
|
33
|
+
"typescript"
|
|
34
|
+
],
|
|
35
|
+
"author": "Aashish Rai",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^1.9.4",
|
|
39
|
+
"@commitlint/cli": "^20.5.0",
|
|
40
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
41
|
+
"husky": "^9.1.7",
|
|
42
|
+
"lint-staged": "^16.4.0",
|
|
43
|
+
"tsup": "^8.3.5",
|
|
44
|
+
"typescript": "^5.7.2",
|
|
45
|
+
"vitest": "^2.1.8"
|
|
46
|
+
},
|
|
47
|
+
"lint-staged": {
|
|
48
|
+
"*.{ts,tsx,js,jsx,json}": "biome check --apply --no-errors-on-unmatched"
|
|
49
|
+
}
|
|
50
|
+
}
|