@diister/quick-permission 0.9.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +747 -0
- package/aggregation.ts +114 -0
- package/core/filtering.ts +70 -0
- package/core/matching.ts +82 -0
- package/core/merging.ts +143 -0
- package/dist/aggregation.d.ts +62 -0
- package/dist/aggregation.d.ts.map +1 -0
- package/dist/aggregation.js +97 -0
- package/dist/aggregation.js.map +1 -0
- package/dist/core/filtering.d.ts +35 -0
- package/dist/core/filtering.d.ts.map +1 -0
- package/dist/core/filtering.js +62 -0
- package/dist/core/filtering.js.map +1 -0
- package/dist/core/matching.d.ts +31 -0
- package/dist/core/matching.d.ts.map +1 -0
- package/dist/core/matching.js +75 -0
- package/dist/core/matching.js.map +1 -0
- package/dist/core/merging.d.ts +29 -0
- package/dist/core/merging.d.ts.map +1 -0
- package/dist/core/merging.js +124 -0
- package/dist/core/merging.js.map +1 -0
- package/dist/indirect-aggregation.d.ts +41 -0
- package/dist/indirect-aggregation.d.ts.map +1 -0
- package/dist/indirect-aggregation.js +185 -0
- package/dist/indirect-aggregation.js.map +1 -0
- package/dist/indirect-resource.d.ts +126 -0
- package/dist/indirect-resource.d.ts.map +1 -0
- package/dist/indirect-resource.js +109 -0
- package/dist/indirect-resource.js.map +1 -0
- package/dist/mod.d.ts +25 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +25 -0
- package/dist/mod.js.map +1 -0
- package/dist/mongo-query.d.ts +38 -0
- package/dist/mongo-query.d.ts.map +1 -0
- package/dist/mongo-query.js +88 -0
- package/dist/mongo-query.js.map +1 -0
- package/dist/permission.d.ts +57 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +60 -0
- package/dist/permission.js.map +1 -0
- package/dist/resource.d.ts +48 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +298 -0
- package/dist/resource.js.map +1 -0
- package/dist/rules.d.ts +106 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +183 -0
- package/dist/rules.js.map +1 -0
- package/dist/sift/core.d.ts +104 -0
- package/dist/sift/core.d.ts.map +1 -0
- package/dist/sift/core.js +248 -0
- package/dist/sift/core.js.map +1 -0
- package/dist/sift/index.d.ts +10 -0
- package/dist/sift/index.d.ts.map +1 -0
- package/dist/sift/index.js +18 -0
- package/dist/sift/index.js.map +1 -0
- package/dist/sift/operations.d.ts +87 -0
- package/dist/sift/operations.d.ts.map +1 -0
- package/dist/sift/operations.js +257 -0
- package/dist/sift/operations.js.map +1 -0
- package/dist/sift/utils.d.ts +12 -0
- package/dist/sift/utils.d.ts.map +1 -0
- package/dist/sift/utils.js +80 -0
- package/dist/sift/utils.js.map +1 -0
- package/dist/system.d.ts +113 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +712 -0
- package/dist/system.js.map +1 -0
- package/dist/target.d.ts +18 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/target.js +41 -0
- package/dist/target.js.map +1 -0
- package/dist/types.d.ts +345 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/indirect-aggregation.ts +216 -0
- package/indirect-resource.ts +205 -0
- package/mod.ts +81 -0
- package/mongo-query.ts +94 -0
- package/package.json +58 -0
- package/permission.ts +88 -0
- package/resource.ts +352 -0
- package/rules.ts +241 -0
- package/sift/MIT-LICENSE.txt +20 -0
- package/sift/core.ts +551 -0
- package/sift/index.ts +62 -0
- package/sift/operations.ts +449 -0
- package/sift/utils.ts +96 -0
- package/system.ts +974 -0
- package/target.ts +84 -0
- package/types.ts +408 -0
package/README.md
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
# Quick Permission - Library 2
|
|
2
|
+
|
|
3
|
+
A TypeScript permission management library that is **type-safe**, **flexible**, and **composable** with support for hierarchical permissions, field-level filtering, and metadata accumulation.
|
|
4
|
+
|
|
5
|
+
## 🎯 Key Features
|
|
6
|
+
|
|
7
|
+
### ✨ **Extreme Type Safety**
|
|
8
|
+
```typescript
|
|
9
|
+
// ✅ TypeScript enforces the correct argument type
|
|
10
|
+
await permSystem.can(user, "article.read", articleId); // Accepts ArticleId
|
|
11
|
+
await permSystem.can(user, "article.create"); // Does not accept argument
|
|
12
|
+
|
|
13
|
+
// ✅ Output type is inferred automatically
|
|
14
|
+
const result = await permSystem.can(user, "article.read", "article:1");
|
|
15
|
+
result.output?.filter // Type: Record<string, boolean> | undefined
|
|
16
|
+
result.output?.data // Type: any
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 🔥 **Field-Level Permissions with Accumulation**
|
|
20
|
+
```typescript
|
|
21
|
+
// Multiple permissions can contribute
|
|
22
|
+
// Owner permission : { filter: { _id, title, owner, salary } }
|
|
23
|
+
// Public permission : { filter: { _id, title, body } }
|
|
24
|
+
// Contributor permission : { filter: { views } }
|
|
25
|
+
|
|
26
|
+
const result = await permSystem.can(ownerUser, "article.read", "article:1");
|
|
27
|
+
// result.output.data = { _id, title, body, owner, salary, views }
|
|
28
|
+
// ↑ Union of all filters!
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 🎪 **Recursive Intermediate Permissions**
|
|
32
|
+
```typescript
|
|
33
|
+
"article.manage": intermediate((ctx) => [
|
|
34
|
+
{ ...ctx, key: "article.read" },
|
|
35
|
+
{ ...ctx, key: "article.update" },
|
|
36
|
+
{ ...ctx, key: "article.comment.manage", target: [ctx.target, "*"] }
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
// Recursive resolution with configurable max depth
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 🧩 **Providers + Rules Architecture**
|
|
43
|
+
```typescript
|
|
44
|
+
const permSystem = createPermissionSystem({
|
|
45
|
+
schemas: permissionsSchemas,
|
|
46
|
+
sources: [
|
|
47
|
+
directProvider(staticPerms),
|
|
48
|
+
ownerProvider(["article.read"], "article:*"),
|
|
49
|
+
customDatabaseProvider(),
|
|
50
|
+
],
|
|
51
|
+
rules: [TimeRule(), IpRule(), WithRule()],
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📚 Usage Guide
|
|
58
|
+
|
|
59
|
+
### Installation
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import {
|
|
63
|
+
createPermissionSystem,
|
|
64
|
+
permission,
|
|
65
|
+
intermediate,
|
|
66
|
+
FilterRule,
|
|
67
|
+
directProvider,
|
|
68
|
+
ownerProvider,
|
|
69
|
+
} from "./library_2/mod.ts";
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 1. Define Permissions
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { permission, intermediate, FilterRule } from "./library_2/mod.ts";
|
|
76
|
+
|
|
77
|
+
// Resource fetchers
|
|
78
|
+
async function getArticle(id: string) {
|
|
79
|
+
return db.articles.findById(id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getComment(ids: [string, string]) {
|
|
83
|
+
const [articleId, commentId] = ids;
|
|
84
|
+
return db.comments.findById(commentId, { articleId });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Permission schemas
|
|
88
|
+
const permissionsSchemas = {
|
|
89
|
+
// Simple permission
|
|
90
|
+
"article.create": permission(),
|
|
91
|
+
|
|
92
|
+
// Permission with context + field filtering
|
|
93
|
+
"article.read": permission(getArticle, [FilterRule()] as const),
|
|
94
|
+
|
|
95
|
+
// Permission with context but without rules
|
|
96
|
+
"article.update": permission(getArticle),
|
|
97
|
+
|
|
98
|
+
// Intermediate permission (expand to children)
|
|
99
|
+
"article.manage": intermediate(
|
|
100
|
+
(ctx) => [
|
|
101
|
+
{ subject: ctx.subject, key: "article.read", target: ctx.target },
|
|
102
|
+
{ subject: ctx.subject, key: "article.update", target: ctx.target },
|
|
103
|
+
{ subject: ctx.subject, key: "article.delete", target: ctx.target },
|
|
104
|
+
],
|
|
105
|
+
getArticle
|
|
106
|
+
),
|
|
107
|
+
|
|
108
|
+
// Permission with nested context
|
|
109
|
+
"article.comment.delete": permission(getComment),
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2. Create Providers
|
|
114
|
+
|
|
115
|
+
**Providers** are sources of permissions. They return the permissions for a subject.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { directProvider, ownerProvider } from "./library_2/mod.ts";
|
|
119
|
+
import type { PermissionProvider } from "./library_2/mod.ts";
|
|
120
|
+
|
|
121
|
+
// Provider 1: Static permissions
|
|
122
|
+
const staticPerms = directProvider([
|
|
123
|
+
{
|
|
124
|
+
subject: adminUser,
|
|
125
|
+
key: "article.create",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
subject: moderatorUser,
|
|
129
|
+
key: "article.update",
|
|
130
|
+
target: "article:*",
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// Provider 2: Ownership-based permissions
|
|
135
|
+
const ownerPerms = ownerProvider(
|
|
136
|
+
["article.read", "article.update", "article.delete"],
|
|
137
|
+
"article:*",
|
|
138
|
+
{
|
|
139
|
+
filter: { _id: true, title: true, body: true, owner: true }
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Provider 3: Custom provider (e.g., from database)
|
|
144
|
+
function databaseProvider(): PermissionProvider {
|
|
145
|
+
return {
|
|
146
|
+
provide: async (subject, _key, _target) => {
|
|
147
|
+
const userPerms = await db.permissions.find({ userId: subject.id });
|
|
148
|
+
return userPerms.map(p => ({
|
|
149
|
+
subject,
|
|
150
|
+
key: p.permissionKey,
|
|
151
|
+
target: p.target,
|
|
152
|
+
filter: p.filter,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Provider 4: Conditional provider
|
|
159
|
+
function publicArticleProvider(): PermissionProvider {
|
|
160
|
+
return {
|
|
161
|
+
provide: (subject, _key, _target) => {
|
|
162
|
+
return Promise.resolve([
|
|
163
|
+
{
|
|
164
|
+
key: "article.read",
|
|
165
|
+
subject,
|
|
166
|
+
target: "*",
|
|
167
|
+
with: { public: true }, // Condition: article must be public
|
|
168
|
+
filter: { _id: true, title: true, body: true },
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 3. Create the Permission System
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { createPermissionSystem, TimeRule, IpRule, WithRule } from "./library_2/mod.ts";
|
|
180
|
+
|
|
181
|
+
const permSystem = createPermissionSystem({
|
|
182
|
+
schemas: permissionsSchemas,
|
|
183
|
+
|
|
184
|
+
sources: [
|
|
185
|
+
staticPerms,
|
|
186
|
+
ownerPerms,
|
|
187
|
+
publicArticleProvider(),
|
|
188
|
+
databaseProvider(),
|
|
189
|
+
],
|
|
190
|
+
|
|
191
|
+
// Global validation rules (applied to ALL permissions)
|
|
192
|
+
rules: [
|
|
193
|
+
TimeRule(), // Validate time constraints (startDate, endDate)
|
|
194
|
+
IpRule(), // Validate IP restrictions
|
|
195
|
+
WithRule(), // Validate resource constraints
|
|
196
|
+
] as const,
|
|
197
|
+
|
|
198
|
+
// Max depth for recursive intermediate resolution
|
|
199
|
+
maxIntermediateDepth: 10,
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 4. Check Permissions
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const user = { id: "user:123" };
|
|
207
|
+
|
|
208
|
+
// Permission without context
|
|
209
|
+
const canCreate = await permSystem.can(user, "article.create");
|
|
210
|
+
if (canCreate.ok) {
|
|
211
|
+
// User can create articles
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Permission with context (target)
|
|
215
|
+
const canRead = await permSystem.can(user, "article.read", "article:1");
|
|
216
|
+
if (canRead.ok) {
|
|
217
|
+
// User can read article:1
|
|
218
|
+
console.log("Allowed fields:", canRead.output?.filter);
|
|
219
|
+
console.log("Filtered data:", canRead.output?.data);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create a checker with shared context + cache
|
|
223
|
+
const checker = permSystem.context({
|
|
224
|
+
subject: user,
|
|
225
|
+
checkDate: new Date("2024-01-01"),
|
|
226
|
+
ips: ["192.168.1.1"],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// All calls to checker.can() share the same cache!
|
|
230
|
+
const r1 = await checker.can("article.read", "article:1"); // FETCH
|
|
231
|
+
const r2 = await checker.can("article.update", "article:1"); // CACHE HIT
|
|
232
|
+
const r3 = await checker.can("article.delete", "article:1"); // CACHE HIT
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 🧪 Complete Examples
|
|
238
|
+
|
|
239
|
+
### Example 1: Blog with Field Filtering
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { createPermissionSystem, permission, FilterRule, ownerProvider } from "./library_2/mod.ts";
|
|
243
|
+
|
|
244
|
+
type Article = {
|
|
245
|
+
_id: string;
|
|
246
|
+
title: string;
|
|
247
|
+
body: string;
|
|
248
|
+
owner: string;
|
|
249
|
+
salary?: number;
|
|
250
|
+
secret?: string;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
async function getArticle(id: string): Promise<Article | undefined> {
|
|
254
|
+
return db.articles.findById(id);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const schemas = {
|
|
258
|
+
"article.read": permission(getArticle, [FilterRule()] as const),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const permSystem = createPermissionSystem({
|
|
262
|
+
schemas,
|
|
263
|
+
sources: [
|
|
264
|
+
// Public users see basic fields
|
|
265
|
+
publicProvider({
|
|
266
|
+
filter: { _id: true, title: true, body: true }
|
|
267
|
+
}),
|
|
268
|
+
|
|
269
|
+
// Owners see more fields
|
|
270
|
+
ownerProvider(["article.read"], "article:*", {
|
|
271
|
+
filter: { _id: true, title: true, body: true, owner: true, salary: true }
|
|
272
|
+
}),
|
|
273
|
+
|
|
274
|
+
// Admins see everything
|
|
275
|
+
adminProvider({
|
|
276
|
+
filter: { _id: true, title: true, body: true, owner: true, salary: true, secret: true }
|
|
277
|
+
}),
|
|
278
|
+
],
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Usage
|
|
282
|
+
const result = await permSystem.can(publicUser, "article.read", "article:1");
|
|
283
|
+
// result.output.data = { _id, title, body } ← filtered automatically
|
|
284
|
+
|
|
285
|
+
const result2 = await permSystem.can(ownerUser, "article.read", "article:1");
|
|
286
|
+
// result2.output.data = { _id, title, body, owner, salary } ← more fields
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Example 2: Hierarchical Permissions
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const schemas = {
|
|
293
|
+
"article.read": permission(getArticle),
|
|
294
|
+
"article.update": permission(getArticle),
|
|
295
|
+
"article.delete": permission(getArticle),
|
|
296
|
+
|
|
297
|
+
// Intermediate permission
|
|
298
|
+
"article.manage": intermediate(
|
|
299
|
+
(ctx) => [
|
|
300
|
+
{ ...ctx, key: "article.read" },
|
|
301
|
+
{ ...ctx, key: "article.update" },
|
|
302
|
+
{ ...ctx, key: "article.delete" },
|
|
303
|
+
],
|
|
304
|
+
getArticle
|
|
305
|
+
),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const permSystem = createPermissionSystem({
|
|
309
|
+
schemas,
|
|
310
|
+
sources: [
|
|
311
|
+
directProvider([
|
|
312
|
+
{ subject: admin, key: "article.manage", target: "*" }
|
|
313
|
+
])
|
|
314
|
+
],
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Admin has article.manage, which expands to read + update + delete
|
|
318
|
+
const canRead = await permSystem.can(admin, "article.read", "article:1");
|
|
319
|
+
// ✅ true (via article.manage)
|
|
320
|
+
|
|
321
|
+
const canUpdate = await permSystem.can(admin, "article.update", "article:1");
|
|
322
|
+
// ✅ true (via article.manage)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Example 3: Conditional Permissions
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { WithRule } from "./library_2/mod.ts";
|
|
329
|
+
|
|
330
|
+
const schemas = {
|
|
331
|
+
"article.read": permission(getArticle),
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const permSystem = createPermissionSystem({
|
|
335
|
+
schemas,
|
|
336
|
+
sources: [
|
|
337
|
+
// Only on public articles
|
|
338
|
+
{
|
|
339
|
+
provide: (subject) => Promise.resolve([{
|
|
340
|
+
subject,
|
|
341
|
+
key: "article.read",
|
|
342
|
+
target: "*",
|
|
343
|
+
with: { public: true }, // ← Condition
|
|
344
|
+
}])
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// Only if user is author
|
|
348
|
+
{
|
|
349
|
+
provide: (subject) => Promise.resolve([{
|
|
350
|
+
subject,
|
|
351
|
+
key: "article.read",
|
|
352
|
+
target: "*",
|
|
353
|
+
with: { author: subject.id }, // ← Condition
|
|
354
|
+
}])
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
rules: [WithRule()] as const,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Only works if article.public === true OR article.author === user.id
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Example 4: Resource Caching with `context()`
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { createPermissionSystem, permission, FilterRule } from "./library_2/mod.ts";
|
|
367
|
+
|
|
368
|
+
const schemas = {
|
|
369
|
+
"article.read": permission(getArticle, [FilterRule()] as const),
|
|
370
|
+
"article.update": permission(getArticle),
|
|
371
|
+
"article.delete": permission(getArticle),
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const permSystem = createPermissionSystem({
|
|
375
|
+
schemas,
|
|
376
|
+
sources: [ownerProvider(["article.read", "article.update", "article.delete"], "article:*")],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// WITHOUT context(): each permission triggers a fetch
|
|
380
|
+
await permSystem.can(user, "article.read", "article:1"); // FETCH #1
|
|
381
|
+
await permSystem.can(user, "article.update", "article:1"); // FETCH #2
|
|
382
|
+
await permSystem.can(user, "article.delete", "article:1"); // FETCH #3
|
|
383
|
+
|
|
384
|
+
// WITH context(): cache is shared between all can()
|
|
385
|
+
const checker = permSystem.context({ subject: user });
|
|
386
|
+
await checker.can("article.read", "article:1"); // FETCH #1
|
|
387
|
+
await checker.can("article.update", "article:1"); // CACHE HIT ✨
|
|
388
|
+
await checker.can("article.delete", "article:1"); // CACHE HIT ✨
|
|
389
|
+
|
|
390
|
+
// Cache also works in nested functions
|
|
391
|
+
async function checkAllPermissions(checker, articleId: string) {
|
|
392
|
+
const canRead = await checker.can("article.read", articleId); // CACHE HIT
|
|
393
|
+
const canUpdate = await checker.can("article.update", articleId); // CACHE HIT
|
|
394
|
+
return { canRead: canRead.ok, canUpdate: canUpdate.ok };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const perms = await checkAllPermissions(checker, "article:1");
|
|
398
|
+
// ↑ No new fetch! Everything is cached
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Advantages of `context()`:**
|
|
402
|
+
- ✅ Cache shared between all `can()` of the same checker
|
|
403
|
+
- ✅ Custom context (dates, IPs, metadata) in all calls
|
|
404
|
+
- ✅ Elegant API: `checker.can(key, target)` instead of `can(subject, key, target)`
|
|
405
|
+
- ✅ No need for callbacks/closures (unlike AsyncContext)
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## 🏗️ Architecture
|
|
410
|
+
|
|
411
|
+
### Validation Flow
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
1. permSystem.can(subject, key, target) or checker.can(key, target)
|
|
415
|
+
↓
|
|
416
|
+
2. Call all providers → [permissions]
|
|
417
|
+
↓
|
|
418
|
+
3. Resolve intermediates (recursive, max depth: 10) → [resolved permissions]
|
|
419
|
+
↓
|
|
420
|
+
4. Filter by key + target matching → [matched permissions]
|
|
421
|
+
↓
|
|
422
|
+
5. Apply global rules (Time, IP, With) → [valid permissions]
|
|
423
|
+
↓
|
|
424
|
+
6. Fetch resource (with cache if context()) ✨
|
|
425
|
+
↓
|
|
426
|
+
7. Apply output rules (FilterRule) → outputs
|
|
427
|
+
↓
|
|
428
|
+
8. Merge outputs (union strategy) → final output
|
|
429
|
+
↓
|
|
430
|
+
9. Return { ok: true, output }
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Note on caching**: Without `context()`, each `can()` creates its own local cache. With `context()`, all `can()` of the same checker share the same cache Map.
|
|
434
|
+
|
|
435
|
+
### Components
|
|
436
|
+
|
|
437
|
+
| Component | Role | Example |
|
|
438
|
+
|-----------|------|---------|
|
|
439
|
+
| **Permission** | Defines a permission with optional context | `permission(getArticle)` |
|
|
440
|
+
| **Intermediate** | Permission that resolves to other permissions | `intermediate((ctx) => [...])` |
|
|
441
|
+
| **Provider** | Source of permissions for a subject | `ownerProvider(...)` |
|
|
442
|
+
| **Global Rule** | Global validation (applied to all permissions) | `TimeRule()`, `IpRule()` |
|
|
443
|
+
| **Output Rule** | Generates metadata/output | `FilterRule()` |
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## 🎨 Advanced Patterns
|
|
448
|
+
|
|
449
|
+
### Pattern 1: Multi-Tenant with Organizations
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
const schemas = {
|
|
453
|
+
"org.member.read": permission(getOrgMember, [FilterRule()] as const),
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
function orgMemberProvider(): PermissionProvider {
|
|
457
|
+
return {
|
|
458
|
+
provide: async (subject, _key, _target) => {
|
|
459
|
+
// Get user's organizations
|
|
460
|
+
const orgs = await db.orgMembers.find({ userId: subject.id });
|
|
461
|
+
|
|
462
|
+
return orgs.map(org => ({
|
|
463
|
+
subject,
|
|
464
|
+
key: "org.member.read",
|
|
465
|
+
target: `org:${org.orgId}:member:*`,
|
|
466
|
+
filter: org.role === 'admin'
|
|
467
|
+
? { _id: true, name: true, email: true, salary: true }
|
|
468
|
+
: { _id: true, name: true, email: true },
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Pattern 2: Rate Limiting
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
function RateLimitRule(): OutputRule<
|
|
479
|
+
{ rateLimit?: { max: number, window: string } }
|
|
480
|
+
> {
|
|
481
|
+
return {
|
|
482
|
+
name: "rateLimit",
|
|
483
|
+
output: ({ state, currentOutput }) => {
|
|
484
|
+
const current = currentOutput?.rateLimit?.max || 0;
|
|
485
|
+
const incoming = state.rateLimit?.max || 0;
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
rateLimit: {
|
|
489
|
+
max: Math.max(current, incoming), // Most permissive
|
|
490
|
+
window: state.rateLimit?.window || "1h"
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Usage
|
|
498
|
+
const schemas = {
|
|
499
|
+
"api.call": permission(undefined, [RateLimitRule()] as const),
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const result = await permSystem.can(user, "api.call");
|
|
503
|
+
// result.output.rateLimit = { max: 1000, window: "1h" }
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Pattern 3: Temporary Permissions
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import { TimeRule } from "./library_2/mod.ts";
|
|
510
|
+
|
|
511
|
+
const permSystem = createPermissionSystem({
|
|
512
|
+
schemas,
|
|
513
|
+
sources: [
|
|
514
|
+
{
|
|
515
|
+
provide: (subject) => Promise.resolve([{
|
|
516
|
+
subject,
|
|
517
|
+
key: "article.read",
|
|
518
|
+
target: "article:1",
|
|
519
|
+
startDate: new Date("2024-01-01"),
|
|
520
|
+
endDate: new Date("2024-12-31"),
|
|
521
|
+
}])
|
|
522
|
+
}
|
|
523
|
+
],
|
|
524
|
+
rules: [TimeRule()] as const,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Only works between 2024-01-01 and 2024-12-31
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## 🚀 Performance
|
|
533
|
+
|
|
534
|
+
### Built-in Optimizations
|
|
535
|
+
|
|
536
|
+
1. **Resource Caching**: With `context()`, cache is shared between all `can()` calls
|
|
537
|
+
2. **Resource Fetching**: Resource is fetched **only once** per permission check
|
|
538
|
+
3. **Short-Circuit**: Global rules stop validation as soon as one fails
|
|
539
|
+
4. **Lazy Evaluation**: Intermediates are resolved only if necessary
|
|
540
|
+
|
|
541
|
+
### Tips for Best Performance
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// ✅ GOOD: Put most restrictive rules first
|
|
545
|
+
rules: [
|
|
546
|
+
IpRule(), // Fast, filters many
|
|
547
|
+
WithRule(), // May fetch resource
|
|
548
|
+
TimeRule(), // Fast
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
// ❌ BAD: Everyone has access, providers are useless
|
|
552
|
+
sources: [
|
|
553
|
+
alwaysAllowProvider(), // ← Useless if you have this
|
|
554
|
+
ownerProvider(...), // ← Never used
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
// ✅ GOOD: Specific provider
|
|
558
|
+
sources: [
|
|
559
|
+
roleBasedProvider(), // Return quickly if wrong role
|
|
560
|
+
ownerProvider(...),
|
|
561
|
+
]
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 🔧 API Reference
|
|
567
|
+
|
|
568
|
+
### Core Functions
|
|
569
|
+
|
|
570
|
+
#### `createPermissionSystem<PS, TRules>(config)`
|
|
571
|
+
|
|
572
|
+
Creates a permission system.
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
const permSystem = createPermissionSystem({
|
|
576
|
+
schemas: PermissionSchemas,
|
|
577
|
+
sources: PermissionProvider[],
|
|
578
|
+
rules?: readonly PermissionRule[],
|
|
579
|
+
maxIntermediateDepth?: number,
|
|
580
|
+
});
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### `permission<C, TRules>(fetchTarget?, rules?)`
|
|
584
|
+
|
|
585
|
+
Creates a permission.
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
// No context, no rules
|
|
589
|
+
permission()
|
|
590
|
+
|
|
591
|
+
// With context
|
|
592
|
+
permission<ArticleId>(getArticle)
|
|
593
|
+
|
|
594
|
+
// With context + rules
|
|
595
|
+
permission(getArticle, [FilterRule()] as const)
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### `intermediate<C, TRules>(provide, fetchTarget?, rules?)`
|
|
599
|
+
|
|
600
|
+
Creates an intermediate permission.
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
intermediate(
|
|
604
|
+
(ctx) => [
|
|
605
|
+
{ ...ctx, key: "article.read" },
|
|
606
|
+
{ ...ctx, key: "article.update" },
|
|
607
|
+
],
|
|
608
|
+
getArticle
|
|
609
|
+
)
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Permission System Methods
|
|
613
|
+
|
|
614
|
+
#### `permSystem.can<K>(subject, key, ...target?)`
|
|
615
|
+
|
|
616
|
+
Checks a permission for a given subject.
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
// Without target
|
|
620
|
+
await permSystem.can(user, "article.create")
|
|
621
|
+
|
|
622
|
+
// With target
|
|
623
|
+
await permSystem.can(user, "article.read", "article:1")
|
|
624
|
+
|
|
625
|
+
// Return
|
|
626
|
+
type PermissionResult<TOutput> = {
|
|
627
|
+
ok: boolean;
|
|
628
|
+
output?: TOutput; // Type inferred from rules
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### `permSystem.context(ctx)`
|
|
633
|
+
|
|
634
|
+
Creates a checker with shared context and cache.
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
const checker = permSystem.context({
|
|
638
|
+
subject: user, // Required
|
|
639
|
+
checkDate: new Date(), // Optional (for TimeRule)
|
|
640
|
+
ips: ["192.168.1.1"], // Optional (for IpRule)
|
|
641
|
+
// ... other custom properties
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// All can() of the checker share the same cache
|
|
645
|
+
await checker.can("article.read", "article:1"); // FETCH
|
|
646
|
+
await checker.can("article.update", "article:1"); // CACHE HIT
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**Return**: `{ can<K>(key, ...target?) => Promise<PermissionResult<...>> }`
|
|
650
|
+
|
|
651
|
+
### Built-in Providers
|
|
652
|
+
|
|
653
|
+
#### `directProvider(permissions)`
|
|
654
|
+
|
|
655
|
+
Provider with static permissions.
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
directProvider([
|
|
659
|
+
{ subject: user1, key: "article.create" },
|
|
660
|
+
{ subject: user2, key: "article.read", target: "article:*" },
|
|
661
|
+
])
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
#### `ownerProvider(keys, targetPattern, metadata?)`
|
|
665
|
+
|
|
666
|
+
Ownership-based provider.
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
ownerProvider(
|
|
670
|
+
["article.read", "article.update"],
|
|
671
|
+
"article:*",
|
|
672
|
+
{ filter: { _id: true, title: true } }
|
|
673
|
+
)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Built-in Rules
|
|
677
|
+
|
|
678
|
+
#### Global Rules
|
|
679
|
+
|
|
680
|
+
- `TimeRule()`: Validates time constraints (`startDate`, `endDate`)
|
|
681
|
+
- `IpRule()`: Validates IP restrictions (`allowedIps`)
|
|
682
|
+
- `WithRule()`: Validates resource constraints (`with`)
|
|
683
|
+
|
|
684
|
+
#### Output Rules
|
|
685
|
+
|
|
686
|
+
- `FilterRule()`: Applies field-level filtering
|
|
687
|
+
|
|
688
|
+
### Utilities
|
|
689
|
+
|
|
690
|
+
#### `matchPath(requested, pattern)`
|
|
691
|
+
|
|
692
|
+
Matches a path with wildcards.
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
matchPath("article:123", "article:*") // true
|
|
696
|
+
matchPath(["article:1", "comment:2"], ["article:*", "*"]) // true
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### `applyFilter(obj, filterSpec)`
|
|
700
|
+
|
|
701
|
+
Applies a filter to an object.
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
applyFilter(
|
|
705
|
+
{ _id: 1, name: "John", password: "secret" },
|
|
706
|
+
{ _id: true, name: true }
|
|
707
|
+
)
|
|
708
|
+
// → { _id: 1, name: "John" }
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
#### `mergeFilters(filter1, filter2)`
|
|
712
|
+
|
|
713
|
+
Merges two filters (union).
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
mergeFilters(
|
|
717
|
+
{ _id: true, name: true },
|
|
718
|
+
{ name: true, email: true }
|
|
719
|
+
)
|
|
720
|
+
// → { _id: true, name: true, email: true }
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## 🆚 Comparison with Other Libraries
|
|
726
|
+
|
|
727
|
+
| Feature | CASL | Casbin | **Library_2** |
|
|
728
|
+
|---------|------|--------|---------------|
|
|
729
|
+
| Type Safety | ⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ |
|
|
730
|
+
| Field-level permissions | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
|
731
|
+
| Auto-filtering | ❌ | ❌ | ✅ |
|
|
732
|
+
| Multi-permission accumulation | ❌ | ❌ | ✅ |
|
|
733
|
+
| Output metadata | ⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ |
|
|
734
|
+
| Hierarchical permissions | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
|
735
|
+
| Flexible providers | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## 📝 License
|
|
740
|
+
|
|
741
|
+
MIT
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## 🤝 Contributing
|
|
746
|
+
|
|
747
|
+
Contributions welcome! See examples in `/playground` for usage patterns.
|