@garethdaine/agentops 0.9.0

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 (148) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +410 -0
  4. package/agents/architecture-researcher.md +115 -0
  5. package/agents/code-critic.md +190 -0
  6. package/agents/delegation-router.md +40 -0
  7. package/agents/feature-researcher.md +117 -0
  8. package/agents/interrogator.md +11 -0
  9. package/agents/pitfalls-researcher.md +112 -0
  10. package/agents/plan-validator.md +173 -0
  11. package/agents/proposer.md +61 -0
  12. package/agents/security-reviewer.md +189 -0
  13. package/agents/skill-builder.md +43 -0
  14. package/agents/spec-compliance-reviewer.md +154 -0
  15. package/agents/stack-researcher.md +89 -0
  16. package/commands/build.md +766 -0
  17. package/commands/code-analysis.md +39 -0
  18. package/commands/code-field.md +22 -0
  19. package/commands/compliance-check.md +34 -0
  20. package/commands/configure.md +178 -0
  21. package/commands/cost-report.md +17 -0
  22. package/commands/enterprise/adr.md +78 -0
  23. package/commands/enterprise/brainstorm.md +461 -0
  24. package/commands/enterprise/design.md +203 -0
  25. package/commands/enterprise/dev-setup.md +136 -0
  26. package/commands/enterprise/docker-dev.md +229 -0
  27. package/commands/enterprise/e2e.md +233 -0
  28. package/commands/enterprise/feature.md +218 -0
  29. package/commands/enterprise/gap-analysis.md +204 -0
  30. package/commands/enterprise/handover.md +195 -0
  31. package/commands/enterprise/herd.md +152 -0
  32. package/commands/enterprise/knowledge.md +173 -0
  33. package/commands/enterprise/onboard.md +86 -0
  34. package/commands/enterprise/qa-check.md +80 -0
  35. package/commands/enterprise/reason.md +196 -0
  36. package/commands/enterprise/review.md +177 -0
  37. package/commands/enterprise/scaffold.md +153 -0
  38. package/commands/enterprise/status-report.md +101 -0
  39. package/commands/enterprise/tech-catalog.md +170 -0
  40. package/commands/enterprise/test-gen.md +138 -0
  41. package/commands/evolve.md +39 -0
  42. package/commands/flags.md +44 -0
  43. package/commands/interrogate.md +263 -0
  44. package/commands/lesson.md +15 -0
  45. package/commands/lessons.md +10 -0
  46. package/commands/plan.md +44 -0
  47. package/commands/prune.md +27 -0
  48. package/commands/star.md +17 -0
  49. package/commands/supply-chain-scan.md +44 -0
  50. package/commands/unicode-scan.md +63 -0
  51. package/commands/verify.md +41 -0
  52. package/commands/workflow.md +436 -0
  53. package/hooks/ai-guardrails.sh +114 -0
  54. package/hooks/audit-log.sh +26 -0
  55. package/hooks/auto-delegate.sh +45 -0
  56. package/hooks/auto-evolve.sh +22 -0
  57. package/hooks/auto-lesson.sh +26 -0
  58. package/hooks/auto-plan.sh +59 -0
  59. package/hooks/auto-test.sh +46 -0
  60. package/hooks/auto-verify.sh +30 -0
  61. package/hooks/budget-check.sh +24 -0
  62. package/hooks/code-field-preamble.sh +30 -0
  63. package/hooks/compliance-gate.sh +50 -0
  64. package/hooks/content-trust.sh +22 -0
  65. package/hooks/credential-redact.sh +23 -0
  66. package/hooks/delegation-trust.sh +15 -0
  67. package/hooks/detect-test-run.sh +19 -0
  68. package/hooks/enforcement-lib.sh +60 -0
  69. package/hooks/evolve-gate.sh +32 -0
  70. package/hooks/evolve-lib.sh +32 -0
  71. package/hooks/exfiltration-check.sh +67 -0
  72. package/hooks/failure-collector.sh +27 -0
  73. package/hooks/feature-flags.sh +67 -0
  74. package/hooks/file-provenance.sh +31 -0
  75. package/hooks/flag-utils.sh +36 -0
  76. package/hooks/hooks.json +145 -0
  77. package/hooks/injection-scan.sh +58 -0
  78. package/hooks/integrity-verify.sh +91 -0
  79. package/hooks/lessons-check.sh +17 -0
  80. package/hooks/lockfile-audit.sh +109 -0
  81. package/hooks/patterns-lib.sh +22 -0
  82. package/hooks/plan-gate.sh +18 -0
  83. package/hooks/redact-lib.sh +15 -0
  84. package/hooks/runtime-mode.sh +56 -0
  85. package/hooks/session-cleanup.sh +74 -0
  86. package/hooks/skill-validator.sh +28 -0
  87. package/hooks/standards-enforce.sh +106 -0
  88. package/hooks/star-gate.sh +93 -0
  89. package/hooks/star-preamble.sh +10 -0
  90. package/hooks/telemetry.sh +33 -0
  91. package/hooks/todo-prune.sh +84 -0
  92. package/hooks/unicode-firewall.sh +122 -0
  93. package/hooks/unicode-lib.sh +66 -0
  94. package/hooks/unicode-scan-session.sh +96 -0
  95. package/hooks/validate-command.sh +103 -0
  96. package/hooks/validate-env.sh +51 -0
  97. package/hooks/validate-path.sh +81 -0
  98. package/package.json +40 -0
  99. package/settings.json +6 -0
  100. package/templates/ai-config/tool-standards.md +56 -0
  101. package/templates/architecture/api-first.md +192 -0
  102. package/templates/architecture/auth-patterns.md +302 -0
  103. package/templates/architecture/caching-strategy.md +359 -0
  104. package/templates/architecture/database-patterns.md +347 -0
  105. package/templates/architecture/event-driven.md +252 -0
  106. package/templates/architecture/integration-patterns.md +185 -0
  107. package/templates/architecture/multi-tenancy.md +104 -0
  108. package/templates/architecture/service-boundaries.md +200 -0
  109. package/templates/build/brief-template.md +86 -0
  110. package/templates/build/summary-template.md +100 -0
  111. package/templates/build/task-plan-template.md +133 -0
  112. package/templates/communication/effort-estimate.md +54 -0
  113. package/templates/communication/incident-response.md +59 -0
  114. package/templates/communication/post-mortem.md +109 -0
  115. package/templates/communication/risk-register.md +43 -0
  116. package/templates/communication/sprint-demo-checklist.md +64 -0
  117. package/templates/communication/stakeholder-presentation-outline.md +84 -0
  118. package/templates/communication/technical-proposal.md +77 -0
  119. package/templates/delivery/deployment/deployment-checklist.md +49 -0
  120. package/templates/delivery/design/solution-design-checklist.md +37 -0
  121. package/templates/delivery/discovery/stakeholder-questions.md +33 -0
  122. package/templates/delivery/handover/knowledge-transfer-checklist.md +75 -0
  123. package/templates/delivery/handover/operational-runbook.md +117 -0
  124. package/templates/delivery/handover/support-escalation-matrix.md +56 -0
  125. package/templates/delivery/implementation/blocker-escalation-template.md +55 -0
  126. package/templates/delivery/implementation/sprint-planning-template.md +49 -0
  127. package/templates/delivery/implementation/task-decomposition-guide.md +59 -0
  128. package/templates/delivery/qa/test-plan-template.md +76 -0
  129. package/templates/delivery/qa/test-results-template.md +55 -0
  130. package/templates/delivery/qa/uat-signoff-template.md +44 -0
  131. package/templates/governance/codeowners.md +60 -0
  132. package/templates/integration/adapter-pattern.md +160 -0
  133. package/templates/scaffolds/env-validation.md +85 -0
  134. package/templates/scaffolds/error-handling.md +171 -0
  135. package/templates/scaffolds/graceful-shutdown.md +139 -0
  136. package/templates/scaffolds/health-check.md +109 -0
  137. package/templates/scaffolds/structured-logging.md +134 -0
  138. package/templates/standards/engineering-standards.md +413 -0
  139. package/templates/standards/standards-checklist.md +125 -0
  140. package/templates/tech-catalog.json +663 -0
  141. package/templates/utilities/project-detection.md +75 -0
  142. package/templates/utilities/requirements-collection.md +68 -0
  143. package/templates/utilities/template-rendering.md +81 -0
  144. package/templates/workflows/architecture-decision.md +90 -0
  145. package/templates/workflows/bug-investigation.md +83 -0
  146. package/templates/workflows/feature-implementation.md +80 -0
  147. package/templates/workflows/refactoring.md +83 -0
  148. package/templates/workflows/spike-exploration.md +82 -0
@@ -0,0 +1,192 @@
1
+ # Architecture Pattern: API-First Design
2
+
3
+ ## When to Use
4
+
5
+ - Building services consumed by multiple clients (web, mobile, CLI, third-party)
6
+ - Teams working in parallel on frontend and backend
7
+ - Public or partner-facing APIs where stability guarantees matter
8
+ - Microservice architectures where contracts prevent integration drift
9
+
10
+ ## Pattern Description
11
+
12
+ API-first means the API contract is designed, reviewed, and agreed upon before any implementation begins. The OpenAPI specification becomes the single source of truth. Code is generated from the spec (not the other way around), and breaking changes follow a strict deprecation workflow.
13
+
14
+ ## OpenAPI Spec as Source of Truth
15
+
16
+ Define your API contract before writing a single route handler:
17
+
18
+ ```typescript
19
+ /**
20
+ * Generate types directly from the OpenAPI spec.
21
+ * Tools like openapi-typescript produce exact type definitions.
22
+ *
23
+ * npx openapi-typescript ./api/openapi.yaml -o ./src/generated/api-types.ts
24
+ */
25
+ import type { paths, components } from './generated/api-types';
26
+
27
+ // Request and response types are derived from the spec, not hand-written
28
+ type CreateProjectBody = components['schemas']['CreateProjectInput'];
29
+ type ProjectResponse = components['schemas']['Project'];
30
+ type ListProjectsParams = paths['/projects']['get']['parameters']['query'];
31
+ ```
32
+
33
+ ### Spec-Driven Validation Middleware
34
+
35
+ ```typescript
36
+ import { OpenAPIValidator } from 'openapi-backend';
37
+
38
+ const validator = new OpenAPIValidator({
39
+ definition: './api/openapi.yaml',
40
+ });
41
+
42
+ export function validateRequest(req: Request, res: Response, next: NextFunction) {
43
+ const result = validator.validateRequest(req);
44
+ if (result.errors && result.errors.length > 0) {
45
+ throw new ValidationError('Request does not match API contract', {
46
+ errors: result.errors,
47
+ });
48
+ }
49
+ next();
50
+ }
51
+ ```
52
+
53
+ ## API Versioning Strategies
54
+
55
+ ### URL Path Versioning (Recommended for Public APIs)
56
+
57
+ ```typescript
58
+ // Simple, explicit, easy to route and cache
59
+ router.use('/api/v1/projects', v1ProjectRoutes);
60
+ router.use('/api/v2/projects', v2ProjectRoutes);
61
+ ```
62
+
63
+ **Pros:** Visible in logs and debugging, easy CDN cache segmentation, simple routing.
64
+ **Cons:** URL pollution, clients must update base URLs on major version bumps.
65
+
66
+ ### Header Versioning (Recommended for Internal APIs)
67
+
68
+ ```typescript
69
+ export function versionRouter(req: Request, res: Response, next: NextFunction) {
70
+ const version = req.headers['api-version'] ?? req.headers['accept-version'] ?? '1';
71
+ req.apiVersion = parseInt(version as string, 10);
72
+ next();
73
+ }
74
+
75
+ // Route handler checks version
76
+ async function getProjects(req: Request, res: Response) {
77
+ if (req.apiVersion >= 2) {
78
+ return res.json(await projectService.listV2(req.query));
79
+ }
80
+ return res.json(await projectService.listV1(req.query));
81
+ }
82
+ ```
83
+
84
+ **Pros:** Clean URLs, fine-grained per-endpoint versioning.
85
+ **Cons:** Hidden from logs without explicit extraction, harder to cache.
86
+
87
+ ### Query Parameter Versioning (Avoid for New APIs)
88
+
89
+ ```typescript
90
+ // /api/projects?version=2
91
+ // Only appropriate for legacy systems or simple internal tools.
92
+ ```
93
+
94
+ ## Breaking Change Policy
95
+
96
+ Define what constitutes a breaking change and enforce it in CI:
97
+
98
+ ```typescript
99
+ /**
100
+ * BREAKING (requires major version bump):
101
+ * - Removing an endpoint
102
+ * - Removing or renaming a required field
103
+ * - Changing a field type
104
+ * - Narrowing allowed enum values
105
+ * - Changing authentication requirements
106
+ *
107
+ * NON-BREAKING (minor or patch):
108
+ * - Adding a new optional field to a response
109
+ * - Adding a new endpoint
110
+ * - Widening allowed enum values
111
+ * - Adding optional query parameters
112
+ */
113
+
114
+ // CI check: compare current spec against published baseline
115
+ // npx openapi-diff ./api/openapi-baseline.yaml ./api/openapi.yaml --breaking
116
+ ```
117
+
118
+ ## Deprecation Workflow
119
+
120
+ ```typescript
121
+ /**
122
+ * Step 1: Mark deprecated in spec and add Sunset header
123
+ * Step 2: Log usage metrics for deprecated endpoints
124
+ * Step 3: Notify consumers via changelog and response headers
125
+ * Step 4: Remove after sunset date
126
+ */
127
+ export function deprecationMiddleware(
128
+ sunsetDate: string,
129
+ replacementUrl?: string,
130
+ ) {
131
+ return (req: Request, res: Response, next: NextFunction) => {
132
+ res.setHeader('Deprecation', 'true');
133
+ res.setHeader('Sunset', sunsetDate);
134
+ if (replacementUrl) {
135
+ res.setHeader('Link', `<${replacementUrl}>; rel="successor-version"`);
136
+ }
137
+ logger.warn('Deprecated endpoint called', {
138
+ path: req.path,
139
+ sunsetDate,
140
+ callerIp: req.ip,
141
+ apiKey: req.headers['x-api-key'],
142
+ });
143
+ next();
144
+ };
145
+ }
146
+
147
+ router.get(
148
+ '/api/v1/projects',
149
+ deprecationMiddleware('2025-09-01', '/api/v2/projects'),
150
+ v1GetProjects,
151
+ );
152
+ ```
153
+
154
+ ## Contract Testing in CI
155
+
156
+ ```typescript
157
+ describe('API contract', () => {
158
+ it('should match the OpenAPI spec for GET /projects', async () => {
159
+ const response = await request(app).get('/api/v1/projects').expect(200);
160
+
161
+ const validation = validator.validateResponse(
162
+ response.body,
163
+ { method: 'GET', path: '/api/v1/projects', statusCode: 200 },
164
+ );
165
+ expect(validation.errors).toHaveLength(0);
166
+ });
167
+
168
+ it('should reject requests missing required fields', async () => {
169
+ const response = await request(app)
170
+ .post('/api/v1/projects')
171
+ .send({})
172
+ .expect(400);
173
+
174
+ expect(response.body.errors).toBeDefined();
175
+ });
176
+ });
177
+ ```
178
+
179
+ ## Trade-offs
180
+
181
+ - **Upfront cost:** Designing the spec first takes time but prevents costly rework later.
182
+ - **Tooling dependency:** You rely on code generation tools staying maintained.
183
+ - **Spec drift risk:** Without CI enforcement, implementation can diverge from the spec.
184
+ - **Team discipline:** Requires buy-in from all contributors to treat the spec as authoritative.
185
+
186
+ ## Common Pitfalls
187
+
188
+ 1. **Generating spec from code** — Inverts the intended flow. The spec should drive the code, not reflect it after the fact.
189
+ 2. **Versioning too eagerly** — Not every change needs a new version. Additive changes are non-breaking.
190
+ 3. **No sunset enforcement** — Marking endpoints deprecated without a removal date means they live forever.
191
+ 4. **Ignoring error schemas** — Define error response shapes in the spec. Clients need to parse errors reliably.
192
+ 5. **Skipping contract tests in CI** — The spec is only useful if violations break the build.
@@ -0,0 +1,302 @@
1
+ # Architecture Pattern: Authentication and Authorisation
2
+
3
+ ## When to Use
4
+
5
+ - Any system that serves multiple users or exposes APIs
6
+ - Services that need role-based or attribute-based access control
7
+ - APIs consumed by third-party clients requiring OAuth2 flows
8
+ - Systems where audit trails and permission boundaries are compliance requirements
9
+
10
+ ## Pattern Description
11
+
12
+ Authentication verifies identity (who are you?). Authorisation verifies permissions (what can you do?). These are separate concerns and should be implemented as distinct middleware layers. Token-based authentication with JWT is the dominant pattern for stateless APIs, while session-based auth remains appropriate for server-rendered applications.
13
+
14
+ ## JWT Structure and Validation
15
+
16
+ ```typescript
17
+ /**
18
+ * JWT payload structure. Keep claims minimal — the token
19
+ * travels with every request.
20
+ */
21
+ export interface TokenPayload {
22
+ sub: string; // User ID
23
+ email: string;
24
+ roles: string[];
25
+ tenantId: string;
26
+ iat: number; // Issued at (seconds since epoch)
27
+ exp: number; // Expiration (seconds since epoch)
28
+ jti: string; // Unique token ID for revocation
29
+ }
30
+
31
+ export function verifyToken(token: string, secret: string): TokenPayload {
32
+ try {
33
+ const payload = jwt.verify(token, secret, {
34
+ algorithms: ['HS256'],
35
+ clockTolerance: 5, // 5 seconds tolerance for clock skew
36
+ }) as TokenPayload;
37
+
38
+ // Additional validation beyond signature check
39
+ if (!payload.sub || !payload.tenantId) {
40
+ throw new AuthenticationError('Token missing required claims');
41
+ }
42
+
43
+ return payload;
44
+ } catch (error) {
45
+ if (error instanceof jwt.TokenExpiredError) {
46
+ throw new AuthenticationError('Token expired');
47
+ }
48
+ if (error instanceof jwt.JsonWebTokenError) {
49
+ throw new AuthenticationError('Invalid token');
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Authentication Middleware
57
+
58
+ ```typescript
59
+ export function authMiddleware(req: Request, res: Response, next: NextFunction) {
60
+ const header = req.headers.authorization;
61
+ if (!header?.startsWith('Bearer ')) {
62
+ throw new AuthenticationError('Missing or malformed Authorization header');
63
+ }
64
+
65
+ const token = header.slice(7);
66
+ const payload = verifyToken(token, config.jwtSecret);
67
+
68
+ // Attach to request for downstream use
69
+ req.user = {
70
+ id: payload.sub,
71
+ email: payload.email,
72
+ roles: payload.roles,
73
+ tenantId: payload.tenantId,
74
+ };
75
+
76
+ next();
77
+ }
78
+ ```
79
+
80
+ ## OAuth2 Flows
81
+
82
+ ### Authorization Code Flow (User-Facing Applications)
83
+
84
+ ```typescript
85
+ /**
86
+ * Step 1: Redirect user to identity provider
87
+ * Step 2: IDP redirects back with authorization code
88
+ * Step 3: Exchange code for tokens server-side (never in the browser)
89
+ */
90
+ export async function handleOAuthCallback(req: Request, res: Response) {
91
+ const { code, state } = req.query;
92
+
93
+ // Verify state parameter to prevent CSRF
94
+ const savedState = await sessionStore.get(`oauth_state:${state}`);
95
+ if (!savedState) {
96
+ throw new AuthenticationError('Invalid OAuth state parameter');
97
+ }
98
+
99
+ // Exchange authorization code for tokens
100
+ const tokenResponse = await httpClient.post(config.oauth.tokenEndpoint, {
101
+ grant_type: 'authorization_code',
102
+ code,
103
+ redirect_uri: config.oauth.redirectUri,
104
+ client_id: config.oauth.clientId,
105
+ client_secret: config.oauth.clientSecret,
106
+ });
107
+
108
+ const { access_token, refresh_token, expires_in } = tokenResponse.data;
109
+
110
+ // Store refresh token securely server-side
111
+ await tokenStore.save(savedState.userId, {
112
+ refreshToken: refresh_token,
113
+ expiresAt: new Date(Date.now() + expires_in * 1000),
114
+ });
115
+
116
+ // Set access token in HTTP-only cookie or return to client
117
+ res.cookie('access_token', access_token, {
118
+ httpOnly: true,
119
+ secure: true,
120
+ sameSite: 'strict',
121
+ maxAge: expires_in * 1000,
122
+ });
123
+
124
+ res.redirect(savedState.returnUrl);
125
+ }
126
+ ```
127
+
128
+ ### Client Credentials Flow (Service-to-Service)
129
+
130
+ ```typescript
131
+ /**
132
+ * For machine-to-machine authentication where no user is involved.
133
+ * The service authenticates with its own credentials.
134
+ */
135
+ export async function getServiceToken(): Promise<string> {
136
+ const cached = tokenCache.get('service_token');
137
+ if (cached && cached.expiresAt > Date.now() + 60_000) {
138
+ return cached.accessToken;
139
+ }
140
+
141
+ const response = await httpClient.post(config.oauth.tokenEndpoint, {
142
+ grant_type: 'client_credentials',
143
+ client_id: config.serviceClientId,
144
+ client_secret: config.serviceClientSecret,
145
+ scope: 'read:orders write:orders',
146
+ });
147
+
148
+ tokenCache.set('service_token', {
149
+ accessToken: response.data.access_token,
150
+ expiresAt: Date.now() + response.data.expires_in * 1000,
151
+ });
152
+
153
+ return response.data.access_token;
154
+ }
155
+ ```
156
+
157
+ ## Token Refresh Strategy
158
+
159
+ ```typescript
160
+ /**
161
+ * Refresh tokens before they expire. Use a buffer window
162
+ * to avoid race conditions where a token expires mid-request.
163
+ */
164
+ export async function getValidAccessToken(userId: string): Promise<string> {
165
+ const stored = await tokenStore.get(userId);
166
+ if (!stored) {
167
+ throw new AuthenticationError('No token found — user must re-authenticate');
168
+ }
169
+
170
+ const bufferMs = 60_000; // Refresh 60 seconds before expiry
171
+ if (stored.expiresAt.getTime() - Date.now() > bufferMs) {
172
+ return stored.accessToken;
173
+ }
174
+
175
+ const response = await httpClient.post(config.oauth.tokenEndpoint, {
176
+ grant_type: 'refresh_token',
177
+ refresh_token: stored.refreshToken,
178
+ client_id: config.oauth.clientId,
179
+ client_secret: config.oauth.clientSecret,
180
+ });
181
+
182
+ await tokenStore.save(userId, {
183
+ accessToken: response.data.access_token,
184
+ refreshToken: response.data.refresh_token ?? stored.refreshToken,
185
+ expiresAt: new Date(Date.now() + response.data.expires_in * 1000),
186
+ });
187
+
188
+ return response.data.access_token;
189
+ }
190
+ ```
191
+
192
+ ## RBAC vs ABAC
193
+
194
+ ### Role-Based Access Control (Simpler, Start Here)
195
+
196
+ ```typescript
197
+ /**
198
+ * Use RBAC when permissions map cleanly to job functions.
199
+ * Roles: admin, manager, member, viewer
200
+ */
201
+ export function requireRole(...roles: string[]) {
202
+ return (req: Request, res: Response, next: NextFunction) => {
203
+ const userRoles = req.user.roles;
204
+ const hasRole = roles.some((role) => userRoles.includes(role));
205
+ if (!hasRole) {
206
+ throw new AuthorisationError(`Requires one of: ${roles.join(', ')}`);
207
+ }
208
+ next();
209
+ };
210
+ }
211
+
212
+ // Usage
213
+ router.delete('/projects/:id', requireRole('admin', 'manager'), deleteProject);
214
+ ```
215
+
216
+ ### Attribute-Based Access Control (More Flexible, More Complex)
217
+
218
+ ```typescript
219
+ /**
220
+ * Use ABAC when access depends on resource attributes, time,
221
+ * location, or other contextual factors beyond simple roles.
222
+ */
223
+ export interface AccessPolicy {
224
+ effect: 'allow' | 'deny';
225
+ condition: (context: AccessContext) => boolean;
226
+ }
227
+
228
+ export interface AccessContext {
229
+ user: { id: string; roles: string[]; department: string };
230
+ resource: { type: string; ownerId: string; status: string };
231
+ action: string;
232
+ environment: { time: Date; ipAddress: string };
233
+ }
234
+
235
+ export function evaluatePolicies(
236
+ policies: AccessPolicy[],
237
+ context: AccessContext,
238
+ ): boolean {
239
+ // Deny takes precedence
240
+ const denied = policies.some(
241
+ (p) => p.effect === 'deny' && p.condition(context),
242
+ );
243
+ if (denied) return false;
244
+
245
+ // At least one allow must match
246
+ return policies.some(
247
+ (p) => p.effect === 'allow' && p.condition(context),
248
+ );
249
+ }
250
+
251
+ // Example policy: users can only edit their own resources
252
+ const ownerOnlyEdit: AccessPolicy = {
253
+ effect: 'allow',
254
+ condition: (ctx) =>
255
+ ctx.action === 'edit' && ctx.resource.ownerId === ctx.user.id,
256
+ };
257
+ ```
258
+
259
+ ## Permission System Architecture
260
+
261
+ ```typescript
262
+ /**
263
+ * Permissions as granular capabilities, grouped into roles.
264
+ * Stored in the database, cached aggressively.
265
+ */
266
+ export const PERMISSIONS = {
267
+ 'projects:read': 'View projects',
268
+ 'projects:write': 'Create and edit projects',
269
+ 'projects:delete': 'Delete projects',
270
+ 'billing:read': 'View billing information',
271
+ 'billing:manage': 'Manage billing and subscriptions',
272
+ } as const;
273
+
274
+ export type Permission = keyof typeof PERMISSIONS;
275
+
276
+ export function requirePermission(...permissions: Permission[]) {
277
+ return async (req: Request, res: Response, next: NextFunction) => {
278
+ const userPermissions = await permissionCache.getForUser(req.user.id);
279
+ const hasAll = permissions.every((p) => userPermissions.includes(p));
280
+ if (!hasAll) {
281
+ throw new AuthorisationError(`Missing permissions: ${permissions.join(', ')}`);
282
+ }
283
+ next();
284
+ };
285
+ }
286
+ ```
287
+
288
+ ## Trade-offs
289
+
290
+ - **JWT size:** Every claim increases token size and bandwidth. Keep payloads lean.
291
+ - **Stateless vs revocation:** JWTs cannot be revoked without a blocklist, which reintroduces state.
292
+ - **RBAC simplicity vs ABAC flexibility:** RBAC is easy to reason about but rigid. ABAC handles complex policies but requires more infrastructure.
293
+ - **Token lifetime:** Short-lived tokens are more secure but increase refresh traffic.
294
+
295
+ ## Common Pitfalls
296
+
297
+ 1. **Storing sensitive data in JWT payload** — JWTs are encoded, not encrypted. Never include passwords, PII, or secrets.
298
+ 2. **No token revocation strategy** — If a user is compromised, you need a way to invalidate their tokens before expiry. Maintain a short blocklist in Redis.
299
+ 3. **Checking roles instead of permissions** — `if (user.role === 'admin')` scattered through code is unmaintainable. Check permissions, map roles to permissions centrally.
300
+ 4. **Missing CSRF protection on cookie-based auth** — HTTP-only cookies need SameSite flags and CSRF tokens for state-changing requests.
301
+ 5. **Symmetric JWT signing in multi-service setups** — Use asymmetric keys (RS256) so services can verify tokens without knowing the signing secret.
302
+ 6. **Hardcoded roles** — Store role-permission mappings in the database so they can be updated without deployments.