@jaypie/mcp 0.1.0 → 0.1.2

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.
Files changed (50) hide show
  1. package/dist/index.js +0 -0
  2. package/package.json +9 -5
  3. package/prompts/Branch_Management.md +34 -0
  4. package/prompts/Development_Process.md +67 -0
  5. package/prompts/Jaypie_Agent_Rules.md +110 -0
  6. package/prompts/Jaypie_Auth0_Express_Mongoose.md +736 -0
  7. package/prompts/Jaypie_Browser_and_Frontend_Web_Packages.md +18 -0
  8. package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +156 -0
  9. package/prompts/Jaypie_CICD_with_GitHub_Actions.md +151 -0
  10. package/prompts/Jaypie_Commander_CLI_Package.md +166 -0
  11. package/prompts/Jaypie_Core_Errors_and_Logging.md +39 -0
  12. package/prompts/Jaypie_Eslint_NPM_Package.md +78 -0
  13. package/prompts/Jaypie_Ideal_Project_Structure.md +78 -0
  14. package/prompts/Jaypie_Init_Express_on_Lambda.md +87 -0
  15. package/prompts/Jaypie_Init_Jaypie_CDK_Package.md +35 -0
  16. package/prompts/Jaypie_Init_Lambda_Package.md +245 -0
  17. package/prompts/Jaypie_Init_Monorepo_Project.md +44 -0
  18. package/prompts/Jaypie_Init_Project_Subpackage.md +70 -0
  19. package/prompts/Jaypie_Legacy_Patterns.md +11 -0
  20. package/prompts/Jaypie_Llm_Calls.md +113 -0
  21. package/prompts/Jaypie_Llm_Tools.md +124 -0
  22. package/prompts/Jaypie_Mocks_and_Testkit.md +137 -0
  23. package/prompts/Jaypie_Mongoose_Models_Package.md +231 -0
  24. package/prompts/Jaypie_Mongoose_with_Express_CRUD.md +1000 -0
  25. package/prompts/Jaypie_Scrub.md +177 -0
  26. package/prompts/Write_Efficient_Prompt_Guides.md +48 -0
  27. package/prompts/Write_and_Maintain_Engaging_Readme.md +67 -0
  28. package/prompts/templates/cdk-subpackage/bin/cdk.ts +11 -0
  29. package/prompts/templates/cdk-subpackage/cdk.json +19 -0
  30. package/prompts/templates/cdk-subpackage/lib/cdk-app.ts +41 -0
  31. package/prompts/templates/cdk-subpackage/lib/cdk-infrastructure.ts +15 -0
  32. package/prompts/templates/express-subpackage/index.ts +8 -0
  33. package/prompts/templates/express-subpackage/src/app.ts +18 -0
  34. package/prompts/templates/express-subpackage/src/handler.config.ts +44 -0
  35. package/prompts/templates/express-subpackage/src/routes/resource/__tests__/resourceGet.route.spec.ts +29 -0
  36. package/prompts/templates/express-subpackage/src/routes/resource/resourceGet.route.ts +22 -0
  37. package/prompts/templates/express-subpackage/src/routes/resource.router.ts +11 -0
  38. package/prompts/templates/express-subpackage/src/types/express.ts +9 -0
  39. package/prompts/templates/project-monorepo/.vscode/settings.json +72 -0
  40. package/prompts/templates/project-monorepo/eslint.config.mjs +1 -0
  41. package/prompts/templates/project-monorepo/package.json +20 -0
  42. package/prompts/templates/project-monorepo/tsconfig.base.json +18 -0
  43. package/prompts/templates/project-monorepo/tsconfig.json +6 -0
  44. package/prompts/templates/project-monorepo/vitest.workspace.js +3 -0
  45. package/prompts/templates/project-subpackage/package.json +16 -0
  46. package/prompts/templates/project-subpackage/tsconfig.json +11 -0
  47. package/prompts/templates/project-subpackage/vite.config.ts +21 -0
  48. package/prompts/templates/project-subpackage/vitest.config.ts +7 -0
  49. package/prompts/templates/project-subpackage/vitest.setup.ts +6 -0
  50. package/LICENSE.txt +0 -21
@@ -0,0 +1,736 @@
1
+ ---
2
+ description: Auth0 authentication and authorization patterns for Jaypie Express with Mongoose
3
+ ---
4
+
5
+ # Jaypie Auth0 Express Mongoose
6
+
7
+ Complete authentication and authorization patterns for Jaypie Express applications using Auth0 JWT tokens with Mongoose user management in TypeScript.
8
+
9
+ ## Pattern
10
+
11
+ - TypeScript with ES modules (`"type": "module"`)
12
+ - Auth0 JWT token validation with fallback to project secrets
13
+ - User context resolution from MongoDB with aggregated permissions
14
+ - Group-based authorization with portfolio access control
15
+ - Role-based access control (RBAC) patterns
16
+ - Secure user data handling with proper error responses
17
+ - Type-safe interfaces for all authentication and authorization patterns
18
+
19
+ ## Dependencies
20
+
21
+ ```json
22
+ {
23
+ "dependencies": {
24
+ "express-oauth2-jwt-bearer": "^1.6.0",
25
+ "@yourorg/models": "^1.0.0",
26
+ "jaypie": "^1.1.0",
27
+ "express": "^4.19.0",
28
+ "mongoose": "^8.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^4.17.0",
32
+ "@types/mongoose": "^8.0.0",
33
+ "typescript": "^5.0.0"
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## TypeScript Configuration
39
+
40
+ **tsconfig.json**
41
+ ```json
42
+ {
43
+ "compilerOptions": {
44
+ "target": "ES2022",
45
+ "module": "ESNext",
46
+ "moduleResolution": "node",
47
+ "declaration": true,
48
+ "outDir": "./dist",
49
+ "strict": true,
50
+ "esModuleInterop": true,
51
+ "skipLibCheck": true,
52
+ "forceConsistentCasingInFileNames": true,
53
+ "resolveJsonModule": true,
54
+ "allowSyntheticDefaultImports": true
55
+ },
56
+ "include": ["src/**/*", "index.ts"],
57
+ "exclude": ["node_modules", "dist", "**/*.spec.ts"]
58
+ }
59
+ ```
60
+
61
+ ## Environment Variables
62
+
63
+ ```bash
64
+ AUTH0_DOMAIN=your-auth0-domain.auth0.com
65
+ AUTH0_AUDIENCE=your-api-identifier
66
+ PROJECT_SECRET=your-project-secret
67
+ PROJECT_USER_XID=system-user-id
68
+ ```
69
+
70
+ ## Core Authentication
71
+
72
+ **src/util/validateAuth.ts** - JWT and project secret validation
73
+ ```typescript
74
+ import { getHeaderFrom, HTTP, UnauthorizedError } from "jaypie";
75
+ import { auth } from "express-oauth2-jwt-bearer";
76
+ import type { Request, Response, NextFunction } from "express";
77
+
78
+ interface AuthRequest extends Request {
79
+ auth?: {
80
+ payload: {
81
+ sub: string;
82
+ [key: string]: any;
83
+ };
84
+ };
85
+ }
86
+
87
+ const validateAuth = async (req: AuthRequest): Promise<boolean> => {
88
+ const clientProjectSecret = getHeaderFrom(HTTP.HEADER.PROJECT.SECRET, req);
89
+
90
+ // Project secret authentication (for service-to-service calls)
91
+ if (clientProjectSecret && process.env.PROJECT_SECRET &&
92
+ clientProjectSecret === process.env.PROJECT_SECRET) {
93
+ req.auth = { payload: { sub: process.env.PROJECT_USER_XID || "system" } };
94
+ return true;
95
+ }
96
+
97
+ // Auth0 JWT token validation
98
+ const validateAuthToken = auth({
99
+ audience: process.env.AUTH0_AUDIENCE,
100
+ issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
101
+ });
102
+
103
+ await validateAuthToken(req, {} as Response, (error?: Error) => {
104
+ if (error) throw new UnauthorizedError();
105
+ });
106
+
107
+ return true;
108
+ };
109
+
110
+ export default validateAuth;
111
+ ```
112
+
113
+ **Alternative: Promise-based validation**
114
+ ```typescript
115
+ import { UnauthorizedError } from "jaypie";
116
+ import { auth } from "express-oauth2-jwt-bearer";
117
+ import type { Request, Response, NextFunction } from "express";
118
+
119
+ const jwtCheck = auth({
120
+ audience: process.env.AUTH0_AUDIENCE,
121
+ issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
122
+ });
123
+
124
+ export default async (req: Request, res: Response, next: NextFunction): Promise<void> => {
125
+ return new Promise((resolve, reject) => {
126
+ jwtCheck(req, res, (err) => {
127
+ if (err) {
128
+ reject(new UnauthorizedError("Invalid token"));
129
+ } else {
130
+ resolve();
131
+ }
132
+ });
133
+ });
134
+ };
135
+ ```
136
+
137
+ ## User Context Resolution
138
+
139
+ **src/util/userLocal.ts** - MongoDB user lookup with permissions
140
+ ```typescript
141
+ import { ConfigurationError, UnauthorizedError } from "jaypie";
142
+ import Model from "@yourorg/models";
143
+ import type { Request } from "express";
144
+
145
+ interface AuthRequest extends Request {
146
+ auth?: {
147
+ payload: {
148
+ sub: string;
149
+ [key: string]: any;
150
+ };
151
+ };
152
+ }
153
+
154
+ interface UserWithPortfolios {
155
+ _id: string;
156
+ xid: string;
157
+ groups: string[];
158
+ portfolios: string[];
159
+ roles?: string[];
160
+ [key: string]: any;
161
+ }
162
+
163
+ const userLocal = async (req: AuthRequest = {} as AuthRequest): Promise<UserWithPortfolios> => {
164
+ if (!req?.auth?.payload?.sub) {
165
+ throw new ConfigurationError();
166
+ }
167
+
168
+ const xid = req.auth.payload.sub;
169
+ const users = await Model.User.aggregate([
170
+ { $match: { xid, deletedAt: { $exists: false } } },
171
+ {
172
+ $lookup: {
173
+ from: "groups",
174
+ let: { groupIds: "$groups" },
175
+ pipeline: [
176
+ { $match: { $expr: { $in: ["$_id", "$$groupIds"] } } },
177
+ { $project: { portfolios: 1, _id: 1 } },
178
+ ],
179
+ as: "groups_docs",
180
+ },
181
+ },
182
+ {
183
+ $addFields: {
184
+ portfolios: {
185
+ $reduce: {
186
+ input: "$groups_docs.portfolios",
187
+ initialValue: [],
188
+ in: { $setUnion: ["$$value", "$$this"] },
189
+ },
190
+ },
191
+ },
192
+ },
193
+ { $project: { groups_docs: 0 } },
194
+ ]);
195
+
196
+ if (!users?.length) throw new UnauthorizedError();
197
+
198
+ const user = new Model.User(users[0]);
199
+ user.portfolios = users[0].portfolios.map(String);
200
+ return user;
201
+ };
202
+
203
+ export default userLocal;
204
+ ```
205
+
206
+ **Simplified user context (without aggregation)**
207
+ ```typescript
208
+ import { UnauthorizedError } from "jaypie";
209
+ import Model from "@yourorg/models";
210
+ import type { Request } from "express";
211
+
212
+ interface AuthRequest extends Request {
213
+ auth?: {
214
+ payload: {
215
+ sub: string;
216
+ email?: string;
217
+ name?: string;
218
+ [key: string]: any;
219
+ };
220
+ };
221
+ }
222
+
223
+ interface UserContext {
224
+ sub: string;
225
+ email?: string;
226
+ name?: string;
227
+ groups: string[];
228
+ portfolios: string[];
229
+ roles: string[];
230
+ }
231
+
232
+ export default async (req: AuthRequest): Promise<UserContext> => {
233
+ const user = req.auth?.payload;
234
+ if (!user) {
235
+ throw new UnauthorizedError("User not authenticated");
236
+ }
237
+
238
+ // Look up user in database if needed
239
+ const dbUser = await Model.User.findOne({
240
+ xid: user.sub,
241
+ deletedAt: { $exists: false }
242
+ });
243
+
244
+ if (!dbUser) {
245
+ throw new UnauthorizedError("User not found");
246
+ }
247
+
248
+ return {
249
+ sub: user.sub,
250
+ email: user.email,
251
+ name: user.name,
252
+ groups: dbUser.groups || [],
253
+ portfolios: dbUser.portfolios || [],
254
+ roles: dbUser.roles || [],
255
+ };
256
+ };
257
+ ```
258
+
259
+ ## Authorization Patterns
260
+
261
+ **src/util/authUserHasGroups.ts** - Group membership validation
262
+ ```typescript
263
+ import { ForbiddenError } from "jaypie";
264
+
265
+ interface AuthUserHasGroupsOptions {
266
+ requireGroups?: boolean;
267
+ }
268
+
269
+ interface User {
270
+ groups?: string[];
271
+ }
272
+
273
+ export default (user: User, options: AuthUserHasGroupsOptions = {}): boolean => {
274
+ const { requireGroups = true } = options;
275
+
276
+ if (requireGroups && (!user.groups || user.groups.length === 0)) {
277
+ throw new ForbiddenError("User must belong to at least one group");
278
+ }
279
+
280
+ return true;
281
+ };
282
+ ```
283
+
284
+ **src/util/authUserHasAllGroups.ts** - Specific group requirements
285
+ ```typescript
286
+ import { ForbiddenError } from "jaypie";
287
+
288
+ interface AuthUserHasAllGroupsOptions {
289
+ groupUuids: string[];
290
+ }
291
+
292
+ interface User {
293
+ groups?: string[];
294
+ }
295
+
296
+ export default async (user: User, { groupUuids }: AuthUserHasAllGroupsOptions): Promise<boolean> => {
297
+ const userGroups = user.groups || [];
298
+ const hasAllGroups = groupUuids.every(groupUuid =>
299
+ userGroups.includes(groupUuid)
300
+ );
301
+
302
+ if (!hasAllGroups) {
303
+ throw new ForbiddenError("User lacks required group permissions");
304
+ }
305
+
306
+ return true;
307
+ };
308
+ ```
309
+
310
+ **src/util/authUserHasAnyGroups.ts** - Alternative group validation
311
+ ```typescript
312
+ import { ForbiddenError } from "jaypie";
313
+
314
+ interface AuthUserHasAnyGroupsOptions {
315
+ groupUuids: string[];
316
+ }
317
+
318
+ interface User {
319
+ groups?: string[];
320
+ }
321
+
322
+ export default async (user: User, { groupUuids }: AuthUserHasAnyGroupsOptions): Promise<boolean> => {
323
+ const userGroups = user.groups || [];
324
+ const hasAnyGroup = groupUuids.some(groupUuid =>
325
+ userGroups.includes(groupUuid)
326
+ );
327
+
328
+ if (!hasAnyGroup) {
329
+ throw new ForbiddenError("User lacks required group permissions");
330
+ }
331
+
332
+ return true;
333
+ };
334
+ ```
335
+
336
+ **src/util/authUserHasPortfolios.ts** - Portfolio access control
337
+ ```typescript
338
+ import { ForbiddenError } from "jaypie";
339
+
340
+ interface User {
341
+ portfolios?: string[];
342
+ }
343
+
344
+ export default (user: User, portfolioIds: string | string[]): boolean => {
345
+ const portfolioIdsArray = Array.isArray(portfolioIds) ? portfolioIds : [portfolioIds];
346
+
347
+ const userPortfolios = user.portfolios || [];
348
+ const hasAccess = portfolioIdsArray.every(portfolioId =>
349
+ userPortfolios.includes(portfolioId)
350
+ );
351
+
352
+ if (!hasAccess) {
353
+ throw new ForbiddenError("User lacks portfolio access");
354
+ }
355
+
356
+ return true;
357
+ };
358
+ ```
359
+
360
+ **src/util/authUserHasRoles.ts** - Role-based access control
361
+ ```typescript
362
+ import { ForbiddenError } from "jaypie";
363
+
364
+ interface User {
365
+ roles?: string[];
366
+ }
367
+
368
+ export default (user: User, requiredRoles: string | string[]): boolean => {
369
+ const requiredRolesArray = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
370
+
371
+ const userRoles = user.roles || [];
372
+ const hasRequiredRole = requiredRolesArray.some(role =>
373
+ userRoles.includes(role)
374
+ );
375
+
376
+ if (!hasRequiredRole) {
377
+ throw new ForbiddenError("User lacks required role permissions");
378
+ }
379
+
380
+ return true;
381
+ };
382
+ ```
383
+
384
+ ## System Context
385
+
386
+ **src/util/systemContextLocal.ts** - System metadata
387
+ ```typescript
388
+ import type { Request } from "express";
389
+
390
+ interface SystemContext {
391
+ timestamp: string;
392
+ requestId: string;
393
+ userAgent?: string;
394
+ ip?: string;
395
+ environment?: string;
396
+ }
397
+
398
+ export default async (req: Request): Promise<SystemContext> => {
399
+ return {
400
+ timestamp: new Date().toISOString(),
401
+ requestId: req.headers["x-request-id"] as string || req.id,
402
+ userAgent: req.headers["user-agent"],
403
+ ip: req.ip || req.connection?.remoteAddress,
404
+ environment: process.env.NODE_ENV,
405
+ };
406
+ };
407
+ ```
408
+
409
+ ## Usage in Handler Config
410
+
411
+ **src/handler.config.ts** - Complete configuration
412
+ ```typescript
413
+ import { force } from "jaypie";
414
+ import Model from "@yourorg/models";
415
+ import systemContextLocal from "./util/systemContextLocal.js";
416
+ import userLocal from "./util/userLocal.js";
417
+ import validateAuth from "./util/validateAuth.js";
418
+
419
+ interface HandlerConfigOptions {
420
+ locals?: Record<string, any>;
421
+ setup?: Array<() => void | Promise<void>>;
422
+ teardown?: Array<() => void | Promise<void>>;
423
+ validate?: Array<() => boolean | Promise<boolean>>;
424
+ }
425
+
426
+ interface HandlerConfig {
427
+ name: string;
428
+ locals: Record<string, any>;
429
+ setup: Array<() => void | Promise<void>>;
430
+ teardown: Array<() => void | Promise<void>>;
431
+ validate: Array<() => boolean | Promise<boolean>>;
432
+ }
433
+
434
+ const handlerConfig = (
435
+ nameOrConfig: string | (HandlerConfig & { name: string }),
436
+ options: HandlerConfigOptions = {}
437
+ ): HandlerConfig => {
438
+ let name: string;
439
+ let locals: Record<string, any>;
440
+ let setup: Array<() => void | Promise<void>>;
441
+ let teardown: Array<() => void | Promise<void>>;
442
+ let validate: Array<() => boolean | Promise<boolean>>;
443
+
444
+ if (typeof nameOrConfig === "object") {
445
+ ({ name, locals = {}, setup = [], teardown = [], validate = [] } = nameOrConfig);
446
+ } else {
447
+ name = nameOrConfig;
448
+ ({ locals = {}, setup = [], teardown = [], validate = [] } = options);
449
+ }
450
+
451
+ return {
452
+ name,
453
+ locals: {
454
+ ...force.object(locals),
455
+ systemContext: systemContextLocal,
456
+ user: userLocal
457
+ },
458
+ setup: [Model.connect, ...force.array(setup)],
459
+ teardown: [...force.array(teardown), Model.disconnect],
460
+ validate: [validateAuth, ...force.array(validate)],
461
+ };
462
+ };
463
+
464
+ export default handlerConfig;
465
+ ```
466
+
467
+ ## Route Authorization Examples
468
+
469
+ **Admin-only route**
470
+ ```typescript
471
+ import { expressHandler } from "jaypie";
472
+ import handlerConfig from "../handler.config.js";
473
+ import authUserHasRoles from "../util/authUserHasRoles.js";
474
+ import type { Request } from "express";
475
+
476
+ interface AdminRouteRequest extends Request {
477
+ locals: {
478
+ user: {
479
+ roles: string[];
480
+ };
481
+ };
482
+ }
483
+
484
+ interface AdminRouteResponse {
485
+ message: string;
486
+ }
487
+
488
+ export default expressHandler(
489
+ handlerConfig({
490
+ name: "adminRoute",
491
+ validate: (req: AdminRouteRequest) => authUserHasRoles(req.locals.user, ["admin"]),
492
+ }),
493
+ async (req: AdminRouteRequest): Promise<AdminRouteResponse> => {
494
+ return { message: "Admin access granted" };
495
+ }
496
+ );
497
+ ```
498
+
499
+ **Group-specific route**
500
+ ```typescript
501
+ import { expressHandler } from "jaypie";
502
+ import handlerConfig from "../handler.config.js";
503
+ import authUserHasAllGroups from "../util/authUserHasAllGroups.js";
504
+ import type { Request } from "express";
505
+
506
+ interface GroupRouteRequest extends Request {
507
+ locals: {
508
+ user: {
509
+ groups: string[];
510
+ };
511
+ };
512
+ }
513
+
514
+ interface GroupRouteResponse {
515
+ message: string;
516
+ }
517
+
518
+ export default expressHandler(
519
+ handlerConfig({
520
+ name: "groupRoute",
521
+ validate: async (req: GroupRouteRequest) => {
522
+ const requiredGroups = ["managers", "developers"];
523
+ await authUserHasAllGroups(req.locals.user, { groupUuids: requiredGroups });
524
+ },
525
+ }),
526
+ async (req: GroupRouteRequest): Promise<GroupRouteResponse> => {
527
+ return { message: "Group access granted" };
528
+ }
529
+ );
530
+ ```
531
+
532
+ **Portfolio-scoped route**
533
+ ```typescript
534
+ import { expressHandler } from "jaypie";
535
+ import handlerConfig from "../handler.config.js";
536
+ import authUserHasPortfolios from "../util/authUserHasPortfolios.js";
537
+ import type { Request } from "express";
538
+
539
+ interface PortfolioRouteRequest extends Request {
540
+ params: {
541
+ portfolioId: string;
542
+ };
543
+ locals: {
544
+ user: {
545
+ portfolios: string[];
546
+ };
547
+ };
548
+ }
549
+
550
+ interface PortfolioRouteResponse {
551
+ message: string;
552
+ }
553
+
554
+ export default expressHandler(
555
+ handlerConfig({
556
+ name: "portfolioRoute",
557
+ validate: (req: PortfolioRouteRequest) => {
558
+ const portfolioId = req.params.portfolioId;
559
+ authUserHasPortfolios(req.locals.user, portfolioId);
560
+ },
561
+ }),
562
+ async (req: PortfolioRouteRequest): Promise<PortfolioRouteResponse> => {
563
+ return { message: "Portfolio access granted" };
564
+ }
565
+ );
566
+ ```
567
+
568
+ ## Testing Authentication
569
+
570
+ **Mock setup**
571
+ ```js
572
+ import { expect, vi } from "vitest";
573
+
574
+ // Mock validateAuth
575
+ vi.mock("./src/util/validateAuth.js", () => ({
576
+ default: vi.fn().mockResolvedValue(true),
577
+ }));
578
+
579
+ // Mock userLocal
580
+ vi.mock("./src/util/userLocal.js", () => ({
581
+ default: vi.fn().mockResolvedValue({
582
+ sub: "test-user-id",
583
+ email: "test@example.com",
584
+ groups: ["test-group-id"],
585
+ portfolios: ["test-portfolio-id"],
586
+ roles: ["user"],
587
+ }),
588
+ }));
589
+ ```
590
+
591
+ **Test authenticated route**
592
+ ```typescript
593
+ import { describe, expect, it } from "vitest";
594
+ import userLocal from "../util/userLocal.js";
595
+ import authenticatedRoute from "../routes/authenticated.route.js";
596
+
597
+ describe("Authenticated Route", () => {
598
+ it("returns data for authenticated user", async () => {
599
+ const mockUser = {
600
+ sub: "user123",
601
+ groups: ["group1"],
602
+ portfolios: ["portfolio1"],
603
+ };
604
+
605
+ userLocal.mockResolvedValue(mockUser);
606
+
607
+ const mockRequest = {
608
+ locals: { user: mockUser },
609
+ } as any;
610
+
611
+ const response = await authenticatedRoute(mockRequest);
612
+
613
+ expect(response).toEqual({
614
+ message: "Access granted",
615
+ user: mockUser,
616
+ });
617
+ });
618
+
619
+ it("throws when user lacks required groups", async () => {
620
+ const mockUser = {
621
+ sub: "user123",
622
+ groups: [],
623
+ portfolios: [],
624
+ };
625
+
626
+ const mockRequest = {
627
+ locals: { user: mockUser },
628
+ } as any;
629
+
630
+ await expect(() => authenticatedRoute(mockRequest)).toThrowForbiddenError();
631
+ });
632
+ });
633
+ ```
634
+
635
+ ## Security Best Practices
636
+
637
+ ### Token Validation
638
+ - Always validate JWT tokens server-side
639
+ - Use proper audience and issuer validation
640
+ - Handle token expiration gracefully
641
+ - Implement token refresh patterns
642
+
643
+ ### Error Handling
644
+ - Never expose sensitive user data in error messages
645
+ - Use generic error messages for authorization failures
646
+ - Log security events for monitoring
647
+ - Implement rate limiting for authentication endpoints
648
+
649
+ ### User Data Protection
650
+ - Only expose necessary user data to client
651
+ - Hash/encrypt sensitive user information
652
+ - Implement proper session management
653
+ - Use secure HTTP headers
654
+
655
+ ## Common Patterns
656
+
657
+ ### Multi-tenant Authorization
658
+ ```typescript
659
+ interface User {
660
+ tenants?: string[];
661
+ }
662
+
663
+ const authUserHasTenantAccess = (user: User, tenantId: string): boolean => {
664
+ const userTenants = user.tenants || [];
665
+ if (!userTenants.includes(tenantId)) {
666
+ throw new ForbiddenError("User lacks tenant access");
667
+ }
668
+ return true;
669
+ };
670
+ ```
671
+
672
+ ### Resource-level Permissions
673
+ ```typescript
674
+ interface User {
675
+ groups: string[];
676
+ }
677
+
678
+ interface Resource {
679
+ groups: string[];
680
+ }
681
+
682
+ const authUserCanAccessResource = async (user: User, resourceId: string): Promise<boolean> => {
683
+ const resource = await Model.Resource.findById(resourceId) as Resource;
684
+ if (!resource) {
685
+ throw new NotFoundError("Resource not found");
686
+ }
687
+
688
+ const hasAccess = resource.groups.some(group =>
689
+ user.groups.includes(group)
690
+ );
691
+
692
+ if (!hasAccess) {
693
+ throw new ForbiddenError("User lacks resource access");
694
+ }
695
+
696
+ return true;
697
+ };
698
+ ```
699
+
700
+ ### Conditional Authorization
701
+ ```typescript
702
+ interface User {
703
+ sub: string;
704
+ roles: string[];
705
+ groups: string[];
706
+ }
707
+
708
+ interface Resource {
709
+ ownerId: string;
710
+ groups: string[];
711
+ }
712
+
713
+ const authUserCanModifyResource = (user: User, resource: Resource): boolean => {
714
+ // Resource owner can always modify
715
+ if (resource.ownerId === user.sub) {
716
+ return true;
717
+ }
718
+
719
+ // Admins can modify any resource
720
+ if (user.roles.includes("admin")) {
721
+ return true;
722
+ }
723
+
724
+ // Managers can modify resources in their groups
725
+ if (user.roles.includes("manager")) {
726
+ const hasGroupAccess = resource.groups.some(group =>
727
+ user.groups.includes(group)
728
+ );
729
+ if (hasGroupAccess) {
730
+ return true;
731
+ }
732
+ }
733
+
734
+ throw new ForbiddenError("User cannot modify this resource");
735
+ };
736
+ ```