@girardmedia/bootspring 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.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/agents/README.md +93 -0
- package/agents/api-expert/context.md +416 -0
- package/agents/architecture-expert/context.md +454 -0
- package/agents/backend-expert/context.md +483 -0
- package/agents/code-review-expert/context.md +365 -0
- package/agents/database-expert/context.md +250 -0
- package/agents/devops-expert/context.md +446 -0
- package/agents/frontend-expert/context.md +364 -0
- package/agents/index.js +140 -0
- package/agents/performance-expert/context.md +377 -0
- package/agents/security-expert/context.md +343 -0
- package/agents/testing-expert/context.md +414 -0
- package/agents/ui-ux-expert/context.md +448 -0
- package/agents/vercel-expert/context.md +426 -0
- package/bin/bootspring.js +310 -0
- package/cli/agent.js +337 -0
- package/cli/context.js +194 -0
- package/cli/dashboard.js +150 -0
- package/cli/generate.js +294 -0
- package/cli/init.js +410 -0
- package/cli/loop.js +421 -0
- package/cli/mcp.js +241 -0
- package/cli/memory.js +303 -0
- package/cli/orchestrator.js +400 -0
- package/cli/plugin.js +451 -0
- package/cli/quality.js +332 -0
- package/cli/skill.js +369 -0
- package/cli/task.js +628 -0
- package/cli/telemetry.js +114 -0
- package/cli/todo.js +614 -0
- package/cli/update.js +312 -0
- package/core/config.js +245 -0
- package/core/context.js +329 -0
- package/core/entitlements.js +209 -0
- package/core/index.js +43 -0
- package/core/policies.js +68 -0
- package/core/telemetry.js +247 -0
- package/core/utils.js +380 -0
- package/dashboard/server.js +818 -0
- package/docs/integrations/claude-code.md +42 -0
- package/docs/integrations/codex.md +42 -0
- package/docs/mcp-api-platform.md +102 -0
- package/generators/generate.js +598 -0
- package/generators/index.js +18 -0
- package/hooks/context-detector.js +177 -0
- package/hooks/index.js +35 -0
- package/hooks/prompt-enhancer.js +289 -0
- package/intelligence/git-memory.js +551 -0
- package/intelligence/index.js +59 -0
- package/intelligence/orchestrator.js +964 -0
- package/intelligence/prd.js +447 -0
- package/intelligence/recommendation-weights.json +18 -0
- package/intelligence/recommendations.js +234 -0
- package/mcp/capabilities.js +71 -0
- package/mcp/contracts/mcp-contract.v1.json +497 -0
- package/mcp/registry.js +213 -0
- package/mcp/response-formatter.js +462 -0
- package/mcp/server.js +99 -0
- package/mcp/tools/agent-tool.js +137 -0
- package/mcp/tools/capabilities-tool.js +54 -0
- package/mcp/tools/context-tool.js +49 -0
- package/mcp/tools/dashboard-tool.js +58 -0
- package/mcp/tools/generate-tool.js +46 -0
- package/mcp/tools/loop-tool.js +134 -0
- package/mcp/tools/memory-tool.js +180 -0
- package/mcp/tools/orchestrator-tool.js +232 -0
- package/mcp/tools/plugin-tool.js +76 -0
- package/mcp/tools/quality-tool.js +47 -0
- package/mcp/tools/skill-tool.js +233 -0
- package/mcp/tools/telemetry-tool.js +95 -0
- package/mcp/tools/todo-tool.js +133 -0
- package/package.json +98 -0
- package/plugins/index.js +141 -0
- package/quality/index.js +380 -0
- package/quality/lint-budgets.json +19 -0
- package/skills/index.js +787 -0
- package/skills/patterns/README.md +163 -0
- package/skills/patterns/api/route-handler.md +217 -0
- package/skills/patterns/api/server-action.md +249 -0
- package/skills/patterns/auth/clerk.md +132 -0
- package/skills/patterns/database/prisma.md +180 -0
- package/skills/patterns/payments/stripe.md +272 -0
- package/skills/patterns/security/validation.md +268 -0
- package/skills/patterns/testing/vitest.md +307 -0
- package/templates/bootspring.config.js +83 -0
- package/templates/mcp.json +9 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Performance Expert Agent
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
Specialized in web performance optimization, Core Web Vitals, caching strategies, bundle optimization, and ensuring fast user experiences.
|
|
5
|
+
|
|
6
|
+
## Core Expertise
|
|
7
|
+
|
|
8
|
+
### Core Web Vitals
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
LCP (Largest Contentful Paint): < 2.5s
|
|
12
|
+
- Optimize images (WebP, proper sizing)
|
|
13
|
+
- Preload critical resources
|
|
14
|
+
- Server-side rendering
|
|
15
|
+
|
|
16
|
+
FID (First Input Delay): < 100ms
|
|
17
|
+
- Minimize JavaScript execution
|
|
18
|
+
- Break up long tasks
|
|
19
|
+
- Use Web Workers for heavy computation
|
|
20
|
+
|
|
21
|
+
CLS (Cumulative Layout Shift): < 0.1
|
|
22
|
+
- Set explicit dimensions on images/videos
|
|
23
|
+
- Reserve space for dynamic content
|
|
24
|
+
- Avoid inserting content above existing content
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Image Optimization
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Use Next.js Image component
|
|
31
|
+
import Image from 'next/image';
|
|
32
|
+
|
|
33
|
+
// Good: Optimized images
|
|
34
|
+
<Image
|
|
35
|
+
src="/hero.jpg"
|
|
36
|
+
alt="Hero image"
|
|
37
|
+
width={1200}
|
|
38
|
+
height={600}
|
|
39
|
+
priority // For above-the-fold images
|
|
40
|
+
placeholder="blur"
|
|
41
|
+
blurDataURL="data:image/jpeg;base64,..."
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
// Responsive images
|
|
45
|
+
<Image
|
|
46
|
+
src="/product.jpg"
|
|
47
|
+
alt="Product"
|
|
48
|
+
fill
|
|
49
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
50
|
+
className="object-cover"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
// Remote images - configure in next.config.js
|
|
54
|
+
module.exports = {
|
|
55
|
+
images: {
|
|
56
|
+
remotePatterns: [
|
|
57
|
+
{
|
|
58
|
+
protocol: 'https',
|
|
59
|
+
hostname: 'cdn.example.com',
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
formats: ['image/avif', 'image/webp'],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Code Splitting & Lazy Loading
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// Dynamic imports for code splitting
|
|
71
|
+
import dynamic from 'next/dynamic';
|
|
72
|
+
|
|
73
|
+
// Heavy component loaded only when needed
|
|
74
|
+
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
|
|
75
|
+
loading: () => <ChartSkeleton />,
|
|
76
|
+
ssr: false, // Client-only component
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Route-based code splitting (automatic in App Router)
|
|
80
|
+
// Each page.tsx is automatically code-split
|
|
81
|
+
|
|
82
|
+
// Lazy load below-the-fold content
|
|
83
|
+
'use client';
|
|
84
|
+
import { useInView } from 'react-intersection-observer';
|
|
85
|
+
|
|
86
|
+
function LazySection() {
|
|
87
|
+
const { ref, inView } = useInView({
|
|
88
|
+
triggerOnce: true,
|
|
89
|
+
rootMargin: '200px',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div ref={ref}>
|
|
94
|
+
{inView ? <HeavyContent /> : <Placeholder />}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Bundle Analysis & Optimization
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Analyze bundle
|
|
104
|
+
npm install @next/bundle-analyzer
|
|
105
|
+
|
|
106
|
+
# next.config.js
|
|
107
|
+
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
|
108
|
+
enabled: process.env.ANALYZE === 'true',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
module.exports = withBundleAnalyzer({
|
|
112
|
+
// config
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
# Run analysis
|
|
116
|
+
ANALYZE=true npm run build
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Tree-shaking friendly imports
|
|
121
|
+
// Bad: Imports entire library
|
|
122
|
+
import _ from 'lodash';
|
|
123
|
+
|
|
124
|
+
// Good: Import only what's needed
|
|
125
|
+
import debounce from 'lodash/debounce';
|
|
126
|
+
|
|
127
|
+
// Or use lodash-es for better tree-shaking
|
|
128
|
+
import { debounce } from 'lodash-es';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Caching Strategies
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// app/api/posts/route.ts
|
|
135
|
+
import { NextResponse } from 'next/server';
|
|
136
|
+
|
|
137
|
+
export async function GET() {
|
|
138
|
+
const posts = await getPosts();
|
|
139
|
+
|
|
140
|
+
return NextResponse.json(posts, {
|
|
141
|
+
headers: {
|
|
142
|
+
// Cache for 1 hour, revalidate in background
|
|
143
|
+
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Page-level caching with revalidation
|
|
149
|
+
// app/posts/page.tsx
|
|
150
|
+
export const revalidate = 3600; // Revalidate every hour
|
|
151
|
+
|
|
152
|
+
// On-demand revalidation
|
|
153
|
+
// app/api/revalidate/route.ts
|
|
154
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
155
|
+
|
|
156
|
+
export async function POST(request: Request) {
|
|
157
|
+
const { path, tag } = await request.json();
|
|
158
|
+
|
|
159
|
+
if (path) revalidatePath(path);
|
|
160
|
+
if (tag) revalidateTag(tag);
|
|
161
|
+
|
|
162
|
+
return Response.json({ revalidated: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Using tags for granular cache control
|
|
166
|
+
async function getPosts() {
|
|
167
|
+
return fetch('https://api.example.com/posts', {
|
|
168
|
+
next: { tags: ['posts'] },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Database Query Optimization
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// Avoid N+1 queries
|
|
177
|
+
// Bad: N+1 problem
|
|
178
|
+
const users = await prisma.user.findMany();
|
|
179
|
+
for (const user of users) {
|
|
180
|
+
user.posts = await prisma.post.findMany({ where: { authorId: user.id } });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Good: Include related data
|
|
184
|
+
const users = await prisma.user.findMany({
|
|
185
|
+
include: {
|
|
186
|
+
posts: {
|
|
187
|
+
take: 5,
|
|
188
|
+
orderBy: { createdAt: 'desc' },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Use select for partial data
|
|
194
|
+
const users = await prisma.user.findMany({
|
|
195
|
+
select: {
|
|
196
|
+
id: true,
|
|
197
|
+
name: true,
|
|
198
|
+
email: true,
|
|
199
|
+
// Only fields you need
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Pagination with cursor (more efficient than offset)
|
|
204
|
+
const posts = await prisma.post.findMany({
|
|
205
|
+
take: 20,
|
|
206
|
+
cursor: lastPostId ? { id: lastPostId } : undefined,
|
|
207
|
+
skip: lastPostId ? 1 : 0,
|
|
208
|
+
orderBy: { createdAt: 'desc' },
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### React Performance
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// Memo for expensive computations
|
|
216
|
+
'use client';
|
|
217
|
+
import { useMemo, useCallback, memo } from 'react';
|
|
218
|
+
|
|
219
|
+
function ExpensiveList({ items, filter }) {
|
|
220
|
+
// Memoize expensive computation
|
|
221
|
+
const filteredItems = useMemo(() => {
|
|
222
|
+
return items.filter(item =>
|
|
223
|
+
item.name.toLowerCase().includes(filter.toLowerCase())
|
|
224
|
+
);
|
|
225
|
+
}, [items, filter]);
|
|
226
|
+
|
|
227
|
+
// Memoize callback
|
|
228
|
+
const handleItemClick = useCallback((id: string) => {
|
|
229
|
+
console.log('Clicked:', id);
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<ul>
|
|
234
|
+
{filteredItems.map(item => (
|
|
235
|
+
<MemoizedItem
|
|
236
|
+
key={item.id}
|
|
237
|
+
item={item}
|
|
238
|
+
onClick={handleItemClick}
|
|
239
|
+
/>
|
|
240
|
+
))}
|
|
241
|
+
</ul>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Memoize component
|
|
246
|
+
const MemoizedItem = memo(function Item({ item, onClick }) {
|
|
247
|
+
return (
|
|
248
|
+
<li onClick={() => onClick(item.id)}>
|
|
249
|
+
{item.name}
|
|
250
|
+
</li>
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Virtualization for long lists
|
|
255
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
256
|
+
|
|
257
|
+
function VirtualList({ items }) {
|
|
258
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
259
|
+
|
|
260
|
+
const virtualizer = useVirtualizer({
|
|
261
|
+
count: items.length,
|
|
262
|
+
getScrollElement: () => parentRef.current,
|
|
263
|
+
estimateSize: () => 50,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div ref={parentRef} className="h-[400px] overflow-auto">
|
|
268
|
+
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
|
|
269
|
+
{virtualizer.getVirtualItems().map(virtualRow => (
|
|
270
|
+
<div
|
|
271
|
+
key={virtualRow.key}
|
|
272
|
+
style={{
|
|
273
|
+
position: 'absolute',
|
|
274
|
+
top: 0,
|
|
275
|
+
left: 0,
|
|
276
|
+
width: '100%',
|
|
277
|
+
height: `${virtualRow.size}px`,
|
|
278
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
{items[virtualRow.index].name}
|
|
282
|
+
</div>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Preloading & Prefetching
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
// Prefetch on hover
|
|
294
|
+
import Link from 'next/link';
|
|
295
|
+
|
|
296
|
+
<Link href="/dashboard" prefetch={true}>
|
|
297
|
+
Dashboard
|
|
298
|
+
</Link>
|
|
299
|
+
|
|
300
|
+
// Preload critical resources
|
|
301
|
+
// app/layout.tsx
|
|
302
|
+
export default function RootLayout({ children }) {
|
|
303
|
+
return (
|
|
304
|
+
<html>
|
|
305
|
+
<head>
|
|
306
|
+
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossOrigin="" />
|
|
307
|
+
<link rel="preconnect" href="https://api.example.com" />
|
|
308
|
+
<link rel="dns-prefetch" href="https://analytics.example.com" />
|
|
309
|
+
</head>
|
|
310
|
+
<body>{children}</body>
|
|
311
|
+
</html>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Streaming with Suspense
|
|
316
|
+
import { Suspense } from 'react';
|
|
317
|
+
|
|
318
|
+
export default async function Page() {
|
|
319
|
+
return (
|
|
320
|
+
<div>
|
|
321
|
+
<h1>Dashboard</h1>
|
|
322
|
+
{/* Stream this content as it becomes available */}
|
|
323
|
+
<Suspense fallback={<StatsSkeleton />}>
|
|
324
|
+
<Stats />
|
|
325
|
+
</Suspense>
|
|
326
|
+
<Suspense fallback={<ChartSkeleton />}>
|
|
327
|
+
<RevenueChart />
|
|
328
|
+
</Suspense>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Monitoring & Metrics
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Using Web Vitals API
|
|
338
|
+
// app/components/WebVitals.tsx
|
|
339
|
+
'use client';
|
|
340
|
+
|
|
341
|
+
import { useReportWebVitals } from 'next/web-vitals';
|
|
342
|
+
|
|
343
|
+
export function WebVitals() {
|
|
344
|
+
useReportWebVitals((metric) => {
|
|
345
|
+
// Send to analytics
|
|
346
|
+
console.log(metric);
|
|
347
|
+
|
|
348
|
+
// Example: Send to custom endpoint
|
|
349
|
+
fetch('/api/analytics', {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
name: metric.name,
|
|
353
|
+
value: metric.value,
|
|
354
|
+
rating: metric.rating, // 'good', 'needs-improvement', 'poor'
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Performance Checklist
|
|
364
|
+
|
|
365
|
+
- [ ] Images optimized with next/image
|
|
366
|
+
- [ ] Lazy loading for below-fold content
|
|
367
|
+
- [ ] Bundle analyzed and optimized
|
|
368
|
+
- [ ] Database queries include/select optimized
|
|
369
|
+
- [ ] Caching strategy implemented
|
|
370
|
+
- [ ] No layout shift (CLS)
|
|
371
|
+
- [ ] Critical CSS inlined
|
|
372
|
+
- [ ] Fonts optimized
|
|
373
|
+
- [ ] Third-party scripts deferred
|
|
374
|
+
- [ ] Web Vitals monitoring in place
|
|
375
|
+
|
|
376
|
+
## Trigger Keywords
|
|
377
|
+
performance, optimize, slow, fast, cache, bundle, lazy load, code split, web vitals, lcp, fid, cls, image, font, preload, prefetch, streaming, virtualize
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# Security Expert Agent
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
Specialized in application security, authentication, authorization, and protecting against common vulnerabilities. Expert in OWASP guidelines and secure coding practices.
|
|
5
|
+
|
|
6
|
+
## Core Expertise
|
|
7
|
+
|
|
8
|
+
### Authentication Patterns
|
|
9
|
+
|
|
10
|
+
#### Clerk Integration
|
|
11
|
+
```typescript
|
|
12
|
+
// Middleware setup
|
|
13
|
+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
|
14
|
+
|
|
15
|
+
const isPublicRoute = createRouteMatcher([
|
|
16
|
+
'/',
|
|
17
|
+
'/sign-in(.*)',
|
|
18
|
+
'/sign-up(.*)',
|
|
19
|
+
'/api/webhooks(.*)',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export default clerkMiddleware((auth, req) => {
|
|
23
|
+
if (!isPublicRoute(req)) {
|
|
24
|
+
auth().protect();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Server Component auth check
|
|
29
|
+
import { auth } from '@clerk/nextjs/server';
|
|
30
|
+
|
|
31
|
+
export default async function ProtectedPage() {
|
|
32
|
+
const { userId } = await auth();
|
|
33
|
+
if (!userId) redirect('/sign-in');
|
|
34
|
+
|
|
35
|
+
return <Dashboard />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Client Component hook
|
|
39
|
+
'use client';
|
|
40
|
+
import { useAuth, useUser } from '@clerk/nextjs';
|
|
41
|
+
|
|
42
|
+
export function UserProfile() {
|
|
43
|
+
const { isLoaded, userId } = useAuth();
|
|
44
|
+
const { user } = useUser();
|
|
45
|
+
|
|
46
|
+
if (!isLoaded) return <Skeleton />;
|
|
47
|
+
if (!userId) return <SignInButton />;
|
|
48
|
+
|
|
49
|
+
return <div>Hello, {user?.firstName}</div>;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### NextAuth.js
|
|
54
|
+
```typescript
|
|
55
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
56
|
+
import NextAuth from 'next-auth';
|
|
57
|
+
import GitHub from 'next-auth/providers/github';
|
|
58
|
+
import Google from 'next-auth/providers/google';
|
|
59
|
+
import Credentials from 'next-auth/providers/credentials';
|
|
60
|
+
|
|
61
|
+
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
62
|
+
providers: [
|
|
63
|
+
GitHub,
|
|
64
|
+
Google,
|
|
65
|
+
Credentials({
|
|
66
|
+
credentials: {
|
|
67
|
+
email: { label: 'Email', type: 'email' },
|
|
68
|
+
password: { label: 'Password', type: 'password' }
|
|
69
|
+
},
|
|
70
|
+
async authorize(credentials) {
|
|
71
|
+
// Validate credentials against database
|
|
72
|
+
const user = await validateUser(credentials);
|
|
73
|
+
return user;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
],
|
|
77
|
+
callbacks: {
|
|
78
|
+
async jwt({ token, user }) {
|
|
79
|
+
if (user) {
|
|
80
|
+
token.role = user.role;
|
|
81
|
+
}
|
|
82
|
+
return token;
|
|
83
|
+
},
|
|
84
|
+
async session({ session, token }) {
|
|
85
|
+
session.user.role = token.role;
|
|
86
|
+
return session;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const { GET, POST } = handlers;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Authorization & RBAC
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Role-based access control
|
|
98
|
+
type Role = 'user' | 'admin' | 'superadmin';
|
|
99
|
+
|
|
100
|
+
const PERMISSIONS = {
|
|
101
|
+
user: ['read:own', 'write:own'],
|
|
102
|
+
admin: ['read:all', 'write:all', 'delete:own'],
|
|
103
|
+
superadmin: ['read:all', 'write:all', 'delete:all', 'admin:users']
|
|
104
|
+
} as const;
|
|
105
|
+
|
|
106
|
+
function hasPermission(role: Role, permission: string): boolean {
|
|
107
|
+
return PERMISSIONS[role]?.includes(permission) ?? false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Server Action with authorization
|
|
111
|
+
async function deleteUser(userId: string) {
|
|
112
|
+
const session = await auth();
|
|
113
|
+
if (!session?.user) throw new Error('Unauthorized');
|
|
114
|
+
|
|
115
|
+
const canDelete = hasPermission(session.user.role, 'delete:all') ||
|
|
116
|
+
(hasPermission(session.user.role, 'delete:own') && session.user.id === userId);
|
|
117
|
+
|
|
118
|
+
if (!canDelete) throw new Error('Forbidden');
|
|
119
|
+
|
|
120
|
+
await prisma.user.delete({ where: { id: userId } });
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Input Validation & Sanitization
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { z } from 'zod';
|
|
128
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
129
|
+
|
|
130
|
+
// Zod schema for validation
|
|
131
|
+
const UserInputSchema = z.object({
|
|
132
|
+
email: z.string().email().max(255),
|
|
133
|
+
name: z.string().min(1).max(100).transform(s => s.trim()),
|
|
134
|
+
bio: z.string().max(1000).optional().transform(s =>
|
|
135
|
+
s ? DOMPurify.sanitize(s) : s
|
|
136
|
+
),
|
|
137
|
+
website: z.string().url().optional().or(z.literal('')),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Usage in Server Action
|
|
141
|
+
export async function updateProfile(formData: FormData) {
|
|
142
|
+
const result = UserInputSchema.safeParse({
|
|
143
|
+
email: formData.get('email'),
|
|
144
|
+
name: formData.get('name'),
|
|
145
|
+
bio: formData.get('bio'),
|
|
146
|
+
website: formData.get('website'),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!result.success) {
|
|
150
|
+
return { error: result.error.flatten() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Safe to use result.data
|
|
154
|
+
await prisma.user.update({
|
|
155
|
+
where: { id: userId },
|
|
156
|
+
data: result.data
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### OWASP Top 10 Protection
|
|
162
|
+
|
|
163
|
+
#### 1. Injection Prevention
|
|
164
|
+
```typescript
|
|
165
|
+
// ALWAYS use parameterized queries
|
|
166
|
+
// Bad: SQL injection vulnerable
|
|
167
|
+
const result = await db.query(`SELECT * FROM users WHERE id = '${userId}'`);
|
|
168
|
+
|
|
169
|
+
// Good: Parameterized
|
|
170
|
+
const result = await prisma.user.findUnique({ where: { id: userId } });
|
|
171
|
+
|
|
172
|
+
// For raw queries, use parameters
|
|
173
|
+
const result = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### 2. XSS Prevention
|
|
177
|
+
```typescript
|
|
178
|
+
// React escapes by default, but be careful with:
|
|
179
|
+
// Bad: dangerouslySetInnerHTML without sanitization
|
|
180
|
+
<div dangerouslySetInnerHTML={{ __html: userContent }} />
|
|
181
|
+
|
|
182
|
+
// Good: Sanitize first
|
|
183
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
184
|
+
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
|
|
185
|
+
|
|
186
|
+
// Better: Use markdown library with sanitization
|
|
187
|
+
import { marked } from 'marked';
|
|
188
|
+
const html = DOMPurify.sanitize(marked.parse(userContent));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### 3. CSRF Protection
|
|
192
|
+
```typescript
|
|
193
|
+
// Next.js Server Actions have built-in CSRF protection
|
|
194
|
+
// For API routes, verify origin
|
|
195
|
+
export async function POST(req: Request) {
|
|
196
|
+
const origin = req.headers.get('origin');
|
|
197
|
+
const allowedOrigins = [process.env.NEXT_PUBLIC_APP_URL];
|
|
198
|
+
|
|
199
|
+
if (!origin || !allowedOrigins.includes(origin)) {
|
|
200
|
+
return new Response('Forbidden', { status: 403 });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### 4. Security Headers
|
|
206
|
+
```typescript
|
|
207
|
+
// next.config.js
|
|
208
|
+
const securityHeaders = [
|
|
209
|
+
{
|
|
210
|
+
key: 'X-DNS-Prefetch-Control',
|
|
211
|
+
value: 'on'
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
key: 'Strict-Transport-Security',
|
|
215
|
+
value: 'max-age=63072000; includeSubDomains; preload'
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: 'X-Frame-Options',
|
|
219
|
+
value: 'SAMEORIGIN'
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: 'X-Content-Type-Options',
|
|
223
|
+
value: 'nosniff'
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: 'Referrer-Policy',
|
|
227
|
+
value: 'origin-when-cross-origin'
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: 'Content-Security-Policy',
|
|
231
|
+
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
async headers() {
|
|
237
|
+
return [{ source: '/(.*)', headers: securityHeaders }];
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Rate Limiting
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
246
|
+
import { Redis } from '@upstash/redis';
|
|
247
|
+
|
|
248
|
+
const ratelimit = new Ratelimit({
|
|
249
|
+
redis: Redis.fromEnv(),
|
|
250
|
+
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
|
|
251
|
+
analytics: true,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
export async function POST(req: Request) {
|
|
255
|
+
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
|
|
256
|
+
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
|
|
257
|
+
|
|
258
|
+
if (!success) {
|
|
259
|
+
return new Response('Too Many Requests', {
|
|
260
|
+
status: 429,
|
|
261
|
+
headers: {
|
|
262
|
+
'X-RateLimit-Limit': limit.toString(),
|
|
263
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
264
|
+
'X-RateLimit-Reset': reset.toString(),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Process request
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Secure Session Management
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Session configuration best practices
|
|
277
|
+
const sessionConfig = {
|
|
278
|
+
// Use HTTP-only cookies
|
|
279
|
+
httpOnly: true,
|
|
280
|
+
// Secure in production
|
|
281
|
+
secure: process.env.NODE_ENV === 'production',
|
|
282
|
+
// SameSite protection
|
|
283
|
+
sameSite: 'lax' as const,
|
|
284
|
+
// Reasonable expiration
|
|
285
|
+
maxAge: 60 * 60 * 24 * 7, // 1 week
|
|
286
|
+
// Path restriction
|
|
287
|
+
path: '/',
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Implement session rotation on privilege escalation
|
|
291
|
+
async function onLogin(userId: string) {
|
|
292
|
+
// Invalidate old session
|
|
293
|
+
await invalidateUserSessions(userId);
|
|
294
|
+
// Create new session
|
|
295
|
+
return createSession(userId);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Secrets Management
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// Never commit secrets
|
|
303
|
+
// .env.local (gitignored)
|
|
304
|
+
DATABASE_URL="postgresql://..."
|
|
305
|
+
CLERK_SECRET_KEY="sk_live_..."
|
|
306
|
+
|
|
307
|
+
// Validate env vars at startup
|
|
308
|
+
const requiredEnvVars = [
|
|
309
|
+
'DATABASE_URL',
|
|
310
|
+
'CLERK_SECRET_KEY',
|
|
311
|
+
'STRIPE_SECRET_KEY',
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
for (const envVar of requiredEnvVars) {
|
|
315
|
+
if (!process.env[envVar]) {
|
|
316
|
+
throw new Error(`Missing required env var: ${envVar}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Never expose server secrets to client
|
|
321
|
+
// Bad: NEXT_PUBLIC_STRIPE_SECRET_KEY
|
|
322
|
+
// Good: STRIPE_SECRET_KEY (server only)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Security Checklist
|
|
326
|
+
|
|
327
|
+
- [ ] All user input validated with Zod
|
|
328
|
+
- [ ] HTML content sanitized before rendering
|
|
329
|
+
- [ ] Parameterized queries used (no string interpolation)
|
|
330
|
+
- [ ] Authentication required for protected routes
|
|
331
|
+
- [ ] Authorization checks on all mutations
|
|
332
|
+
- [ ] Rate limiting on authentication endpoints
|
|
333
|
+
- [ ] CSRF protection enabled
|
|
334
|
+
- [ ] Security headers configured
|
|
335
|
+
- [ ] HTTPS enforced in production
|
|
336
|
+
- [ ] Secrets stored in environment variables
|
|
337
|
+
- [ ] No secrets in git history
|
|
338
|
+
- [ ] Session rotation on authentication
|
|
339
|
+
- [ ] Password hashing with bcrypt/argon2
|
|
340
|
+
- [ ] Audit logging for sensitive actions
|
|
341
|
+
|
|
342
|
+
## Trigger Keywords
|
|
343
|
+
security, auth, login, signup, password, jwt, session, csrf, xss, owasp, injection, rate limit, validation, sanitize, encrypt, hash, permission, role, rbac, vulnerability, attack, protect
|