factor-based-permissions 0.0.3 → 0.0.4
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/README.md +439 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +110 -0
package/README.md
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Factor-Based Permissions (TypeScript)
|
|
2
|
+
|
|
3
|
+
A lightweight TypeScript library for **parsing and checking** factor-based permissions on the client side. Designed to work with permissions serialized by the [C# Factor-Based Permissions](../CSharp/README.md) library and embedded in JWT tokens.
|
|
4
|
+
|
|
5
|
+
## Why This Library?
|
|
6
|
+
|
|
7
|
+
When using factor-based permissions with JWTs:
|
|
8
|
+
|
|
9
|
+
1. **Server** (C#) creates and serializes permissions into a compact string
|
|
10
|
+
2. **JWT** carries this string in a claim (typically 30-50 characters)
|
|
11
|
+
3. **Client** (TypeScript) parses and checks permissions for UI decisions
|
|
12
|
+
|
|
13
|
+
This library handles step 3 — fast, dependency-free permission checking in the browser.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install factor-based-permissions
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Extract Serialized Permissions from JWT
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { FactorBasedPermissions } from "factor-based-permissions";
|
|
27
|
+
|
|
28
|
+
// Assuming you've decoded your JWT and extracted the "ap" claim
|
|
29
|
+
const serialized = decodedJwt.ap; // e.g., "!1,3#1+1&2+1,3"
|
|
30
|
+
|
|
31
|
+
const permissions = new FactorBasedPermissions(serialized);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Check Permissions
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Define your permission enum (matching server-side values)
|
|
38
|
+
enum Permission {
|
|
39
|
+
ViewDashboard = 1,
|
|
40
|
+
DownloadReports = 2,
|
|
41
|
+
ManageApiKeys = 3,
|
|
42
|
+
AccessAdminPanel = 4,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if permission is granted
|
|
46
|
+
const canDownload = permissions.checkPermission(Permission.DownloadReports);
|
|
47
|
+
|
|
48
|
+
if (canDownload === true) {
|
|
49
|
+
// Permission granted — all required factors satisfied
|
|
50
|
+
showDownloadButton();
|
|
51
|
+
} else if (canDownload === false) {
|
|
52
|
+
// Permission exists but factors not satisfied
|
|
53
|
+
showDownloadButtonDisabled();
|
|
54
|
+
} else {
|
|
55
|
+
// canDownload === null — permission not in policy
|
|
56
|
+
hideDownloadButton();
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. Show Missing Requirements to User
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
enum Factor {
|
|
64
|
+
EmailVerified = 1,
|
|
65
|
+
PhoneVerified = 2,
|
|
66
|
+
SubscriptionActive = 3,
|
|
67
|
+
TwoFactorEnabled = 4,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const missing = permissions.getMissingFactors(Permission.ManageApiKeys);
|
|
71
|
+
|
|
72
|
+
if (missing.length > 0) {
|
|
73
|
+
// Show user what they need to do
|
|
74
|
+
const messages = missing.map((factor) => {
|
|
75
|
+
switch (factor) {
|
|
76
|
+
case Factor.EmailVerified:
|
|
77
|
+
return "Verify your email";
|
|
78
|
+
case Factor.TwoFactorEnabled:
|
|
79
|
+
return "Enable two-factor authentication";
|
|
80
|
+
default:
|
|
81
|
+
return "Complete verification";
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
showRequirementsDialog(messages);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API Reference
|
|
90
|
+
|
|
91
|
+
### Constructor
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
new FactorBasedPermissions<TFactor extends number, TPermission extends number>(
|
|
95
|
+
serialized?: string | null | undefined
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Creates a new instance. If `serialized` is provided, parses the permission data immediately.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// With data
|
|
103
|
+
const permissions = new FactorBasedPermissions("!1,3#1+1&2+1,3");
|
|
104
|
+
|
|
105
|
+
// Empty instance (all checks return null)
|
|
106
|
+
const empty = new FactorBasedPermissions();
|
|
107
|
+
const alsoEmpty = new FactorBasedPermissions(null);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### checkPermission
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
checkPermission(permission: TPermission): boolean | null
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Check if a permission is granted.
|
|
117
|
+
|
|
118
|
+
| Return Value | Meaning |
|
|
119
|
+
|--------------|---------|
|
|
120
|
+
| `true` | Permission exists and all required factors are satisfied |
|
|
121
|
+
| `false` | Permission exists but some required factors are missing |
|
|
122
|
+
| `null` | Permission is not defined in the policy |
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const result = permissions.checkPermission(Permission.DownloadReports);
|
|
126
|
+
|
|
127
|
+
// Use in conditionals
|
|
128
|
+
if (result === true) {
|
|
129
|
+
// Granted
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Truthy check (true only)
|
|
133
|
+
if (result) {
|
|
134
|
+
// Granted
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Falsy check catches both false and null
|
|
138
|
+
if (!result) {
|
|
139
|
+
// Not granted (either missing factors or not in policy)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### getSatisfiedFactors
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
getSatisfiedFactors(permission?: TPermission): TFactor[]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Get satisfied factors, optionally filtered by a specific permission.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// All satisfied factors
|
|
153
|
+
const allSatisfied = permissions.getSatisfiedFactors();
|
|
154
|
+
// [1, 3] (Factor.EmailVerified, Factor.SubscriptionActive)
|
|
155
|
+
|
|
156
|
+
// Satisfied factors relevant to a specific permission
|
|
157
|
+
const satisfiedForDownload = permissions.getSatisfiedFactors(Permission.DownloadReports);
|
|
158
|
+
// [1, 3] if permission requires factors 1 and 3, and both are satisfied
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### getMissingFactors
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
getMissingFactors(permission: TPermission): TFactor[]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Get factors that are required for a permission but not satisfied.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const missing = permissions.getMissingFactors(Permission.ManageApiKeys);
|
|
171
|
+
// [4] if ManageApiKeys requires factors 1 and 4, but only 1 is satisfied
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### serialized
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
get serialized(): string
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Returns the original serialized string (useful for passing to other contexts).
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const original = permissions.serialized;
|
|
184
|
+
// "!1,3#1+1&2+1,3"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Serialization Format
|
|
188
|
+
|
|
189
|
+
The format is identical to the C# library:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
[!<satisfied_factors>][#<permission_groups>]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Both sections are **optional**:
|
|
196
|
+
- If no factors are satisfied, the `!...` section is omitted
|
|
197
|
+
- If no permissions are defined, the `#...` section is omitted
|
|
198
|
+
- An empty policy serializes to an empty string `""`
|
|
199
|
+
|
|
200
|
+
### Example
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
!1,3#1+1&2+1,3&3+1,4
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Breakdown:
|
|
207
|
+
- `!1,3` — Factors 1 and 3 are satisfied
|
|
208
|
+
- `#1+1` — Permission 1 requires factor 1
|
|
209
|
+
- `&2+1,3` — Permission 2 requires factors 1 and 3
|
|
210
|
+
- `&3+1,4` — Permission 3 requires factors 1 and 4
|
|
211
|
+
|
|
212
|
+
### Number Encoding
|
|
213
|
+
|
|
214
|
+
Numbers are encoded in **Base32** (characters `0-9` and `a-v`):
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// The library handles this automatically via parseInt(value, 32)
|
|
218
|
+
parseInt("v8", 32); // 1000
|
|
219
|
+
parseInt("10", 32); // 32
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Usage Patterns
|
|
223
|
+
|
|
224
|
+
### React Hook
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { useMemo } from "react";
|
|
228
|
+
import { FactorBasedPermissions } from "factor-based-permissions";
|
|
229
|
+
|
|
230
|
+
function usePermissions(serialized: string | null) {
|
|
231
|
+
return useMemo(
|
|
232
|
+
() => new FactorBasedPermissions(serialized),
|
|
233
|
+
[serialized]
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// In component
|
|
238
|
+
function Dashboard() {
|
|
239
|
+
const { accessPolicies } = useAuth(); // Get from JWT/context
|
|
240
|
+
const permissions = usePermissions(accessPolicies);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div>
|
|
244
|
+
{permissions.checkPermission(Permission.ViewDashboard) && (
|
|
245
|
+
<DashboardContent />
|
|
246
|
+
)}
|
|
247
|
+
{permissions.checkPermission(Permission.DownloadReports) && (
|
|
248
|
+
<DownloadButton />
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Permission Guard Component
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
interface PermissionGuardProps {
|
|
259
|
+
permission: Permission;
|
|
260
|
+
permissions: FactorBasedPermissions<Factor, Permission>;
|
|
261
|
+
children: React.ReactNode;
|
|
262
|
+
fallback?: React.ReactNode;
|
|
263
|
+
onMissingFactors?: (factors: Factor[]) => void;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function PermissionGuard({
|
|
267
|
+
permission,
|
|
268
|
+
permissions,
|
|
269
|
+
children,
|
|
270
|
+
fallback = null,
|
|
271
|
+
onMissingFactors,
|
|
272
|
+
}: PermissionGuardProps) {
|
|
273
|
+
const result = permissions.checkPermission(permission);
|
|
274
|
+
|
|
275
|
+
if (result === true) {
|
|
276
|
+
return <>{children}</>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (result === false && onMissingFactors) {
|
|
280
|
+
const missing = permissions.getMissingFactors(permission);
|
|
281
|
+
onMissingFactors(missing);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return <>{fallback}</>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Usage
|
|
288
|
+
<PermissionGuard
|
|
289
|
+
permission={Permission.ManageApiKeys}
|
|
290
|
+
permissions={permissions}
|
|
291
|
+
fallback={<UpgradePrompt />}
|
|
292
|
+
onMissingFactors={(factors) => trackMissingFactors(factors)}
|
|
293
|
+
>
|
|
294
|
+
<ApiKeysManager />
|
|
295
|
+
</PermissionGuard>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Vue Composable
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { computed, type Ref } from "vue";
|
|
302
|
+
import { FactorBasedPermissions } from "factor-based-permissions";
|
|
303
|
+
|
|
304
|
+
export function usePermissions(serialized: Ref<string | null>) {
|
|
305
|
+
const permissions = computed(
|
|
306
|
+
() => new FactorBasedPermissions(serialized.value)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const can = (permission: Permission) =>
|
|
310
|
+
permissions.value.checkPermission(permission) === true;
|
|
311
|
+
|
|
312
|
+
const cannot = (permission: Permission) =>
|
|
313
|
+
permissions.value.checkPermission(permission) !== true;
|
|
314
|
+
|
|
315
|
+
const missingFor = (permission: Permission) =>
|
|
316
|
+
permissions.value.getMissingFactors(permission);
|
|
317
|
+
|
|
318
|
+
return { permissions, can, cannot, missingFor };
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Utility Functions
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Check multiple permissions at once
|
|
326
|
+
function hasAllPermissions(
|
|
327
|
+
permissions: FactorBasedPermissions<Factor, Permission>,
|
|
328
|
+
required: Permission[]
|
|
329
|
+
): boolean {
|
|
330
|
+
return required.every((p) => permissions.checkPermission(p) === true);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check if any permission is granted
|
|
334
|
+
function hasAnyPermission(
|
|
335
|
+
permissions: FactorBasedPermissions<Factor, Permission>,
|
|
336
|
+
required: Permission[]
|
|
337
|
+
): boolean {
|
|
338
|
+
return required.some((p) => permissions.checkPermission(p) === true);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Get all granted permissions from a list
|
|
342
|
+
function getGrantedPermissions(
|
|
343
|
+
permissions: FactorBasedPermissions<Factor, Permission>,
|
|
344
|
+
toCheck: Permission[]
|
|
345
|
+
): Permission[] {
|
|
346
|
+
return toCheck.filter((p) => permissions.checkPermission(p) === true);
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Type Safety
|
|
351
|
+
|
|
352
|
+
The library uses generics for type-safe factor and permission IDs:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// Define your enums
|
|
356
|
+
enum Factor {
|
|
357
|
+
EmailVerified = 1,
|
|
358
|
+
SubscriptionActive = 3,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
enum Permission {
|
|
362
|
+
ViewDashboard = 1,
|
|
363
|
+
DownloadReports = 2,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Type-safe usage
|
|
367
|
+
const permissions = new FactorBasedPermissions<Factor, Permission>(serialized);
|
|
368
|
+
|
|
369
|
+
// TypeScript knows these return Factor[]
|
|
370
|
+
const satisfied: Factor[] = permissions.getSatisfiedFactors();
|
|
371
|
+
const missing: Factor[] = permissions.getMissingFactors(Permission.DownloadReports);
|
|
372
|
+
|
|
373
|
+
// TypeScript enforces Permission type
|
|
374
|
+
permissions.checkPermission(Permission.ViewDashboard); // OK
|
|
375
|
+
permissions.checkPermission(999); // OK (number), but semantically wrong
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Caching
|
|
379
|
+
|
|
380
|
+
The library automatically caches permission check results:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
const permissions = new FactorBasedPermissions(serialized);
|
|
384
|
+
|
|
385
|
+
// First call: computes and caches result
|
|
386
|
+
permissions.checkPermission(Permission.ViewDashboard);
|
|
387
|
+
|
|
388
|
+
// Subsequent calls: returns cached result (O(1))
|
|
389
|
+
permissions.checkPermission(Permission.ViewDashboard);
|
|
390
|
+
permissions.checkPermission(Permission.ViewDashboard);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Important Notes
|
|
394
|
+
|
|
395
|
+
### This Library Does NOT Serialize
|
|
396
|
+
|
|
397
|
+
This is a **read-only** library. It parses and checks permissions but cannot create or modify them. Serialization should only happen on the server (C#) where the source of truth for factors and permissions resides.
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// ❌ Not supported
|
|
401
|
+
permissions.addFactor(Factor.EmailVerified);
|
|
402
|
+
permissions.serialize();
|
|
403
|
+
|
|
404
|
+
// ✅ Correct pattern
|
|
405
|
+
// 1. Client requests server to perform action
|
|
406
|
+
// 2. Server updates factors, creates new permissions
|
|
407
|
+
// 3. Server issues new JWT with updated "ap" claim
|
|
408
|
+
// 4. Client receives new JWT and re-creates FactorBasedPermissions
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Always Validate on Server
|
|
412
|
+
|
|
413
|
+
Client-side permission checks are for **UI purposes only**. Always validate permissions on the server before performing sensitive operations:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Client: Show/hide UI based on permissions
|
|
417
|
+
if (permissions.checkPermission(Permission.DeleteUser)) {
|
|
418
|
+
showDeleteButton();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Server: ALWAYS verify before actually deleting
|
|
422
|
+
app.delete("/users/:id", authorize(Permission.DeleteUser), (req, res) => {
|
|
423
|
+
// Server-side check happens in authorize middleware
|
|
424
|
+
deleteUser(req.params.id);
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Browser Support
|
|
429
|
+
|
|
430
|
+
- All modern browsers (ES2015+)
|
|
431
|
+
- Node.js 14+
|
|
432
|
+
|
|
433
|
+
## Bundle Size
|
|
434
|
+
|
|
435
|
+
~1KB minified (no dependencies)
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
MIT
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,qBAAa,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAAE,WAAW,SAAS,MAAM;IACpF,OAAO,CAAC,iBAAiB,CAAsB;IAC/C,OAAO,CAAC,kBAAkB,CAAqC;IAC/D,OAAO,CAAC,sBAAsB,CAA0C;IACxE,OAAO,CAAC,WAAW,CAAM;gBAEN,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IASzD,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,iBAAiB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,qBAAa,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAAE,WAAW,SAAS,MAAM;IACpF,OAAO,CAAC,iBAAiB,CAAsB;IAC/C,OAAO,CAAC,kBAAkB,CAAqC;IAC/D,OAAO,CAAC,sBAAsB,CAA0C;IACxE,OAAO,CAAC,WAAW,CAAM;gBAEN,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IASzD,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,iBAAiB;IAgBlB,eAAe,CAAC,UAAU,EAAE,WAAW;IAuBvC,mBAAmB,CAAC,UAAU,CAAC,EAAE,WAAW;IAa5C,iBAAiB,CAAC,UAAU,EAAE,WAAW;IAUhD,IAAW,UAAU,WAEpB;CACF"}
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,9 @@ export class FactorBasedPermissions {
|
|
|
40
40
|
for (const permissionGroup of permissionGroupsGroup.split(GROUP_DELIM)) {
|
|
41
41
|
const [permissionsRaw, requiredFactorsRaw] = permissionGroup.split(REQUIRED_FACTORS_DELIM);
|
|
42
42
|
const permissions = permissionsRaw?.split(ITEMS_DELIM)?.map((parseNumeric)) ?? [];
|
|
43
|
-
const requiredFactors = requiredFactorsRaw
|
|
43
|
+
const requiredFactors = requiredFactorsRaw
|
|
44
|
+
? requiredFactorsRaw.split(ITEMS_DELIM).map((parseNumeric))
|
|
45
|
+
: [];
|
|
44
46
|
for (const permission of permissions)
|
|
45
47
|
this._permissionsLookup.set(permission, requiredFactors);
|
|
46
48
|
}
|
|
@@ -63,7 +65,7 @@ export class FactorBasedPermissions {
|
|
|
63
65
|
return true;
|
|
64
66
|
}
|
|
65
67
|
getSatisfiedFactors(permission) {
|
|
66
|
-
if (
|
|
68
|
+
if (permission === undefined)
|
|
67
69
|
return [...this._satisfiedFactors];
|
|
68
70
|
const cachedResult = this._permissionCheckLookup.get(permission);
|
|
69
71
|
const requiredFactors = this._permissionsLookup.get(permission) ?? [];
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,wBAAwB,GAAG,GAAG,CAAC;AACrC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,MAAM,WAAW,GAAG,gBAAgB,EAAE,CAAC;AAEvC,SAAS,gBAAgB;IACvB,MAAM,aAAa,GAAG,wBAAwB,GAAG,kBAAkB,CAAC;IACpE,MAAM,qBAAqB,GAAG,MAAM,wBAAwB,MAAM,aAAa,OAAO,CAAC;IACvF,MAAM,qBAAqB,GAAG,MAAM,kBAAkB,MAAM,aAAa,OAAO,CAAC;IACjF,OAAO,IAAI,MAAM,CAAC,qBAAqB,GAAG,qBAAqB,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,YAAY,CAAmB,KAAa;IACnD,OAAO,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAM,CAAC;AAClC,CAAC;AAED,MAAM,OAAO,sBAAsB;IACzB,iBAAiB,GAAG,IAAI,GAAG,EAAW,CAAC;IACvC,kBAAkB,GAAG,IAAI,GAAG,EAA0B,CAAC;IACvD,sBAAsB,GAAG,IAAI,GAAG,EAA+B,CAAC;IAChE,WAAW,GAAG,EAAE,CAAC;IAEzB,YAAmB,UAAsC;QACvD,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC7C,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;YAClD,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;YAC7C,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC;QAChC,CAAC;IACH,CAAC;IAEO,sBAAsB,CAAC,qBAAoC;QACjE,IAAI,CAAC,qBAAqB;YACxB,OAAO;QAET,KAAK,MAAM,MAAM,IAAI,qBAAqB,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YAC9D,MAAM,aAAa,GAAG,YAAY,CAAU,MAAM,CAAC,CAAC;YACpD,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAEO,iBAAiB,CAAC,qBAAoC;QAC5D,IAAI,CAAC,qBAAqB;YACxB,OAAO;QAET,KAAK,MAAM,eAAe,IAAI,qBAAqB,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YACvE,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;YAC3F,MAAM,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,CAAA,YAAyB,CAAA,CAAC,IAAI,EAAE,CAAC;YAC7F,MAAM,eAAe,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,wBAAwB,GAAG,GAAG,CAAC;AACrC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,MAAM,WAAW,GAAG,gBAAgB,EAAE,CAAC;AAEvC,SAAS,gBAAgB;IACvB,MAAM,aAAa,GAAG,wBAAwB,GAAG,kBAAkB,CAAC;IACpE,MAAM,qBAAqB,GAAG,MAAM,wBAAwB,MAAM,aAAa,OAAO,CAAC;IACvF,MAAM,qBAAqB,GAAG,MAAM,kBAAkB,MAAM,aAAa,OAAO,CAAC;IACjF,OAAO,IAAI,MAAM,CAAC,qBAAqB,GAAG,qBAAqB,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,YAAY,CAAmB,KAAa;IACnD,OAAO,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAM,CAAC;AAClC,CAAC;AAED,MAAM,OAAO,sBAAsB;IACzB,iBAAiB,GAAG,IAAI,GAAG,EAAW,CAAC;IACvC,kBAAkB,GAAG,IAAI,GAAG,EAA0B,CAAC;IACvD,sBAAsB,GAAG,IAAI,GAAG,EAA+B,CAAC;IAChE,WAAW,GAAG,EAAE,CAAC;IAEzB,YAAmB,UAAsC;QACvD,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC7C,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;YAClD,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;YAC7C,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC;QAChC,CAAC;IACH,CAAC;IAEO,sBAAsB,CAAC,qBAAoC;QACjE,IAAI,CAAC,qBAAqB;YACxB,OAAO;QAET,KAAK,MAAM,MAAM,IAAI,qBAAqB,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YAC9D,MAAM,aAAa,GAAG,YAAY,CAAU,MAAM,CAAC,CAAC;YACpD,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAEO,iBAAiB,CAAC,qBAAoC;QAC5D,IAAI,CAAC,qBAAqB;YACxB,OAAO;QAET,KAAK,MAAM,eAAe,IAAI,qBAAqB,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YACvE,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;YAC3F,MAAM,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,CAAA,YAAyB,CAAA,CAAC,IAAI,EAAE,CAAC;YAC7F,MAAM,eAAe,GAAG,kBAAkB;gBACxC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAA,YAAqB,CAAA,CAAC;gBAClE,CAAC,CAAC,EAAE,CAAC;YAEP,KAAK,MAAM,UAAU,IAAI,WAAW;gBAClC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAEM,eAAe,CAAC,UAAuB;QAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAEjE,IAAI,YAAY,KAAK,SAAS;YAC5B,OAAO,YAAY,CAAC;QAEtB,MAAM,eAAe,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAEhE,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,eAAe;YAC1C,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;gBAChD,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;gBACnD,OAAO,KAAK,CAAC;YACf,CAAC;QAEH,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,mBAAmB,CAAC,UAAwB;QACjD,IAAI,UAAU,KAAK,SAAS;YAC1B,OAAO,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAErC,MAAM,YAAY,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACjE,MAAM,eAAe,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QAEtE,IAAI,YAAY,KAAK,IAAI;YACvB,OAAO,eAAe,CAAC;QAEzB,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAChF,CAAC;IAEM,iBAAiB,CAAC,UAAuB;QAC9C,MAAM,YAAY,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAEjE,IAAI,YAAY,KAAK,IAAI;YACvB,OAAO,EAAE,CAAC;QAEZ,MAAM,eAAe,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QACtE,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACjF,CAAC;IAED,IAAW,UAAU;QACnB,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factor-based-permissions",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Lightweight library for parsing and checking factor-based permissions from JWT tokens",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "0x2E757",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"files": [
|
|
9
|
+
"src",
|
|
9
10
|
"dist"
|
|
10
11
|
],
|
|
11
12
|
"main": "dist/index.js",
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const SATISFIED_FACTORS_PREFIX = "!";
|
|
2
|
+
const PERMISSIONS_PREFIX = "#";
|
|
3
|
+
const ITEMS_DELIM = ",";
|
|
4
|
+
const GROUP_DELIM = "&";
|
|
5
|
+
const REQUIRED_FACTORS_DELIM = "+";
|
|
6
|
+
|
|
7
|
+
const groupsRegex = buildGroupsRegex();
|
|
8
|
+
|
|
9
|
+
function buildGroupsRegex() {
|
|
10
|
+
const groupPrefixes = SATISFIED_FACTORS_PREFIX + PERMISSIONS_PREFIX;
|
|
11
|
+
const satisfiedFactorsGroup = `(?:${SATISFIED_FACTORS_PREFIX}([^${groupPrefixes}]*))?`;
|
|
12
|
+
const permissionGroupsGroup = `(?:${PERMISSIONS_PREFIX}([^${groupPrefixes}]*))?`;
|
|
13
|
+
return new RegExp(satisfiedFactorsGroup + permissionGroupsGroup);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseNumeric<T extends number>(value: string) {
|
|
17
|
+
return parseInt(value, 32) as T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class FactorBasedPermissions<TFactor extends number, TPermission extends number> {
|
|
21
|
+
private _satisfiedFactors = new Set<TFactor>();
|
|
22
|
+
private _permissionsLookup = new Map<TPermission, TFactor[]>();
|
|
23
|
+
private _permissionCheckLookup = new Map<TPermission, boolean | null>();
|
|
24
|
+
private _serialized = "";
|
|
25
|
+
|
|
26
|
+
public constructor(serialized?: string | null | undefined) {
|
|
27
|
+
if (typeof serialized === "string") {
|
|
28
|
+
const matches = groupsRegex.exec(serialized);
|
|
29
|
+
this._parseSatisfiedFactors(matches?.[1] ?? null);
|
|
30
|
+
this._parsePermissions(matches?.[2] ?? null);
|
|
31
|
+
this._serialized = serialized;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private _parseSatisfiedFactors(satisfiedFactorsGroup: string | null) {
|
|
36
|
+
if (!satisfiedFactorsGroup)
|
|
37
|
+
return;
|
|
38
|
+
|
|
39
|
+
for (const factor of satisfiedFactorsGroup.split(ITEMS_DELIM)) {
|
|
40
|
+
const factorNumeric = parseNumeric<TFactor>(factor);
|
|
41
|
+
this._satisfiedFactors.add(factorNumeric);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private _parsePermissions(permissionGroupsGroup: string | null) {
|
|
46
|
+
if (!permissionGroupsGroup)
|
|
47
|
+
return;
|
|
48
|
+
|
|
49
|
+
for (const permissionGroup of permissionGroupsGroup.split(GROUP_DELIM)) {
|
|
50
|
+
const [permissionsRaw, requiredFactorsRaw] = permissionGroup.split(REQUIRED_FACTORS_DELIM);
|
|
51
|
+
const permissions = permissionsRaw?.split(ITEMS_DELIM)?.map(parseNumeric<TPermission>) ?? [];
|
|
52
|
+
const requiredFactors = requiredFactorsRaw
|
|
53
|
+
? requiredFactorsRaw.split(ITEMS_DELIM).map(parseNumeric<TFactor>)
|
|
54
|
+
: [];
|
|
55
|
+
|
|
56
|
+
for (const permission of permissions)
|
|
57
|
+
this._permissionsLookup.set(permission, requiredFactors);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public checkPermission(permission: TPermission) {
|
|
62
|
+
const cachedResult = this._permissionCheckLookup.get(permission);
|
|
63
|
+
|
|
64
|
+
if (cachedResult !== undefined)
|
|
65
|
+
return cachedResult;
|
|
66
|
+
|
|
67
|
+
const requiredFactors = this._permissionsLookup.get(permission);
|
|
68
|
+
|
|
69
|
+
if (!requiredFactors) {
|
|
70
|
+
this._permissionCheckLookup.set(permission, null);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const requiredFactor of requiredFactors)
|
|
75
|
+
if (!this._satisfiedFactors.has(requiredFactor)) {
|
|
76
|
+
this._permissionCheckLookup.set(permission, false);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this._permissionCheckLookup.set(permission, true);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public getSatisfiedFactors(permission?: TPermission) {
|
|
85
|
+
if (permission === undefined)
|
|
86
|
+
return [...this._satisfiedFactors];
|
|
87
|
+
|
|
88
|
+
const cachedResult = this._permissionCheckLookup.get(permission);
|
|
89
|
+
const requiredFactors = this._permissionsLookup.get(permission) ?? [];
|
|
90
|
+
|
|
91
|
+
if (cachedResult === true)
|
|
92
|
+
return requiredFactors;
|
|
93
|
+
|
|
94
|
+
return requiredFactors.filter((factor) => this._satisfiedFactors.has(factor));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public getMissingFactors(permission: TPermission) {
|
|
98
|
+
const cachedResult = this._permissionCheckLookup.get(permission);
|
|
99
|
+
|
|
100
|
+
if (cachedResult === true)
|
|
101
|
+
return [];
|
|
102
|
+
|
|
103
|
+
const requiredFactors = this._permissionsLookup.get(permission) ?? [];
|
|
104
|
+
return requiredFactors.filter((factor) => !this._satisfiedFactors.has(factor));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public get serialized() {
|
|
108
|
+
return this._serialized;
|
|
109
|
+
}
|
|
110
|
+
}
|