@flink-app/jwt-auth-plugin 1.0.0 → 2.0.0-alpha.48

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @flink-app/jwt-auth-plugin
2
2
 
3
+ ## 2.0.0-alpha.48
4
+
5
+ ### Minor Changes
6
+
7
+ - AI features
8
+
3
9
  ## 1.0.0
4
10
 
5
11
  ### Minor Changes
@@ -149,6 +149,12 @@ function authenticateRequest(req_1, routePermissions_1, rolePermissions_1, _a) {
149
149
  _c.label = 3;
150
150
  case 3:
151
151
  req.user = user;
152
+ // Populate userPermissions from resolved permissions
153
+ req.userPermissions = computeUserPermissions(user, decodedToken, {
154
+ useDynamicRoles: useDynamicRoles,
155
+ rolePermissions: rolePermissions,
156
+ checkPermissions: checkPermissions,
157
+ });
152
158
  return [2 /*return*/, true];
153
159
  case 4: return [2 /*return*/, false];
154
160
  }
@@ -211,3 +217,40 @@ function validatePassword(password, passwordHash, salt) {
211
217
  });
212
218
  });
213
219
  }
220
+ /**
221
+ * Compute user permissions based on auth plugin configuration
222
+ *
223
+ * Strategies (in order):
224
+ * 1. User object has permissions directly (from getUser callback)
225
+ * 2. Dynamic roles - map user.roles to permissions
226
+ * 3. Static roles from token - map token.roles to permissions
227
+ * 4. No permissions available - return empty array
228
+ */
229
+ function computeUserPermissions(user, decodedToken, config) {
230
+ // Strategy 1: User object has permissions directly (from getUser callback)
231
+ if (user.permissions && Array.isArray(user.permissions)) {
232
+ return user.permissions;
233
+ }
234
+ // Strategy 2: Dynamic roles - map user.roles to permissions
235
+ if (config.useDynamicRoles && user.roles && Array.isArray(user.roles)) {
236
+ return flattenRolesToPermissions(user.roles, config.rolePermissions);
237
+ }
238
+ // Strategy 3: Static roles from token - map token.roles to permissions
239
+ if (decodedToken.roles && Array.isArray(decodedToken.roles)) {
240
+ return flattenRolesToPermissions(decodedToken.roles, config.rolePermissions);
241
+ }
242
+ // Strategy 4: No permissions available
243
+ return [];
244
+ }
245
+ /**
246
+ * Convert roles to permissions using rolePermissions mapping
247
+ */
248
+ function flattenRolesToPermissions(roles, roleMap) {
249
+ var perms = new Set();
250
+ for (var _i = 0, roles_1 = roles; _i < roles_1.length; _i++) {
251
+ var role = roles_1[_i];
252
+ var rolePerms = roleMap[role] || [];
253
+ rolePerms.forEach(function (p) { return perms.add(p); });
254
+ }
255
+ return Array.from(perms);
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/jwt-auth-plugin",
3
- "version": "1.0.0",
3
+ "version": "2.0.0-alpha.48",
4
4
  "description": "Flink plugin for JWT auth",
5
5
  "author": "joel@frost.se",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "@types/node": "22.13.10",
20
20
  "ts-node": "^10.9.2",
21
21
  "tsc-watch": "^4.2.9",
22
- "@flink-app/flink": "1.0.0"
22
+ "@flink-app/flink": "2.0.0-alpha.48"
23
23
  },
24
24
  "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
25
25
  "scripts": {
package/readme.md CHANGED
@@ -443,6 +443,253 @@ jwtAuthPlugin({
443
443
 
444
444
  **Solution**: Clear permission cache or reduce cache TTL
445
445
 
446
+ ## `req.userPermissions` - AI Permissions Integration
447
+
448
+ Starting in **v1.1.0**, the JWT auth plugin automatically populates `req.userPermissions` with the resolved permissions array. This field is specifically designed for use with Flink's AI agents and tools system, providing a standardized way to handle permissions across handlers, agents, and tools.
449
+
450
+ ### Why `req.userPermissions`?
451
+
452
+ **Before v1.1.0**: Tools and agents had to manually convert roles to permissions or query the database in permission functions, leading to:
453
+ - Duplicated role-to-permission mapping logic
454
+ - Performance issues (N tools × M agent steps = many DB queries)
455
+ - No support for multi-tenant permission contexts
456
+ - Inconsistent permission resolution across the app
457
+
458
+ **After v1.1.0**: The auth plugin populates `req.userPermissions` once during authentication, and this resolved array is passed through to agents and tools:
459
+ - ✅ Single source of truth for permissions
460
+ - ✅ Performance optimization (computed once per request)
461
+ - ✅ Reuses all auth plugin features (roles, dynamic roles, custom checkers)
462
+ - ✅ Consistent permission checking across handlers, agents, and tools
463
+
464
+ ### How It Works
465
+
466
+ The JWT auth plugin automatically populates `req.userPermissions` based on the configuration strategy:
467
+
468
+ **Strategy 1: Direct permissions from user object**
469
+
470
+ ```typescript
471
+ jwtAuthPlugin({
472
+ secret: process.env.JWT_SECRET,
473
+ getUser: async (tokenData) => {
474
+ const user = await userRepo.getById(tokenData.userId);
475
+ return {
476
+ id: user.id,
477
+ permissions: user.permissions, // Direct permissions array
478
+ };
479
+ },
480
+ rolePermissions: {},
481
+ });
482
+
483
+ // Result: req.userPermissions = user.permissions
484
+ ```
485
+
486
+ **Strategy 2: Static role mapping**
487
+
488
+ ```typescript
489
+ jwtAuthPlugin({
490
+ secret: process.env.JWT_SECRET,
491
+ getUser: async (tokenData) => {
492
+ return { id: tokenData.userId };
493
+ },
494
+ rolePermissions: {
495
+ admin: ["car:read", "car:create", "car:delete"],
496
+ user: ["car:read"],
497
+ premium: ["car:read", "car:create", "premium-features"],
498
+ },
499
+ });
500
+
501
+ // JWT token has roles: ["user", "premium"]
502
+ // Result: req.userPermissions = ["car:read", "car:create", "premium-features"]
503
+ ```
504
+
505
+ **Strategy 3: Dynamic roles (multi-tenant)**
506
+
507
+ ```typescript
508
+ jwtAuthPlugin({
509
+ secret: process.env.JWT_SECRET,
510
+ useDynamicRoles: true,
511
+ getUser: async (tokenData, req) => {
512
+ const orgId = req.headers['x-organization-id'];
513
+ const membership = await orgMembershipRepo.getByUserAndOrg(
514
+ tokenData.userId,
515
+ orgId
516
+ );
517
+ return {
518
+ id: tokenData.userId,
519
+ roles: [membership.role], // Org-specific role
520
+ };
521
+ },
522
+ rolePermissions: {
523
+ admin: ["car:read", "car:create", "car:delete"],
524
+ user: ["car:read"],
525
+ },
526
+ });
527
+
528
+ // Same user, different orgs → different permissions
529
+ // Org A: admin role → req.userPermissions = ["car:read", "car:create", "car:delete"]
530
+ // Org B: user role → req.userPermissions = ["car:read"]
531
+ ```
532
+
533
+ ### Using with AI Agents and Tools
534
+
535
+ Pass `req.userPermissions` to agents in your handlers:
536
+
537
+ ```typescript
538
+ // Handler
539
+ const handler: Handler<AppCtx, RequestBody, ResponseBody> = async ({ req, ctx }) => {
540
+ const agent = ctx.agents.carAgent
541
+ .withUser(req.user)
542
+ .withPermissions(req.userPermissions); // Pass resolved permissions
543
+
544
+ return await agent.execute({ message: req.body.message });
545
+ };
546
+
547
+ // Agent
548
+ export default class CarAgent extends FlinkAgent<AppCtx> {
549
+ description = "Expert in car models";
550
+ instructions = "You are a car expert...";
551
+ tools = ["get-cars-tool", "create-car-tool"];
552
+ permissions = "car:access"; // Agent-level permission
553
+ }
554
+
555
+ // Tool
556
+ export const Tool: FlinkToolProps = {
557
+ id: "create-car",
558
+ description: "Create a new car",
559
+ permissions: "car:create", // Tool-level permission
560
+ inputSchema: z.object({
561
+ brand: z.string(),
562
+ model: z.string(),
563
+ }),
564
+ };
565
+ ```
566
+
567
+ **What happens:**
568
+ 1. Auth plugin resolves permissions → `req.userPermissions = ["car:access", "car:read", "car:create"]`
569
+ 2. Agent checks if user has `"car:access"` → ✅ allowed
570
+ 3. LLM only sees tools user has permission for (`"create-car"` is shown, others hidden)
571
+ 4. Tool execution checks `"car:create"` permission → ✅ allowed
572
+
573
+ ### Permission Resolution Priority
574
+
575
+ The plugin computes `req.userPermissions` in this order:
576
+
577
+ 1. **Direct permissions**: If `user.permissions` exists (from `getUser`), use it directly
578
+ 2. **Dynamic roles**: If `useDynamicRoles: true`, map `user.roles` to permissions via `rolePermissions`
579
+ 3. **Static roles**: Map token roles to permissions via `rolePermissions`
580
+ 4. **Empty array**: If no permissions found, return `[]`
581
+
582
+ ### Backward Compatibility
583
+
584
+ `req.userPermissions` is **optional** - existing apps continue to work:
585
+
586
+ - If not using AI agents/tools → field is populated but not used
587
+ - Tools/agents fall back to `user.permissions` if `req.userPermissions` is not passed
588
+ - No breaking changes to existing code
589
+
590
+ ### Performance Benefits
591
+
592
+ **Before** (function-based permissions without `userPermissions`):
593
+ ```typescript
594
+ // Permission checked N times (once per tool use)
595
+ permissions: async (input, user) => {
596
+ const perms = await fetchUserPermissions(user.id); // DB query per tool!
597
+ return perms.includes("car:create");
598
+ }
599
+ ```
600
+
601
+ **After** (using `userPermissions`):
602
+ ```typescript
603
+ // Permissions computed ONCE during authentication
604
+ req.userPermissions = ["car:read", "car:create"]; // Happens once
605
+
606
+ // Tools check the array (fast O(1) lookup)
607
+ permissions: "car:create"
608
+ ```
609
+
610
+ **Result**: Single DB query/computation per request, regardless of how many tools or agent steps are used.
611
+
612
+ ### Example: Complete Integration
613
+
614
+ ```typescript
615
+ // 1. Configure JWT auth plugin
616
+ jwtAuthPlugin({
617
+ secret: process.env.JWT_SECRET,
618
+ rolePermissions: {
619
+ admin: ["car:read", "car:create", "car:delete", "premium-features"],
620
+ user: ["car:read"],
621
+ premium: ["car:read", "car:create", "premium-features"],
622
+ },
623
+ getUser: async (tokenData) => {
624
+ const user = await userRepo.getById(tokenData.userId);
625
+ return {
626
+ id: user.id,
627
+ email: user.email,
628
+ // Roles from DB or token will be mapped to permissions automatically
629
+ };
630
+ },
631
+ });
632
+
633
+ // 2. Handler passes userPermissions to agent
634
+ const handler: Handler<AppCtx, { question: string }, { answer: string }> = async ({
635
+ req,
636
+ ctx,
637
+ }) => {
638
+ const agent = ctx.agents.carAgent
639
+ .withUser(req.user)
640
+ .withPermissions(req.userPermissions); // Resolved permissions passed here
641
+
642
+ const response = await agent.execute({ message: req.body.question });
643
+ return { data: { answer: response.message } };
644
+ };
645
+
646
+ // 3. Agent defines permissions
647
+ export default class CarAgent extends FlinkAgent<AppCtx> {
648
+ description = "Car inventory expert";
649
+ instructions = "Help users with car-related queries";
650
+ tools = ["get-cars", "create-car", "premium-report"];
651
+ permissions = "car:read"; // User must have car:read
652
+ }
653
+
654
+ // 4. Tools define individual permissions
655
+ export const GetCarsTool: FlinkToolProps = {
656
+ id: "get-cars",
657
+ description: "Search car inventory",
658
+ permissions: "car:read", // Basic read access
659
+ inputSchema: z.object({ brand: z.string().optional() }),
660
+ };
661
+
662
+ export const CreateCarTool: FlinkToolProps = {
663
+ id: "create-car",
664
+ description: "Add new car to inventory",
665
+ permissions: "car:create", // Requires create permission
666
+ inputSchema: z.object({ brand: z.string(), model: z.string() }),
667
+ };
668
+
669
+ export const PremiumReportTool: FlinkToolProps = {
670
+ id: "premium-report",
671
+ description: "Generate advanced analytics",
672
+ permissions: ["car:read", "premium-features"], // Requires BOTH
673
+ inputSchema: z.object({ carId: z.string() }),
674
+ };
675
+ ```
676
+
677
+ **Flow**:
678
+ 1. User with JWT token containing `roles: ["user", "premium"]` makes a request
679
+ 2. Auth plugin resolves: `req.userPermissions = ["car:read", "car:create", "premium-features"]`
680
+ 3. Handler passes `req.userPermissions` to agent
681
+ 4. Agent checks `"car:read"` → ✅ user has it
682
+ 5. Agent filters tools:
683
+ - `get-cars` (requires `"car:read"`) → ✅ shown to LLM
684
+ - `create-car` (requires `"car:create"`) → ✅ shown to LLM
685
+ - `premium-report` (requires `["car:read", "premium-features"]`) → ✅ shown to LLM (has both)
686
+ 6. LLM can use all three tools
687
+
688
+ If user only had `roles: ["user"]`:
689
+ - `req.userPermissions = ["car:read"]`
690
+ - Only `get-cars` tool would be shown to LLM
691
+ - LLM cannot hallucinate using `create-car` or `premium-report` (they're hidden)
692
+
446
693
  ## Multi-Tenant & Dynamic Roles
447
694
 
448
695
  The JWT auth plugin supports multi-tenant scenarios where users have different roles depending on the organization or context they're accessing. This is achieved through the `useDynamicRoles` option combined with the `req` parameter in `getUser`.
@@ -219,6 +219,14 @@ async function authenticateRequest(
219
219
  }
220
220
 
221
221
  req.user = user;
222
+
223
+ // Populate userPermissions from resolved permissions
224
+ req.userPermissions = computeUserPermissions(user, decodedToken, {
225
+ useDynamicRoles,
226
+ rolePermissions,
227
+ checkPermissions,
228
+ });
229
+
222
230
  return true;
223
231
  }
224
232
  }
@@ -261,3 +269,52 @@ async function validatePassword(password: string, passwordHash: string, salt: st
261
269
  const hashCandidate = await encrypt(password, salt);
262
270
  return hashCandidate === passwordHash;
263
271
  }
272
+
273
+ /**
274
+ * Compute user permissions based on auth plugin configuration
275
+ *
276
+ * Strategies (in order):
277
+ * 1. User object has permissions directly (from getUser callback)
278
+ * 2. Dynamic roles - map user.roles to permissions
279
+ * 3. Static roles from token - map token.roles to permissions
280
+ * 4. No permissions available - return empty array
281
+ */
282
+ function computeUserPermissions(
283
+ user: any,
284
+ decodedToken: any,
285
+ config: {
286
+ useDynamicRoles?: boolean;
287
+ rolePermissions: { [x: string]: string[] };
288
+ checkPermissions?: PermissionChecker;
289
+ }
290
+ ): string[] {
291
+ // Strategy 1: User object has permissions directly (from getUser callback)
292
+ if (user.permissions && Array.isArray(user.permissions)) {
293
+ return user.permissions;
294
+ }
295
+
296
+ // Strategy 2: Dynamic roles - map user.roles to permissions
297
+ if (config.useDynamicRoles && user.roles && Array.isArray(user.roles)) {
298
+ return flattenRolesToPermissions(user.roles, config.rolePermissions);
299
+ }
300
+
301
+ // Strategy 3: Static roles from token - map token.roles to permissions
302
+ if (decodedToken.roles && Array.isArray(decodedToken.roles)) {
303
+ return flattenRolesToPermissions(decodedToken.roles, config.rolePermissions);
304
+ }
305
+
306
+ // Strategy 4: No permissions available
307
+ return [];
308
+ }
309
+
310
+ /**
311
+ * Convert roles to permissions using rolePermissions mapping
312
+ */
313
+ function flattenRolesToPermissions(roles: string[], roleMap: Record<string, string[]>): string[] {
314
+ const perms = new Set<string>();
315
+ for (const role of roles) {
316
+ const rolePerms = roleMap[role] || [];
317
+ rolePerms.forEach((p) => perms.add(p));
318
+ }
319
+ return Array.from(perms);
320
+ }