@jigyasudham/veto 0.8.3 → 1.1.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 (114) hide show
  1. package/README.md +217 -54
  2. package/dist/adapters/index.js +4 -3
  3. package/dist/agents/executor.js +36 -3
  4. package/dist/cli.js +246 -7
  5. package/dist/context/reader.js +113 -0
  6. package/dist/council/index.js +3 -1
  7. package/dist/memory/local.js +18 -1
  8. package/dist/memory/schema.js +12 -10
  9. package/dist/plugins/loader.js +49 -0
  10. package/dist/router/index.js +2 -2
  11. package/dist/router/learning-updater.js +45 -1
  12. package/dist/server.js +507 -21
  13. package/dist/watcher/index.js +77 -0
  14. package/dist/workflow/pipeline.js +64 -0
  15. package/package.json +12 -3
  16. package/.claude/settings.local.json +0 -9
  17. package/src/adapters/claude.ts +0 -70
  18. package/src/adapters/codex.ts +0 -71
  19. package/src/adapters/gemini.ts +0 -71
  20. package/src/adapters/index.ts +0 -217
  21. package/src/agents/development/api.ts +0 -120
  22. package/src/agents/development/backend.ts +0 -85
  23. package/src/agents/development/coder.ts +0 -213
  24. package/src/agents/development/database.ts +0 -83
  25. package/src/agents/development/debugger.ts +0 -238
  26. package/src/agents/development/devops.ts +0 -86
  27. package/src/agents/development/frontend.ts +0 -85
  28. package/src/agents/development/migration.ts +0 -144
  29. package/src/agents/development/performance.ts +0 -144
  30. package/src/agents/development/refactor.ts +0 -86
  31. package/src/agents/development/reviewer.ts +0 -268
  32. package/src/agents/development/tester.ts +0 -151
  33. package/src/agents/executor.ts +0 -158
  34. package/src/agents/memory/context-manager.ts +0 -171
  35. package/src/agents/memory/decision-logger.ts +0 -160
  36. package/src/agents/memory/knowledge-base.ts +0 -124
  37. package/src/agents/memory/pattern-learner.ts +0 -143
  38. package/src/agents/memory/project-mapper.ts +0 -118
  39. package/src/agents/quality/accessibility.ts +0 -99
  40. package/src/agents/quality/code-quality.ts +0 -115
  41. package/src/agents/quality/compatibility.ts +0 -58
  42. package/src/agents/quality/documentation.ts +0 -105
  43. package/src/agents/quality/error-handling.ts +0 -96
  44. package/src/agents/research/competitor-analyzer.ts +0 -45
  45. package/src/agents/research/cost-analyzer.ts +0 -54
  46. package/src/agents/research/estimator.ts +0 -60
  47. package/src/agents/research/ethics-bias.ts +0 -113
  48. package/src/agents/research/researcher.ts +0 -114
  49. package/src/agents/research/risk-assessor.ts +0 -63
  50. package/src/agents/research/tech-advisor.ts +0 -55
  51. package/src/agents/security/auth.ts +0 -287
  52. package/src/agents/security/dependency-audit.ts +0 -337
  53. package/src/agents/security/penetration.ts +0 -262
  54. package/src/agents/security/privacy.ts +0 -285
  55. package/src/agents/security/scanner.ts +0 -322
  56. package/src/agents/security/secrets.ts +0 -249
  57. package/src/agents/types.ts +0 -66
  58. package/src/agents/workflow/automation.ts +0 -59
  59. package/src/agents/workflow/file-manager.ts +0 -52
  60. package/src/agents/workflow/git-agent.ts +0 -55
  61. package/src/agents/workflow/reporter.ts +0 -51
  62. package/src/agents/workflow/search-agent.ts +0 -40
  63. package/src/agents/workflow/task-coordinator.ts +0 -41
  64. package/src/agents/workflow/task-planner.ts +0 -47
  65. package/src/cli.ts +0 -204
  66. package/src/council/decision-engine.ts +0 -171
  67. package/src/council/devil-advocate.ts +0 -116
  68. package/src/council/index.ts +0 -44
  69. package/src/council/lead-developer.ts +0 -118
  70. package/src/council/legal-compliance.ts +0 -152
  71. package/src/council/product-manager.ts +0 -102
  72. package/src/council/security.ts +0 -172
  73. package/src/council/system-architect.ts +0 -132
  74. package/src/council/types.ts +0 -33
  75. package/src/council/ux-designer.ts +0 -121
  76. package/src/memory/local.ts +0 -305
  77. package/src/memory/schema.ts +0 -174
  78. package/src/memory/sync.ts +0 -274
  79. package/src/router/complexity-scorer.ts +0 -96
  80. package/src/router/context-compressor.ts +0 -74
  81. package/src/router/index.ts +0 -60
  82. package/src/router/learning-updater.ts +0 -271
  83. package/src/router/model-selector.ts +0 -83
  84. package/src/router/rate-monitor.ts +0 -103
  85. package/src/server.ts +0 -1038
  86. package/src/skills/development/skill-api-design.ts +0 -329
  87. package/src/skills/development/skill-auth.ts +0 -271
  88. package/src/skills/development/skill-ci-cd.ts +0 -0
  89. package/src/skills/development/skill-crud.ts +0 -209
  90. package/src/skills/development/skill-db-schema.ts +0 -0
  91. package/src/skills/development/skill-docker.ts +0 -0
  92. package/src/skills/development/skill-env-setup.ts +0 -0
  93. package/src/skills/development/skill-scaffold.ts +0 -323
  94. package/src/skills/intelligence/skill-complexity-score.ts +0 -69
  95. package/src/skills/intelligence/skill-cost-track.ts +0 -39
  96. package/src/skills/intelligence/skill-learning-loop.ts +0 -69
  97. package/src/skills/intelligence/skill-pattern-detect.ts +0 -38
  98. package/src/skills/intelligence/skill-rate-watch.ts +0 -61
  99. package/src/skills/memory/skill-context-compress.ts +0 -98
  100. package/src/skills/memory/skill-cross-sync.ts +0 -104
  101. package/src/skills/memory/skill-decision-log.ts +0 -119
  102. package/src/skills/memory/skill-session-restore.ts +0 -59
  103. package/src/skills/memory/skill-session-save.ts +0 -94
  104. package/src/skills/quality/skill-accessibility.ts +0 -0
  105. package/src/skills/quality/skill-code-review.ts +0 -84
  106. package/src/skills/quality/skill-docs-gen.ts +0 -0
  107. package/src/skills/quality/skill-perf-audit.ts +0 -0
  108. package/src/skills/quality/skill-security-scan.ts +0 -91
  109. package/src/skills/quality/skill-test-suite.ts +0 -290
  110. package/src/skills/workflow/skill-deploy.ts +0 -0
  111. package/src/skills/workflow/skill-git-workflow.ts +0 -0
  112. package/src/skills/workflow/skill-rollback.ts +0 -0
  113. package/src/skills/workflow/skill-task-breakdown.ts +0 -0
  114. package/tsconfig.json +0 -20
@@ -1,329 +0,0 @@
1
- // Skill: api-design — REST API design guide with OpenAPI 3.0 skeleton
2
-
3
- export interface SkillInput {
4
- task: string;
5
- context?: string;
6
- options?: Record<string, unknown>;
7
- }
8
-
9
- export interface SkillOutput {
10
- skill: string;
11
- template?: string;
12
- checklist: string[];
13
- patterns: string[];
14
- gotchas: string[];
15
- resources: string[];
16
- }
17
-
18
- const TEMPLATE = `
19
- openapi: 3.0.3
20
- info:
21
- title: My API
22
- description: >
23
- RESTful API for My Application.
24
- All endpoints require a Bearer token unless marked as public.
25
- version: 1.0.0
26
- contact:
27
- name: API Support
28
- email: api@example.com
29
-
30
- servers:
31
- - url: https://api.example.com/v1
32
- description: Production
33
- - url: http://localhost:3000/v1
34
- description: Local development
35
-
36
- security:
37
- - BearerAuth: []
38
-
39
- # ── Components ─────────────────────────────────────────────────────────────
40
- components:
41
- securitySchemes:
42
- BearerAuth:
43
- type: http
44
- scheme: bearer
45
- bearerFormat: JWT
46
-
47
- schemas:
48
- # Pagination wrapper
49
- PaginatedResponse:
50
- type: object
51
- required: [data, meta]
52
- properties:
53
- data:
54
- type: array
55
- items: {}
56
- meta:
57
- type: object
58
- required: [total, page, pageSize, totalPages]
59
- properties:
60
- total: { type: integer, example: 142 }
61
- page: { type: integer, example: 1 }
62
- pageSize: { type: integer, example: 20 }
63
- totalPages: { type: integer, example: 8 }
64
-
65
- # Error envelope
66
- ErrorResponse:
67
- type: object
68
- required: [error]
69
- properties:
70
- error:
71
- type: object
72
- required: [code, message]
73
- properties:
74
- code: { type: string, example: VALIDATION_ERROR }
75
- message: { type: string, example: "name must not be empty" }
76
- details: { type: array, items: { type: string } }
77
-
78
- # Resource example
79
- Item:
80
- type: object
81
- required: [id, name, userId, createdAt, updatedAt]
82
- properties:
83
- id: { type: string, format: uuid }
84
- name: { type: string, minLength: 1, maxLength: 100 }
85
- description: { type: string, maxLength: 500 }
86
- userId: { type: string, format: uuid }
87
- createdAt: { type: string, format: date-time }
88
- updatedAt: { type: string, format: date-time }
89
-
90
- CreateItemRequest:
91
- type: object
92
- required: [name]
93
- properties:
94
- name: { type: string, minLength: 1, maxLength: 100 }
95
- description: { type: string, maxLength: 500 }
96
-
97
- UpdateItemRequest:
98
- type: object
99
- minProperties: 1
100
- properties:
101
- name: { type: string, minLength: 1, maxLength: 100 }
102
- description: { type: string, maxLength: 500 }
103
-
104
- parameters:
105
- ItemId:
106
- name: id
107
- in: path
108
- required: true
109
- schema: { type: string, format: uuid }
110
- PageParam:
111
- name: page
112
- in: query
113
- schema: { type: integer, minimum: 1, default: 1 }
114
- PageSizeParam:
115
- name: pageSize
116
- in: query
117
- schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
118
- SortParam:
119
- name: sort
120
- in: query
121
- schema: { type: string, enum: [createdAt, updatedAt, name], default: createdAt }
122
- OrderParam:
123
- name: order
124
- in: query
125
- schema: { type: string, enum: [asc, desc], default: desc }
126
-
127
- responses:
128
- Unauthorized:
129
- description: Missing or invalid authentication token
130
- content:
131
- application/json:
132
- schema: { $ref: '#/components/schemas/ErrorResponse' }
133
- Forbidden:
134
- description: Authenticated but not authorised to access this resource
135
- content:
136
- application/json:
137
- schema: { $ref: '#/components/schemas/ErrorResponse' }
138
- NotFound:
139
- description: Resource not found
140
- content:
141
- application/json:
142
- schema: { $ref: '#/components/schemas/ErrorResponse' }
143
- UnprocessableEntity:
144
- description: Validation error in request body
145
- content:
146
- application/json:
147
- schema: { $ref: '#/components/schemas/ErrorResponse' }
148
- TooManyRequests:
149
- description: Rate limit exceeded
150
- headers:
151
- Retry-After: { schema: { type: integer } }
152
- content:
153
- application/json:
154
- schema: { $ref: '#/components/schemas/ErrorResponse' }
155
-
156
- # ── Paths ──────────────────────────────────────────────────────────────────
157
- paths:
158
- /items:
159
- get:
160
- operationId: listItems
161
- summary: List items belonging to the authenticated user
162
- tags: [Items]
163
- parameters:
164
- - $ref: '#/components/parameters/PageParam'
165
- - $ref: '#/components/parameters/PageSizeParam'
166
- - $ref: '#/components/parameters/SortParam'
167
- - $ref: '#/components/parameters/OrderParam'
168
- responses:
169
- '200':
170
- description: Paginated list of items
171
- content:
172
- application/json:
173
- schema:
174
- allOf:
175
- - $ref: '#/components/schemas/PaginatedResponse'
176
- - properties:
177
- data:
178
- type: array
179
- items: { $ref: '#/components/schemas/Item' }
180
- '401': { $ref: '#/components/responses/Unauthorized' }
181
- '429': { $ref: '#/components/responses/TooManyRequests' }
182
-
183
- post:
184
- operationId: createItem
185
- summary: Create a new item
186
- tags: [Items]
187
- requestBody:
188
- required: true
189
- content:
190
- application/json:
191
- schema: { $ref: '#/components/schemas/CreateItemRequest' }
192
- responses:
193
- '201':
194
- description: Item created
195
- headers:
196
- Location: { schema: { type: string }, description: URL of the created item }
197
- content:
198
- application/json:
199
- schema:
200
- type: object
201
- properties:
202
- data: { $ref: '#/components/schemas/Item' }
203
- '401': { $ref: '#/components/responses/Unauthorized' }
204
- '422': { $ref: '#/components/responses/UnprocessableEntity' }
205
-
206
- /items/{id}:
207
- parameters:
208
- - $ref: '#/components/parameters/ItemId'
209
-
210
- get:
211
- operationId: getItem
212
- summary: Get a single item by ID
213
- tags: [Items]
214
- responses:
215
- '200':
216
- description: Item found
217
- content:
218
- application/json:
219
- schema:
220
- type: object
221
- properties:
222
- data: { $ref: '#/components/schemas/Item' }
223
- '401': { $ref: '#/components/responses/Unauthorized' }
224
- '403': { $ref: '#/components/responses/Forbidden' }
225
- '404': { $ref: '#/components/responses/NotFound' }
226
-
227
- patch:
228
- operationId: updateItem
229
- summary: Partially update an item (only provided fields are changed)
230
- tags: [Items]
231
- requestBody:
232
- required: true
233
- content:
234
- application/json:
235
- schema: { $ref: '#/components/schemas/UpdateItemRequest' }
236
- responses:
237
- '200':
238
- description: Item updated
239
- content:
240
- application/json:
241
- schema:
242
- type: object
243
- properties:
244
- data: { $ref: '#/components/schemas/Item' }
245
- '401': { $ref: '#/components/responses/Unauthorized' }
246
- '403': { $ref: '#/components/responses/Forbidden' }
247
- '404': { $ref: '#/components/responses/NotFound' }
248
- '422': { $ref: '#/components/responses/UnprocessableEntity' }
249
-
250
- delete:
251
- operationId: deleteItem
252
- summary: Delete an item
253
- tags: [Items]
254
- responses:
255
- '204':
256
- description: Item deleted — no content returned
257
- '401': { $ref: '#/components/responses/Unauthorized' }
258
- '403': { $ref: '#/components/responses/Forbidden' }
259
- '404': { $ref: '#/components/responses/NotFound' }
260
-
261
- /health:
262
- get:
263
- operationId: getHealth
264
- summary: Health check — no authentication required
265
- tags: [System]
266
- security: []
267
- responses:
268
- '200':
269
- description: Service is healthy
270
- content:
271
- application/json:
272
- schema:
273
- type: object
274
- properties:
275
- status: { type: string, example: ok }
276
- uptime: { type: number, example: 12345.6 }
277
- `.trim();
278
-
279
- export function run(input: SkillInput): SkillOutput {
280
- return {
281
- skill: 'api-design',
282
- template: TEMPLATE,
283
- checklist: [
284
- 'Choose a consistent resource naming convention: plural nouns, kebab-case (e.g., /user-profiles)',
285
- 'Version the API in the URL path: /v1/resource (not Accept header for most APIs)',
286
- 'Use correct HTTP methods: GET (read), POST (create), PUT (replace), PATCH (partial update), DELETE (remove)',
287
- 'Use correct HTTP status codes: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests, 500 Internal Server Error',
288
- 'Always return a consistent error envelope: { error: { code, message, details? } }',
289
- 'Wrap list responses in { data: [], meta: { total, page, pageSize, totalPages } }',
290
- 'Add Location header on 201 Created pointing to the new resource URL',
291
- 'Implement cursor-based pagination for large, frequently-updated datasets',
292
- 'Support filtering via query parameters: ?status=active&userId=xxx',
293
- 'Support sorting via ?sort=createdAt&order=desc',
294
- 'Set authentication via Authorization: Bearer <token> header (not query param)',
295
- 'Define all schemas in OpenAPI components/schemas and $ref them — no inline duplication',
296
- 'Document every response code, including error responses, in the OpenAPI spec',
297
- 'Apply rate limiting headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset',
298
- 'Set security headers: CORS, Content-Type: application/json, X-Content-Type-Options: nosniff',
299
- 'Use idempotency keys for POST endpoints that create resources (Idempotency-Key header)',
300
- 'Do not nest resources more than 2 levels deep: /users/{id}/items, not /users/{id}/orders/{id}/items/{id}/tags',
301
- 'Return 204 with no body for successful DELETE, not 200 with empty object',
302
- 'Use ISO 8601 for all date/time fields: "2024-01-15T10:30:00Z"',
303
- 'Generate an SDK or client library from the OpenAPI spec using openapi-generator',
304
- ],
305
- patterns: [
306
- 'Resource-oriented design: endpoints represent nouns, HTTP methods are the verbs',
307
- 'Envelope pattern: { data: T, meta: M } for lists; { data: T } for single resources',
308
- 'Error code + message pattern: machine-readable code + human-readable message',
309
- 'Consistent pagination: cursor-based for feeds, offset/page for admin views',
310
- 'OpenAPI-first design: write the spec before implementing, validate implementation against it',
311
- ],
312
- gotchas: [
313
- 'Using GET with a request body for search — use POST /resource/search or query params instead',
314
- 'Returning 200 with { success: false } — use the correct status code, not always 200',
315
- 'Inconsistent pluralisation: /user vs /users — pick one convention and apply everywhere',
316
- 'Exposing internal IDs (auto-increment integers) — use UUIDs to prevent IDOR and enumeration',
317
- 'Not documenting rate limits in the spec — clients cannot implement retry logic without this',
318
- 'CORS misconfiguration: wildcard origin with credentials:true is invalid and silently ignored by browsers',
319
- 'Changing URL structure or removing fields without a deprecation period — breaks existing clients',
320
- ],
321
- resources: [
322
- 'https://spec.openapis.org/oas/v3.0.3',
323
- 'https://cloud.google.com/apis/design',
324
- 'https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html',
325
- 'https://restfulapi.net/',
326
- 'https://www.rfc-editor.org/rfc/rfc9457 (Problem Details for HTTP APIs)',
327
- ],
328
- };
329
- }
@@ -1,271 +0,0 @@
1
- // Skill: auth — JWT + bcrypt + refresh token implementation guide
2
-
3
- export interface SkillInput {
4
- task: string;
5
- context?: string;
6
- options?: Record<string, unknown>;
7
- }
8
-
9
- export interface SkillOutput {
10
- skill: string;
11
- template?: string;
12
- checklist: string[];
13
- patterns: string[];
14
- gotchas: string[];
15
- resources: string[];
16
- }
17
-
18
- const TEMPLATE = `
19
- // ── Types (src/models/auth.ts) ────────────────────────────────────────────
20
- export interface User {
21
- id: string;
22
- email: string;
23
- passwordHash: string;
24
- role: 'admin' | 'user';
25
- failedLoginAttempts: number;
26
- lockedUntil?: Date | null;
27
- createdAt: Date;
28
- }
29
-
30
- export interface TokenPair {
31
- accessToken: string; // short-lived, stateless JWT
32
- refreshToken: string; // long-lived, stored in DB
33
- }
34
-
35
- // ── Token utilities (src/utils/tokens.ts) ────────────────────────────────
36
- import jwt from 'jsonwebtoken';
37
- import crypto from 'crypto';
38
-
39
- const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!; // 256-bit random
40
- const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!; // separate secret
41
- const ACCESS_TTL = '15m';
42
- const REFRESH_TTL = '7d';
43
-
44
- export function signAccessToken(userId: string, role: string): string {
45
- return jwt.sign({ sub: userId, role }, ACCESS_SECRET, {
46
- expiresIn: ACCESS_TTL,
47
- algorithm: 'HS256',
48
- });
49
- }
50
-
51
- export function verifyAccessToken(token: string): { sub: string; role: string } {
52
- return jwt.verify(token, ACCESS_SECRET, { algorithms: ['HS256'] }) as { sub: string; role: string };
53
- }
54
-
55
- export function generateRefreshToken(): string {
56
- return crypto.randomBytes(64).toString('hex'); // 512 bits of entropy
57
- }
58
-
59
- // ── Auth service (src/services/auth.service.ts) ───────────────────────────
60
- import bcrypt from 'bcrypt';
61
-
62
- const BCRYPT_ROUNDS = 12;
63
- const MAX_ATTEMPTS = 5;
64
- const LOCKOUT_MINUTES = 30;
65
-
66
- export class AuthService {
67
- constructor(
68
- private readonly userRepo: UserRepository,
69
- private readonly tokenRepo: RefreshTokenRepository,
70
- ) {}
71
-
72
- async register(email: string, password: string): Promise<User> {
73
- const existing = await this.userRepo.findByEmail(email);
74
- if (existing) throw new ConflictError('Email already registered');
75
- const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS);
76
- return this.userRepo.create({ email: email.toLowerCase().trim(), passwordHash });
77
- }
78
-
79
- async login(email: string, password: string): Promise<TokenPair> {
80
- const user = await this.userRepo.findByEmail(email.toLowerCase().trim());
81
- if (!user) throw new UnauthorizedError('Invalid credentials');
82
-
83
- if (user.lockedUntil && user.lockedUntil > new Date()) {
84
- throw new UnauthorizedError('Account locked. Check your email to unlock.');
85
- }
86
-
87
- const valid = await bcrypt.compare(password, user.passwordHash);
88
- if (!valid) {
89
- await this.recordFailedAttempt(user);
90
- throw new UnauthorizedError('Invalid credentials');
91
- }
92
-
93
- await this.userRepo.resetFailedAttempts(user.id);
94
- return this.issueTokenPair(user.id, user.role);
95
- }
96
-
97
- async refresh(rawRefreshToken: string): Promise<TokenPair> {
98
- const tokenHash = hashToken(rawRefreshToken);
99
- const stored = await this.tokenRepo.findByHash(tokenHash);
100
- if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
101
- throw new UnauthorizedError('Invalid or expired refresh token');
102
- }
103
- // Rotate: revoke old, issue new
104
- await this.tokenRepo.revoke(stored.id);
105
- return this.issueTokenPair(stored.userId, stored.role);
106
- }
107
-
108
- async logout(rawRefreshToken: string): Promise<void> {
109
- const tokenHash = hashToken(rawRefreshToken);
110
- const stored = await this.tokenRepo.findByHash(tokenHash);
111
- if (stored) await this.tokenRepo.revoke(stored.id);
112
- }
113
-
114
- private async issueTokenPair(userId: string, role: string): Promise<TokenPair> {
115
- const rawRefreshToken = generateRefreshToken();
116
- await this.tokenRepo.create({
117
- userId,
118
- tokenHash: hashToken(rawRefreshToken),
119
- role,
120
- expiresAt: addDays(new Date(), 7),
121
- });
122
- return {
123
- accessToken: signAccessToken(userId, role),
124
- refreshToken: rawRefreshToken,
125
- };
126
- }
127
-
128
- private async recordFailedAttempt(user: User): Promise<void> {
129
- const attempts = user.failedLoginAttempts + 1;
130
- if (attempts >= MAX_ATTEMPTS) {
131
- const lockedUntil = addMinutes(new Date(), LOCKOUT_MINUTES);
132
- await this.userRepo.lockAccount(user.id, lockedUntil);
133
- // TODO: send unlock email
134
- } else {
135
- await this.userRepo.incrementFailedAttempts(user.id);
136
- }
137
- }
138
- }
139
-
140
- // ── Auth middleware (src/middleware/auth.ts) ───────────────────────────────
141
- import { Request, Response, NextFunction } from 'express';
142
-
143
- export function requireAuth(req: Request, res: Response, next: NextFunction): void {
144
- const header = req.headers.authorization;
145
- if (!header?.startsWith('Bearer ')) {
146
- res.status(401).json({ error: 'Missing or malformed Authorization header' });
147
- return;
148
- }
149
- try {
150
- const payload = verifyAccessToken(header.slice(7));
151
- req.user = { id: payload.sub, role: payload.role };
152
- next();
153
- } catch {
154
- res.status(401).json({ error: 'Invalid or expired access token' });
155
- }
156
- }
157
-
158
- export function requireRole(role: string) {
159
- return (req: Request, res: Response, next: NextFunction): void => {
160
- if (req.user?.role !== role) {
161
- res.status(403).json({ error: 'Insufficient permissions' });
162
- return;
163
- }
164
- next();
165
- };
166
- }
167
-
168
- // ── Auth routes (src/routes/auth.ts) ─────────────────────────────────────
169
- import { Router } from 'express';
170
- import { z } from 'zod';
171
- import rateLimit from 'express-rate-limit';
172
-
173
- const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, standardHeaders: true });
174
-
175
- const RegisterSchema = z.object({
176
- email: z.string().email().max(255),
177
- password: z.string().min(12).max(128),
178
- });
179
-
180
- const LoginSchema = RegisterSchema;
181
-
182
- export function createAuthRouter(service: AuthService): Router {
183
- const router = Router();
184
- router.use(authLimiter);
185
-
186
- router.post('/register', validate(RegisterSchema), async (req, res) => {
187
- const user = await service.register(req.body.email, req.body.password);
188
- res.status(201).json({ data: { id: user.id, email: user.email } });
189
- });
190
-
191
- router.post('/login', validate(LoginSchema), async (req, res) => {
192
- const tokens = await service.login(req.body.email, req.body.password);
193
- res
194
- .cookie('refreshToken', tokens.refreshToken, {
195
- httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000,
196
- })
197
- .json({ data: { accessToken: tokens.accessToken } });
198
- });
199
-
200
- router.post('/refresh', async (req, res) => {
201
- const raw = req.cookies?.refreshToken;
202
- if (!raw) { res.status(401).json({ error: 'No refresh token' }); return; }
203
- const tokens = await service.refresh(raw);
204
- res
205
- .cookie('refreshToken', tokens.refreshToken, {
206
- httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000,
207
- })
208
- .json({ data: { accessToken: tokens.accessToken } });
209
- });
210
-
211
- router.post('/logout', async (req, res) => {
212
- const raw = req.cookies?.refreshToken;
213
- if (raw) await service.logout(raw);
214
- res.clearCookie('refreshToken').status(204).send();
215
- });
216
-
217
- return router;
218
- }
219
- `.trim();
220
-
221
- export function run(input: SkillInput): SkillOutput {
222
- return {
223
- skill: 'auth',
224
- template: TEMPLATE,
225
- checklist: [
226
- 'Install: npm install bcrypt jsonwebtoken cookie-parser express-rate-limit && npm install -D @types/bcrypt @types/jsonwebtoken @types/cookie-parser',
227
- 'Generate two separate 256-bit secrets: JWT_ACCESS_SECRET and JWT_REFRESH_SECRET via crypto.randomBytes(32).toString("hex")',
228
- 'Store both secrets in environment variables; never hardcode them',
229
- 'Implement User model with passwordHash (never password), failedLoginAttempts, lockedUntil fields',
230
- 'Implement RefreshToken model with userId, tokenHash (SHA-256 of raw token), expiresAt, revokedAt',
231
- 'Set bcrypt cost factor to 12; adjust up if server can handle the latency',
232
- 'Set access token TTL to 15 minutes; set refresh token TTL to 7 days',
233
- 'Always call jwt.verify with explicit { algorithms: ["HS256"] } — never allow alg negotiation',
234
- 'Implement refresh token rotation: revoke old token, issue new token on every /refresh call',
235
- 'Store the SHA-256 hash of the refresh token in DB, never the raw value',
236
- 'Deliver access token in response body; deliver refresh token in httpOnly Secure SameSite=Strict cookie',
237
- 'Apply express-rate-limit (10 req/15 min) to all auth endpoints',
238
- 'Implement account lockout: after 5 failures, lock for 30 minutes and send unlock email',
239
- 'Normalise email input: .toLowerCase().trim() before lookup or storage',
240
- 'Return identical error messages for invalid credentials regardless of whether user exists (prevents enumeration)',
241
- 'Implement /logout that revokes refresh token from DB and clears cookie',
242
- 'Implement password reset: generate a single-use HMAC token, send via email, expire in 1 hour',
243
- 'Log every auth event (login success/fail, logout, token refresh, lockout) with userId, IP, user-agent',
244
- 'Write unit tests for AuthService mocking repositories',
245
- 'Write integration tests: register → login → refresh → logout → verify refresh fails after logout',
246
- ],
247
- patterns: [
248
- 'Stateless access token + stateful refresh token hybrid',
249
- 'Refresh token rotation with database revocation',
250
- 'httpOnly cookie for refresh token (XSS-safe delivery)',
251
- 'Hash-before-store for refresh tokens (never store raw token)',
252
- 'Rate limiter middleware applied at router level, not per-route',
253
- ],
254
- gotchas: [
255
- 'Not rotating refresh tokens on use — a stolen refresh token can be used indefinitely',
256
- 'Accepting jwt.verify without specifying algorithms — "alg":"none" bypass attack',
257
- 'Storing refresh tokens in localStorage — immediately exposed to any XSS vulnerability',
258
- 'Using a short bcrypt salt rounds (< 10) — trivial brute force offline after DB leak',
259
- 'Reusing the same secret for access and refresh tokens — compromise of one breaks both',
260
- 'Not invalidating access tokens on logout — they remain valid until expiry (15 min window)',
261
- 'Not sending identical error messages for bad username vs bad password — enables enumeration',
262
- ],
263
- resources: [
264
- 'https://www.npmjs.com/package/jsonwebtoken',
265
- 'https://www.npmjs.com/package/bcrypt',
266
- 'https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html',
267
- 'https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html',
268
- 'https://datatracker.ietf.org/doc/html/rfc6749 (OAuth 2.0)',
269
- ],
270
- };
271
- }
File without changes