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