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 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
@@ -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;IAclB,eAAe,CAAC,UAAU,EAAE,WAAW;IAuBvC,mBAAmB,CAAC,UAAU,CAAC,EAAE,WAAW;IAa5C,iBAAiB,CAAC,UAAU,EAAE,WAAW;IAUhD,IAAW,UAAU,WAEpB;CACF"}
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?.split(ITEMS_DELIM)?.map((parseNumeric)) ?? [];
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 (!permission)
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":"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"}
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.3",
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
+ }