@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.
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/agents/architecture-researcher.md +115 -0
- package/agents/code-critic.md +190 -0
- package/agents/delegation-router.md +40 -0
- package/agents/feature-researcher.md +117 -0
- package/agents/interrogator.md +11 -0
- package/agents/pitfalls-researcher.md +112 -0
- package/agents/plan-validator.md +173 -0
- package/agents/proposer.md +61 -0
- package/agents/security-reviewer.md +189 -0
- package/agents/skill-builder.md +43 -0
- package/agents/spec-compliance-reviewer.md +154 -0
- package/agents/stack-researcher.md +89 -0
- package/commands/build.md +766 -0
- package/commands/code-analysis.md +39 -0
- package/commands/code-field.md +22 -0
- package/commands/compliance-check.md +34 -0
- package/commands/configure.md +178 -0
- package/commands/cost-report.md +17 -0
- package/commands/enterprise/adr.md +78 -0
- package/commands/enterprise/brainstorm.md +461 -0
- package/commands/enterprise/design.md +203 -0
- package/commands/enterprise/dev-setup.md +136 -0
- package/commands/enterprise/docker-dev.md +229 -0
- package/commands/enterprise/e2e.md +233 -0
- package/commands/enterprise/feature.md +218 -0
- package/commands/enterprise/gap-analysis.md +204 -0
- package/commands/enterprise/handover.md +195 -0
- package/commands/enterprise/herd.md +152 -0
- package/commands/enterprise/knowledge.md +173 -0
- package/commands/enterprise/onboard.md +86 -0
- package/commands/enterprise/qa-check.md +80 -0
- package/commands/enterprise/reason.md +196 -0
- package/commands/enterprise/review.md +177 -0
- package/commands/enterprise/scaffold.md +153 -0
- package/commands/enterprise/status-report.md +101 -0
- package/commands/enterprise/tech-catalog.md +170 -0
- package/commands/enterprise/test-gen.md +138 -0
- package/commands/evolve.md +39 -0
- package/commands/flags.md +44 -0
- package/commands/interrogate.md +263 -0
- package/commands/lesson.md +15 -0
- package/commands/lessons.md +10 -0
- package/commands/plan.md +44 -0
- package/commands/prune.md +27 -0
- package/commands/star.md +17 -0
- package/commands/supply-chain-scan.md +44 -0
- package/commands/unicode-scan.md +63 -0
- package/commands/verify.md +41 -0
- package/commands/workflow.md +436 -0
- package/hooks/ai-guardrails.sh +114 -0
- package/hooks/audit-log.sh +26 -0
- package/hooks/auto-delegate.sh +45 -0
- package/hooks/auto-evolve.sh +22 -0
- package/hooks/auto-lesson.sh +26 -0
- package/hooks/auto-plan.sh +59 -0
- package/hooks/auto-test.sh +46 -0
- package/hooks/auto-verify.sh +30 -0
- package/hooks/budget-check.sh +24 -0
- package/hooks/code-field-preamble.sh +30 -0
- package/hooks/compliance-gate.sh +50 -0
- package/hooks/content-trust.sh +22 -0
- package/hooks/credential-redact.sh +23 -0
- package/hooks/delegation-trust.sh +15 -0
- package/hooks/detect-test-run.sh +19 -0
- package/hooks/enforcement-lib.sh +60 -0
- package/hooks/evolve-gate.sh +32 -0
- package/hooks/evolve-lib.sh +32 -0
- package/hooks/exfiltration-check.sh +67 -0
- package/hooks/failure-collector.sh +27 -0
- package/hooks/feature-flags.sh +67 -0
- package/hooks/file-provenance.sh +31 -0
- package/hooks/flag-utils.sh +36 -0
- package/hooks/hooks.json +145 -0
- package/hooks/injection-scan.sh +58 -0
- package/hooks/integrity-verify.sh +91 -0
- package/hooks/lessons-check.sh +17 -0
- package/hooks/lockfile-audit.sh +109 -0
- package/hooks/patterns-lib.sh +22 -0
- package/hooks/plan-gate.sh +18 -0
- package/hooks/redact-lib.sh +15 -0
- package/hooks/runtime-mode.sh +56 -0
- package/hooks/session-cleanup.sh +74 -0
- package/hooks/skill-validator.sh +28 -0
- package/hooks/standards-enforce.sh +106 -0
- package/hooks/star-gate.sh +93 -0
- package/hooks/star-preamble.sh +10 -0
- package/hooks/telemetry.sh +33 -0
- package/hooks/todo-prune.sh +84 -0
- package/hooks/unicode-firewall.sh +122 -0
- package/hooks/unicode-lib.sh +66 -0
- package/hooks/unicode-scan-session.sh +96 -0
- package/hooks/validate-command.sh +103 -0
- package/hooks/validate-env.sh +51 -0
- package/hooks/validate-path.sh +81 -0
- package/package.json +40 -0
- package/settings.json +6 -0
- package/templates/ai-config/tool-standards.md +56 -0
- package/templates/architecture/api-first.md +192 -0
- package/templates/architecture/auth-patterns.md +302 -0
- package/templates/architecture/caching-strategy.md +359 -0
- package/templates/architecture/database-patterns.md +347 -0
- package/templates/architecture/event-driven.md +252 -0
- package/templates/architecture/integration-patterns.md +185 -0
- package/templates/architecture/multi-tenancy.md +104 -0
- package/templates/architecture/service-boundaries.md +200 -0
- package/templates/build/brief-template.md +86 -0
- package/templates/build/summary-template.md +100 -0
- package/templates/build/task-plan-template.md +133 -0
- package/templates/communication/effort-estimate.md +54 -0
- package/templates/communication/incident-response.md +59 -0
- package/templates/communication/post-mortem.md +109 -0
- package/templates/communication/risk-register.md +43 -0
- package/templates/communication/sprint-demo-checklist.md +64 -0
- package/templates/communication/stakeholder-presentation-outline.md +84 -0
- package/templates/communication/technical-proposal.md +77 -0
- package/templates/delivery/deployment/deployment-checklist.md +49 -0
- package/templates/delivery/design/solution-design-checklist.md +37 -0
- package/templates/delivery/discovery/stakeholder-questions.md +33 -0
- package/templates/delivery/handover/knowledge-transfer-checklist.md +75 -0
- package/templates/delivery/handover/operational-runbook.md +117 -0
- package/templates/delivery/handover/support-escalation-matrix.md +56 -0
- package/templates/delivery/implementation/blocker-escalation-template.md +55 -0
- package/templates/delivery/implementation/sprint-planning-template.md +49 -0
- package/templates/delivery/implementation/task-decomposition-guide.md +59 -0
- package/templates/delivery/qa/test-plan-template.md +76 -0
- package/templates/delivery/qa/test-results-template.md +55 -0
- package/templates/delivery/qa/uat-signoff-template.md +44 -0
- package/templates/governance/codeowners.md +60 -0
- package/templates/integration/adapter-pattern.md +160 -0
- package/templates/scaffolds/env-validation.md +85 -0
- package/templates/scaffolds/error-handling.md +171 -0
- package/templates/scaffolds/graceful-shutdown.md +139 -0
- package/templates/scaffolds/health-check.md +109 -0
- package/templates/scaffolds/structured-logging.md +134 -0
- package/templates/standards/engineering-standards.md +413 -0
- package/templates/standards/standards-checklist.md +125 -0
- package/templates/tech-catalog.json +663 -0
- package/templates/utilities/project-detection.md +75 -0
- package/templates/utilities/requirements-collection.md +68 -0
- package/templates/utilities/template-rendering.md +81 -0
- package/templates/workflows/architecture-decision.md +90 -0
- package/templates/workflows/bug-investigation.md +83 -0
- package/templates/workflows/feature-implementation.md +80 -0
- package/templates/workflows/refactoring.md +83 -0
- 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.
|