@ranimontagna/agent-toolkit 0.1.4 → 0.1.5
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/README.md +282 -277
- package/docs/assets/install-plan.svg +29 -0
- package/docs/assets/install-skill-packages.svg +31 -0
- package/docs/assets/install-status.svg +32 -0
- package/package.json +10 -9
- package/setup-agent-toolkit.sh +1 -1
- package/skills/backend/fastify-best-practices/LICENSE +21 -0
- package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
- package/skills/backend/fastify-best-practices/SKILL.md +75 -0
- package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
- package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
- package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
- package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
- package/skills/backend/fastify-best-practices/rules/database.md +320 -0
- package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
- package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
- package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
- package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
- package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
- package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
- package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
- package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
- package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
- package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
- package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
- package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
- package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
- package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
- package/skills/backend/fastify-best-practices/tile.json +11 -0
- package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cors-security
|
|
3
|
+
description: CORS and security headers in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: cors, security, headers, helmet, csrf
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CORS and Security
|
|
9
|
+
|
|
10
|
+
## CORS with @fastify/cors
|
|
11
|
+
|
|
12
|
+
Enable Cross-Origin Resource Sharing:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import cors from '@fastify/cors';
|
|
17
|
+
|
|
18
|
+
const app = Fastify();
|
|
19
|
+
|
|
20
|
+
// Simple CORS - allow all origins
|
|
21
|
+
app.register(cors);
|
|
22
|
+
|
|
23
|
+
// Configured CORS
|
|
24
|
+
app.register(cors, {
|
|
25
|
+
origin: ['https://example.com', 'https://app.example.com'],
|
|
26
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
27
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
28
|
+
exposedHeaders: ['X-Total-Count'],
|
|
29
|
+
credentials: true,
|
|
30
|
+
maxAge: 86400, // 24 hours
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Dynamic CORS Origin
|
|
35
|
+
|
|
36
|
+
Validate origins dynamically:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
app.register(cors, {
|
|
40
|
+
origin: (origin, callback) => {
|
|
41
|
+
// Allow requests with no origin (mobile apps, curl, etc.)
|
|
42
|
+
if (!origin) {
|
|
43
|
+
return callback(null, true);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check against allowed origins
|
|
47
|
+
const allowedOrigins = [
|
|
48
|
+
'https://example.com',
|
|
49
|
+
'https://app.example.com',
|
|
50
|
+
/\.example\.com$/,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const isAllowed = allowedOrigins.some((allowed) => {
|
|
54
|
+
if (allowed instanceof RegExp) {
|
|
55
|
+
return allowed.test(origin);
|
|
56
|
+
}
|
|
57
|
+
return allowed === origin;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (isAllowed) {
|
|
61
|
+
callback(null, true);
|
|
62
|
+
} else {
|
|
63
|
+
callback(new Error('Not allowed by CORS'), false);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
credentials: true,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Per-Route CORS
|
|
71
|
+
|
|
72
|
+
Configure CORS for specific routes:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
app.register(cors, {
|
|
76
|
+
origin: true, // Reflect request origin
|
|
77
|
+
credentials: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Or disable CORS for specific routes
|
|
81
|
+
app.route({
|
|
82
|
+
method: 'GET',
|
|
83
|
+
url: '/internal',
|
|
84
|
+
config: {
|
|
85
|
+
cors: false,
|
|
86
|
+
},
|
|
87
|
+
handler: async () => {
|
|
88
|
+
return { internal: true };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Security Headers with @fastify/helmet
|
|
94
|
+
|
|
95
|
+
Add security headers:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import helmet from '@fastify/helmet';
|
|
99
|
+
|
|
100
|
+
app.register(helmet, {
|
|
101
|
+
contentSecurityPolicy: {
|
|
102
|
+
directives: {
|
|
103
|
+
defaultSrc: ["'self'"],
|
|
104
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
105
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
106
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
107
|
+
connectSrc: ["'self'", 'https://api.example.com'],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
crossOriginEmbedderPolicy: false, // Disable if embedding external resources
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Configure Individual Headers
|
|
115
|
+
|
|
116
|
+
Fine-tune security headers:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
app.register(helmet, {
|
|
120
|
+
// Strict Transport Security
|
|
121
|
+
hsts: {
|
|
122
|
+
maxAge: 31536000, // 1 year
|
|
123
|
+
includeSubDomains: true,
|
|
124
|
+
preload: true,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Content Security Policy
|
|
128
|
+
contentSecurityPolicy: {
|
|
129
|
+
useDefaults: true,
|
|
130
|
+
directives: {
|
|
131
|
+
'script-src': ["'self'", 'https://trusted-cdn.com'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// X-Frame-Options
|
|
136
|
+
frameguard: {
|
|
137
|
+
action: 'deny', // or 'sameorigin'
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// X-Content-Type-Options
|
|
141
|
+
noSniff: true,
|
|
142
|
+
|
|
143
|
+
// X-XSS-Protection (legacy)
|
|
144
|
+
xssFilter: true,
|
|
145
|
+
|
|
146
|
+
// Referrer-Policy
|
|
147
|
+
referrerPolicy: {
|
|
148
|
+
policy: 'strict-origin-when-cross-origin',
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// X-Permitted-Cross-Domain-Policies
|
|
152
|
+
permittedCrossDomainPolicies: false,
|
|
153
|
+
|
|
154
|
+
// X-DNS-Prefetch-Control
|
|
155
|
+
dnsPrefetchControl: {
|
|
156
|
+
allow: false,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Rate Limiting
|
|
162
|
+
|
|
163
|
+
Protect against abuse:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import rateLimit from '@fastify/rate-limit';
|
|
167
|
+
|
|
168
|
+
app.register(rateLimit, {
|
|
169
|
+
max: 100,
|
|
170
|
+
timeWindow: '1 minute',
|
|
171
|
+
errorResponseBuilder: (request, context) => ({
|
|
172
|
+
statusCode: 429,
|
|
173
|
+
error: 'Too Many Requests',
|
|
174
|
+
message: `Rate limit exceeded. Retry in ${context.after}`,
|
|
175
|
+
retryAfter: context.after,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Per-route rate limit
|
|
180
|
+
app.get('/expensive', {
|
|
181
|
+
config: {
|
|
182
|
+
rateLimit: {
|
|
183
|
+
max: 10,
|
|
184
|
+
timeWindow: '1 minute',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}, handler);
|
|
188
|
+
|
|
189
|
+
// Skip rate limit for certain routes
|
|
190
|
+
app.get('/health', {
|
|
191
|
+
config: {
|
|
192
|
+
rateLimit: false,
|
|
193
|
+
},
|
|
194
|
+
}, () => ({ status: 'ok' }));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Redis-Based Rate Limiting
|
|
198
|
+
|
|
199
|
+
Use Redis for distributed rate limiting:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import rateLimit from '@fastify/rate-limit';
|
|
203
|
+
import Redis from 'ioredis';
|
|
204
|
+
|
|
205
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
206
|
+
|
|
207
|
+
app.register(rateLimit, {
|
|
208
|
+
max: 100,
|
|
209
|
+
timeWindow: '1 minute',
|
|
210
|
+
redis,
|
|
211
|
+
nameSpace: 'rate-limit:',
|
|
212
|
+
keyGenerator: (request) => {
|
|
213
|
+
// Rate limit by user ID if authenticated, otherwise by IP
|
|
214
|
+
return request.user?.id || request.ip;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## CSRF Protection
|
|
220
|
+
|
|
221
|
+
Protect against Cross-Site Request Forgery:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import fastifyCsrf from '@fastify/csrf-protection';
|
|
225
|
+
import fastifyCookie from '@fastify/cookie';
|
|
226
|
+
|
|
227
|
+
app.register(fastifyCookie);
|
|
228
|
+
app.register(fastifyCsrf, {
|
|
229
|
+
cookieOpts: {
|
|
230
|
+
signed: true,
|
|
231
|
+
httpOnly: true,
|
|
232
|
+
sameSite: 'strict',
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Generate token
|
|
237
|
+
app.get('/csrf-token', async (request, reply) => {
|
|
238
|
+
const token = reply.generateCsrf();
|
|
239
|
+
return { token };
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Protected route
|
|
243
|
+
app.post('/transfer', {
|
|
244
|
+
preHandler: app.csrfProtection,
|
|
245
|
+
}, async (request) => {
|
|
246
|
+
// CSRF token validated
|
|
247
|
+
return { success: true };
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Custom Security Headers
|
|
252
|
+
|
|
253
|
+
Add custom headers:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
app.addHook('onSend', async (request, reply) => {
|
|
257
|
+
// Custom security headers
|
|
258
|
+
reply.header('X-Request-ID', request.id);
|
|
259
|
+
reply.header('X-Content-Type-Options', 'nosniff');
|
|
260
|
+
reply.header('X-Frame-Options', 'DENY');
|
|
261
|
+
reply.header('Permissions-Policy', 'geolocation=(), camera=()');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Per-route headers
|
|
265
|
+
app.get('/download', async (request, reply) => {
|
|
266
|
+
reply.header('Content-Disposition', 'attachment; filename="file.pdf"');
|
|
267
|
+
reply.header('X-Download-Options', 'noopen');
|
|
268
|
+
return reply.send(fileStream);
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Secure Cookies
|
|
273
|
+
|
|
274
|
+
Configure secure cookies:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import cookie from '@fastify/cookie';
|
|
278
|
+
|
|
279
|
+
app.register(cookie, {
|
|
280
|
+
secret: process.env.COOKIE_SECRET,
|
|
281
|
+
parseOptions: {
|
|
282
|
+
httpOnly: true,
|
|
283
|
+
secure: process.env.NODE_ENV === 'production',
|
|
284
|
+
sameSite: 'strict',
|
|
285
|
+
path: '/',
|
|
286
|
+
maxAge: 3600, // 1 hour
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Set secure cookie
|
|
291
|
+
app.post('/login', async (request, reply) => {
|
|
292
|
+
const token = await createSession(request.body);
|
|
293
|
+
|
|
294
|
+
reply.setCookie('session', token, {
|
|
295
|
+
httpOnly: true,
|
|
296
|
+
secure: true,
|
|
297
|
+
sameSite: 'strict',
|
|
298
|
+
path: '/',
|
|
299
|
+
maxAge: 86400,
|
|
300
|
+
signed: true,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return { success: true };
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Read signed cookie
|
|
307
|
+
app.get('/profile', async (request) => {
|
|
308
|
+
const session = request.cookies.session;
|
|
309
|
+
const unsigned = request.unsignCookie(session);
|
|
310
|
+
|
|
311
|
+
if (!unsigned.valid) {
|
|
312
|
+
throw { statusCode: 401, message: 'Invalid session' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { sessionId: unsigned.value };
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Request Validation Security
|
|
320
|
+
|
|
321
|
+
Validate and sanitize input:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// Schema-based validation protects against injection
|
|
325
|
+
app.post('/users', {
|
|
326
|
+
schema: {
|
|
327
|
+
body: {
|
|
328
|
+
type: 'object',
|
|
329
|
+
properties: {
|
|
330
|
+
email: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
format: 'email',
|
|
333
|
+
maxLength: 254,
|
|
334
|
+
},
|
|
335
|
+
name: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
minLength: 1,
|
|
338
|
+
maxLength: 100,
|
|
339
|
+
pattern: '^[a-zA-Z\\s]+$', // Only letters and spaces
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
required: ['email', 'name'],
|
|
343
|
+
additionalProperties: false,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
}, handler);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## IP Filtering
|
|
350
|
+
|
|
351
|
+
Restrict access by IP:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
const allowedIps = new Set([
|
|
355
|
+
'192.168.1.0/24',
|
|
356
|
+
'10.0.0.0/8',
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
360
|
+
if (request.url.startsWith('/admin')) {
|
|
361
|
+
const clientIp = request.ip;
|
|
362
|
+
|
|
363
|
+
if (!isIpAllowed(clientIp, allowedIps)) {
|
|
364
|
+
reply.code(403).send({ error: 'Forbidden' });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
function isIpAllowed(ip: string, allowed: Set<string>): boolean {
|
|
370
|
+
// Implement IP/CIDR matching
|
|
371
|
+
for (const range of allowed) {
|
|
372
|
+
if (ipInRange(ip, range)) return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Trust Proxy
|
|
379
|
+
|
|
380
|
+
Configure for reverse proxy environments:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
const app = Fastify({
|
|
384
|
+
trustProxy: true, // Trust X-Forwarded-* headers
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Or specific proxy configuration
|
|
388
|
+
const app = Fastify({
|
|
389
|
+
trustProxy: ['127.0.0.1', '10.0.0.0/8'],
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Now request.ip returns the real client IP
|
|
393
|
+
app.get('/ip', async (request) => {
|
|
394
|
+
return {
|
|
395
|
+
ip: request.ip,
|
|
396
|
+
ips: request.ips, // Array of all IPs in chain
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## HTTPS Redirect
|
|
402
|
+
|
|
403
|
+
Force HTTPS in production:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
407
|
+
if (
|
|
408
|
+
process.env.NODE_ENV === 'production' &&
|
|
409
|
+
request.headers['x-forwarded-proto'] !== 'https'
|
|
410
|
+
) {
|
|
411
|
+
const httpsUrl = `https://${request.hostname}${request.url}`;
|
|
412
|
+
reply.redirect(301, httpsUrl);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Security Best Practices Summary
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import Fastify from 'fastify';
|
|
421
|
+
import cors from '@fastify/cors';
|
|
422
|
+
import helmet from '@fastify/helmet';
|
|
423
|
+
import rateLimit from '@fastify/rate-limit';
|
|
424
|
+
|
|
425
|
+
const app = Fastify({
|
|
426
|
+
trustProxy: true,
|
|
427
|
+
bodyLimit: 1048576, // 1MB max body
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Security plugins
|
|
431
|
+
app.register(helmet);
|
|
432
|
+
app.register(cors, {
|
|
433
|
+
origin: process.env.ALLOWED_ORIGINS?.split(','),
|
|
434
|
+
credentials: true,
|
|
435
|
+
});
|
|
436
|
+
app.register(rateLimit, {
|
|
437
|
+
max: 100,
|
|
438
|
+
timeWindow: '1 minute',
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Validate all input with schemas
|
|
442
|
+
// Never expose internal errors in production
|
|
443
|
+
// Use parameterized queries for database
|
|
444
|
+
// Keep dependencies updated
|
|
445
|
+
```
|