@jigyasudham/veto 0.8.2 → 1.0.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 (111) hide show
  1. package/README.md +209 -52
  2. package/dist/agents/executor.js +36 -3
  3. package/dist/cli.js +350 -51
  4. package/dist/context/reader.js +113 -0
  5. package/dist/council/index.js +3 -1
  6. package/dist/plugins/loader.js +49 -0
  7. package/dist/router/index.js +2 -2
  8. package/dist/router/learning-updater.js +45 -1
  9. package/dist/server.js +478 -14
  10. package/dist/watcher/index.js +77 -0
  11. package/dist/workflow/pipeline.js +64 -0
  12. package/package.json +12 -3
  13. package/.claude/settings.local.json +0 -9
  14. package/src/adapters/claude.ts +0 -70
  15. package/src/adapters/codex.ts +0 -71
  16. package/src/adapters/gemini.ts +0 -71
  17. package/src/adapters/index.ts +0 -217
  18. package/src/agents/development/api.ts +0 -120
  19. package/src/agents/development/backend.ts +0 -85
  20. package/src/agents/development/coder.ts +0 -213
  21. package/src/agents/development/database.ts +0 -83
  22. package/src/agents/development/debugger.ts +0 -238
  23. package/src/agents/development/devops.ts +0 -86
  24. package/src/agents/development/frontend.ts +0 -85
  25. package/src/agents/development/migration.ts +0 -144
  26. package/src/agents/development/performance.ts +0 -144
  27. package/src/agents/development/refactor.ts +0 -86
  28. package/src/agents/development/reviewer.ts +0 -268
  29. package/src/agents/development/tester.ts +0 -151
  30. package/src/agents/executor.ts +0 -158
  31. package/src/agents/memory/context-manager.ts +0 -171
  32. package/src/agents/memory/decision-logger.ts +0 -160
  33. package/src/agents/memory/knowledge-base.ts +0 -124
  34. package/src/agents/memory/pattern-learner.ts +0 -143
  35. package/src/agents/memory/project-mapper.ts +0 -118
  36. package/src/agents/quality/accessibility.ts +0 -99
  37. package/src/agents/quality/code-quality.ts +0 -115
  38. package/src/agents/quality/compatibility.ts +0 -58
  39. package/src/agents/quality/documentation.ts +0 -105
  40. package/src/agents/quality/error-handling.ts +0 -96
  41. package/src/agents/research/competitor-analyzer.ts +0 -45
  42. package/src/agents/research/cost-analyzer.ts +0 -54
  43. package/src/agents/research/estimator.ts +0 -60
  44. package/src/agents/research/ethics-bias.ts +0 -113
  45. package/src/agents/research/researcher.ts +0 -114
  46. package/src/agents/research/risk-assessor.ts +0 -63
  47. package/src/agents/research/tech-advisor.ts +0 -55
  48. package/src/agents/security/auth.ts +0 -287
  49. package/src/agents/security/dependency-audit.ts +0 -337
  50. package/src/agents/security/penetration.ts +0 -262
  51. package/src/agents/security/privacy.ts +0 -285
  52. package/src/agents/security/scanner.ts +0 -322
  53. package/src/agents/security/secrets.ts +0 -249
  54. package/src/agents/types.ts +0 -66
  55. package/src/agents/workflow/automation.ts +0 -59
  56. package/src/agents/workflow/file-manager.ts +0 -52
  57. package/src/agents/workflow/git-agent.ts +0 -55
  58. package/src/agents/workflow/reporter.ts +0 -51
  59. package/src/agents/workflow/search-agent.ts +0 -40
  60. package/src/agents/workflow/task-coordinator.ts +0 -41
  61. package/src/agents/workflow/task-planner.ts +0 -47
  62. package/src/cli.ts +0 -135
  63. package/src/council/decision-engine.ts +0 -171
  64. package/src/council/devil-advocate.ts +0 -116
  65. package/src/council/index.ts +0 -44
  66. package/src/council/lead-developer.ts +0 -118
  67. package/src/council/legal-compliance.ts +0 -152
  68. package/src/council/product-manager.ts +0 -102
  69. package/src/council/security.ts +0 -172
  70. package/src/council/system-architect.ts +0 -132
  71. package/src/council/types.ts +0 -33
  72. package/src/council/ux-designer.ts +0 -121
  73. package/src/memory/local.ts +0 -305
  74. package/src/memory/schema.ts +0 -174
  75. package/src/memory/sync.ts +0 -274
  76. package/src/router/complexity-scorer.ts +0 -96
  77. package/src/router/context-compressor.ts +0 -74
  78. package/src/router/index.ts +0 -60
  79. package/src/router/learning-updater.ts +0 -271
  80. package/src/router/model-selector.ts +0 -83
  81. package/src/router/rate-monitor.ts +0 -103
  82. package/src/server.ts +0 -1038
  83. package/src/skills/development/skill-api-design.ts +0 -329
  84. package/src/skills/development/skill-auth.ts +0 -271
  85. package/src/skills/development/skill-ci-cd.ts +0 -0
  86. package/src/skills/development/skill-crud.ts +0 -209
  87. package/src/skills/development/skill-db-schema.ts +0 -0
  88. package/src/skills/development/skill-docker.ts +0 -0
  89. package/src/skills/development/skill-env-setup.ts +0 -0
  90. package/src/skills/development/skill-scaffold.ts +0 -323
  91. package/src/skills/intelligence/skill-complexity-score.ts +0 -69
  92. package/src/skills/intelligence/skill-cost-track.ts +0 -39
  93. package/src/skills/intelligence/skill-learning-loop.ts +0 -69
  94. package/src/skills/intelligence/skill-pattern-detect.ts +0 -38
  95. package/src/skills/intelligence/skill-rate-watch.ts +0 -61
  96. package/src/skills/memory/skill-context-compress.ts +0 -98
  97. package/src/skills/memory/skill-cross-sync.ts +0 -104
  98. package/src/skills/memory/skill-decision-log.ts +0 -119
  99. package/src/skills/memory/skill-session-restore.ts +0 -59
  100. package/src/skills/memory/skill-session-save.ts +0 -94
  101. package/src/skills/quality/skill-accessibility.ts +0 -0
  102. package/src/skills/quality/skill-code-review.ts +0 -84
  103. package/src/skills/quality/skill-docs-gen.ts +0 -0
  104. package/src/skills/quality/skill-perf-audit.ts +0 -0
  105. package/src/skills/quality/skill-security-scan.ts +0 -91
  106. package/src/skills/quality/skill-test-suite.ts +0 -290
  107. package/src/skills/workflow/skill-deploy.ts +0 -0
  108. package/src/skills/workflow/skill-git-workflow.ts +0 -0
  109. package/src/skills/workflow/skill-rollback.ts +0 -0
  110. package/src/skills/workflow/skill-task-breakdown.ts +0 -0
  111. 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