@girardmedia/bootspring 1.2.0 → 2.0.3
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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
# Security Headers Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for HTTP security headers.
|
|
4
|
-
|
|
5
|
-
## Next.js Headers Config
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// next.config.js
|
|
9
|
-
/** @type {import('next').NextConfig} */
|
|
10
|
-
const nextConfig = {
|
|
11
|
-
async headers() {
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
source: '/:path*',
|
|
15
|
-
headers: [
|
|
16
|
-
{
|
|
17
|
-
key: 'X-DNS-Prefetch-Control',
|
|
18
|
-
value: 'on'
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
key: 'Strict-Transport-Security',
|
|
22
|
-
value: 'max-age=63072000; includeSubDomains; preload'
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
key: 'X-Frame-Options',
|
|
26
|
-
value: 'SAMEORIGIN'
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
key: 'X-Content-Type-Options',
|
|
30
|
-
value: 'nosniff'
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
key: 'Referrer-Policy',
|
|
34
|
-
value: 'strict-origin-when-cross-origin'
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
key: 'Permissions-Policy',
|
|
38
|
-
value: 'camera=(), microphone=(), geolocation=()'
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = nextConfig
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Content Security Policy
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
// lib/csp.ts
|
|
53
|
-
type CspDirective =
|
|
54
|
-
| 'default-src'
|
|
55
|
-
| 'script-src'
|
|
56
|
-
| 'style-src'
|
|
57
|
-
| 'img-src'
|
|
58
|
-
| 'font-src'
|
|
59
|
-
| 'connect-src'
|
|
60
|
-
| 'media-src'
|
|
61
|
-
| 'object-src'
|
|
62
|
-
| 'frame-src'
|
|
63
|
-
| 'frame-ancestors'
|
|
64
|
-
| 'base-uri'
|
|
65
|
-
| 'form-action'
|
|
66
|
-
| 'upgrade-insecure-requests'
|
|
67
|
-
|
|
68
|
-
export function buildCsp(directives: Partial<Record<CspDirective, string[]>>): string {
|
|
69
|
-
const defaults: Partial<Record<CspDirective, string[]>> = {
|
|
70
|
-
'default-src': ["'self'"],
|
|
71
|
-
'script-src': ["'self'"],
|
|
72
|
-
'style-src': ["'self'", "'unsafe-inline'"],
|
|
73
|
-
'img-src': ["'self'", 'data:', 'https:'],
|
|
74
|
-
'font-src': ["'self'"],
|
|
75
|
-
'connect-src': ["'self'"],
|
|
76
|
-
'frame-ancestors': ["'none'"],
|
|
77
|
-
'base-uri': ["'self'"],
|
|
78
|
-
'form-action': ["'self'"]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const merged = { ...defaults, ...directives }
|
|
82
|
-
|
|
83
|
-
return Object.entries(merged)
|
|
84
|
-
.map(([key, values]) => `${key} ${values!.join(' ')}`)
|
|
85
|
-
.join('; ')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Usage
|
|
89
|
-
const csp = buildCsp({
|
|
90
|
-
'script-src': ["'self'", 'https://js.stripe.com'],
|
|
91
|
-
'frame-src': ["'self'", 'https://js.stripe.com'],
|
|
92
|
-
'connect-src': ["'self'", 'https://api.stripe.com']
|
|
93
|
-
})
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Middleware with Security Headers
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
// middleware.ts
|
|
100
|
-
import { NextResponse } from 'next/server'
|
|
101
|
-
import type { NextRequest } from 'next/server'
|
|
102
|
-
import { buildCsp } from '@/lib/csp'
|
|
103
|
-
|
|
104
|
-
export function middleware(request: NextRequest) {
|
|
105
|
-
const response = NextResponse.next()
|
|
106
|
-
|
|
107
|
-
// Generate nonce for inline scripts
|
|
108
|
-
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
|
|
109
|
-
|
|
110
|
-
const csp = buildCsp({
|
|
111
|
-
'script-src': ["'self'", `'nonce-${nonce}'`],
|
|
112
|
-
'style-src': ["'self'", "'unsafe-inline'"],
|
|
113
|
-
'connect-src': [
|
|
114
|
-
"'self'",
|
|
115
|
-
'https://api.stripe.com',
|
|
116
|
-
process.env.NEXT_PUBLIC_API_URL!
|
|
117
|
-
]
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
response.headers.set('Content-Security-Policy', csp)
|
|
121
|
-
response.headers.set('X-Nonce', nonce)
|
|
122
|
-
|
|
123
|
-
return response
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Nonce for Inline Scripts
|
|
128
|
-
|
|
129
|
-
```tsx
|
|
130
|
-
// app/layout.tsx
|
|
131
|
-
import { headers } from 'next/headers'
|
|
132
|
-
|
|
133
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
134
|
-
const nonce = headers().get('X-Nonce') ?? ''
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<html lang="en">
|
|
138
|
-
<head>
|
|
139
|
-
<script
|
|
140
|
-
nonce={nonce}
|
|
141
|
-
dangerouslySetInnerHTML={{
|
|
142
|
-
__html: `
|
|
143
|
-
// Inline script with nonce
|
|
144
|
-
window.__CONFIG__ = ${JSON.stringify({ apiUrl: process.env.NEXT_PUBLIC_API_URL })}
|
|
145
|
-
`
|
|
146
|
-
}}
|
|
147
|
-
/>
|
|
148
|
-
</head>
|
|
149
|
-
<body>{children}</body>
|
|
150
|
-
</html>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## CORS Headers
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
// lib/cors.ts
|
|
159
|
-
const ALLOWED_ORIGINS = [
|
|
160
|
-
'https://app.example.com',
|
|
161
|
-
'https://admin.example.com'
|
|
162
|
-
]
|
|
163
|
-
|
|
164
|
-
export function getCorsHeaders(origin: string | null) {
|
|
165
|
-
const headers: Record<string, string> = {}
|
|
166
|
-
|
|
167
|
-
if (origin && ALLOWED_ORIGINS.includes(origin)) {
|
|
168
|
-
headers['Access-Control-Allow-Origin'] = origin
|
|
169
|
-
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
|
170
|
-
headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
|
171
|
-
headers['Access-Control-Allow-Credentials'] = 'true'
|
|
172
|
-
headers['Access-Control-Max-Age'] = '86400'
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return headers
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Usage in API route
|
|
179
|
-
export async function OPTIONS(request: Request) {
|
|
180
|
-
const origin = request.headers.get('origin')
|
|
181
|
-
const corsHeaders = getCorsHeaders(origin)
|
|
182
|
-
|
|
183
|
-
return new Response(null, {
|
|
184
|
-
status: 204,
|
|
185
|
-
headers: corsHeaders
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export async function GET(request: Request) {
|
|
190
|
-
const origin = request.headers.get('origin')
|
|
191
|
-
const corsHeaders = getCorsHeaders(origin)
|
|
192
|
-
|
|
193
|
-
const data = await getData()
|
|
194
|
-
|
|
195
|
-
return Response.json(data, { headers: corsHeaders })
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
## Cache Control Headers
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
// API responses with sensitive data
|
|
203
|
-
export async function GET() {
|
|
204
|
-
const sensitiveData = await getSensitiveData()
|
|
205
|
-
|
|
206
|
-
return Response.json(sensitiveData, {
|
|
207
|
-
headers: {
|
|
208
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, private',
|
|
209
|
-
'Pragma': 'no-cache',
|
|
210
|
-
'Expires': '0'
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Static/public data
|
|
216
|
-
export async function GET() {
|
|
217
|
-
const publicData = await getPublicData()
|
|
218
|
-
|
|
219
|
-
return Response.json(publicData, {
|
|
220
|
-
headers: {
|
|
221
|
-
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
|
|
222
|
-
'CDN-Cache-Control': 'public, max-age=86400'
|
|
223
|
-
}
|
|
224
|
-
})
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## Feature Policy / Permissions Policy
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
// Restrict browser features
|
|
232
|
-
const permissionsPolicy = [
|
|
233
|
-
'camera=()',
|
|
234
|
-
'microphone=()',
|
|
235
|
-
'geolocation=()',
|
|
236
|
-
'interest-cohort=()', // Disable FLoC
|
|
237
|
-
'payment=(self)',
|
|
238
|
-
'usb=()',
|
|
239
|
-
'magnetometer=()',
|
|
240
|
-
'gyroscope=()',
|
|
241
|
-
'accelerometer=()'
|
|
242
|
-
].join(', ')
|
|
243
|
-
|
|
244
|
-
response.headers.set('Permissions-Policy', permissionsPolicy)
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
## When to Use
|
|
248
|
-
|
|
249
|
-
- All production deployments
|
|
250
|
-
- API endpoints
|
|
251
|
-
- Pages with sensitive data
|
|
252
|
-
- Third-party integrations
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
# Input Sanitization Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for sanitizing and validating user input.
|
|
4
|
-
|
|
5
|
-
## Zod Validation with Sanitization
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/schemas.ts
|
|
9
|
-
import { z } from 'zod'
|
|
10
|
-
import DOMPurify from 'isomorphic-dompurify'
|
|
11
|
-
|
|
12
|
-
// Trim and normalize whitespace
|
|
13
|
-
const trimmedString = z.string().transform(s => s.trim().replace(/\s+/g, ' '))
|
|
14
|
-
|
|
15
|
-
// Strip HTML tags
|
|
16
|
-
const plainText = z.string().transform(s =>
|
|
17
|
-
DOMPurify.sanitize(s, { ALLOWED_TAGS: [] })
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
// Normalize email
|
|
21
|
-
const normalizedEmail = z.string()
|
|
22
|
-
.email()
|
|
23
|
-
.transform(s => s.toLowerCase().trim())
|
|
24
|
-
|
|
25
|
-
// Sanitize rich text
|
|
26
|
-
const richText = z.string().transform(s =>
|
|
27
|
-
DOMPurify.sanitize(s, {
|
|
28
|
-
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
|
|
29
|
-
ALLOWED_ATTR: ['href']
|
|
30
|
-
})
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
// Example schema
|
|
34
|
-
export const UserInputSchema = z.object({
|
|
35
|
-
name: trimmedString.min(1).max(100),
|
|
36
|
-
email: normalizedEmail,
|
|
37
|
-
bio: plainText.max(500).optional(),
|
|
38
|
-
about: richText.max(5000).optional()
|
|
39
|
-
})
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## SQL Injection Prevention
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
// ALWAYS use parameterized queries
|
|
46
|
-
|
|
47
|
-
// DON'T do this:
|
|
48
|
-
// await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = '${userId}'`)
|
|
49
|
-
|
|
50
|
-
// DO this:
|
|
51
|
-
await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`
|
|
52
|
-
|
|
53
|
-
// Or use Prisma's type-safe queries:
|
|
54
|
-
await prisma.user.findUnique({ where: { id: userId } })
|
|
55
|
-
|
|
56
|
-
// For dynamic column names (be careful):
|
|
57
|
-
const allowedColumns = ['name', 'email', 'createdAt']
|
|
58
|
-
const orderBy = allowedColumns.includes(input.sortBy)
|
|
59
|
-
? input.sortBy
|
|
60
|
-
: 'createdAt'
|
|
61
|
-
|
|
62
|
-
await prisma.user.findMany({
|
|
63
|
-
orderBy: { [orderBy]: 'desc' }
|
|
64
|
-
})
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Path Traversal Prevention
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// lib/files.ts
|
|
71
|
-
import path from 'path'
|
|
72
|
-
|
|
73
|
-
const UPLOAD_DIR = '/app/uploads'
|
|
74
|
-
|
|
75
|
-
export function sanitizePath(userPath: string): string {
|
|
76
|
-
// Remove null bytes
|
|
77
|
-
const cleaned = userPath.replace(/\0/g, '')
|
|
78
|
-
|
|
79
|
-
// Get basename only (no directory traversal)
|
|
80
|
-
const basename = path.basename(cleaned)
|
|
81
|
-
|
|
82
|
-
// Remove special characters
|
|
83
|
-
const safe = basename.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
84
|
-
|
|
85
|
-
return safe
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function getUploadPath(filename: string): string {
|
|
89
|
-
const safeName = sanitizePath(filename)
|
|
90
|
-
|
|
91
|
-
if (!safeName) {
|
|
92
|
-
throw new Error('Invalid filename')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const fullPath = path.join(UPLOAD_DIR, safeName)
|
|
96
|
-
|
|
97
|
-
// Ensure path is within upload directory
|
|
98
|
-
if (!fullPath.startsWith(UPLOAD_DIR)) {
|
|
99
|
-
throw new Error('Invalid path')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return fullPath
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Command Injection Prevention
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
// DON'T do this:
|
|
110
|
-
// exec(`convert ${userInput}.png output.jpg`)
|
|
111
|
-
|
|
112
|
-
// DO this - use spawn with arguments array:
|
|
113
|
-
import { spawn } from 'child_process'
|
|
114
|
-
|
|
115
|
-
function convertImage(inputFile: string, outputFile: string) {
|
|
116
|
-
// Validate filenames first
|
|
117
|
-
if (!/^[a-zA-Z0-9_-]+\.(png|jpg|gif)$/.test(inputFile)) {
|
|
118
|
-
throw new Error('Invalid input filename')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
const process = spawn('convert', [inputFile, outputFile])
|
|
123
|
-
|
|
124
|
-
process.on('close', code => {
|
|
125
|
-
code === 0 ? resolve(true) : reject(new Error(`Exit code: ${code}`))
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Even better - use libraries instead of shell commands
|
|
131
|
-
import sharp from 'sharp'
|
|
132
|
-
|
|
133
|
-
async function convertImage(inputPath: string, outputPath: string) {
|
|
134
|
-
await sharp(inputPath)
|
|
135
|
-
.toFormat('jpeg')
|
|
136
|
-
.toFile(outputPath)
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## URL Sanitization
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// lib/url.ts
|
|
144
|
-
const ALLOWED_PROTOCOLS = ['http:', 'https:']
|
|
145
|
-
const BLOCKED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
|
|
146
|
-
|
|
147
|
-
export function sanitizeUrl(input: string): string | null {
|
|
148
|
-
try {
|
|
149
|
-
const url = new URL(input)
|
|
150
|
-
|
|
151
|
-
// Check protocol
|
|
152
|
-
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
153
|
-
return null
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Block internal hosts
|
|
157
|
-
if (BLOCKED_HOSTS.some(h => url.hostname.includes(h))) {
|
|
158
|
-
return null
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Block private IPs
|
|
162
|
-
if (isPrivateIp(url.hostname)) {
|
|
163
|
-
return null
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return url.toString()
|
|
167
|
-
} catch {
|
|
168
|
-
return null
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function isPrivateIp(hostname: string): boolean {
|
|
173
|
-
const ipPatterns = [
|
|
174
|
-
/^10\./,
|
|
175
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
176
|
-
/^192\.168\./,
|
|
177
|
-
/^169\.254\./,
|
|
178
|
-
/^fc00:/i,
|
|
179
|
-
/^fe80:/i
|
|
180
|
-
]
|
|
181
|
-
|
|
182
|
-
return ipPatterns.some(pattern => pattern.test(hostname))
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## JSON Sanitization
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
// lib/json.ts
|
|
190
|
-
|
|
191
|
-
// Safe JSON parse that limits depth and size
|
|
192
|
-
export function safeJsonParse<T>(
|
|
193
|
-
input: string,
|
|
194
|
-
maxSize = 1024 * 1024, // 1MB
|
|
195
|
-
maxDepth = 10
|
|
196
|
-
): T {
|
|
197
|
-
if (input.length > maxSize) {
|
|
198
|
-
throw new Error('JSON too large')
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Count nesting depth
|
|
202
|
-
let depth = 0
|
|
203
|
-
let maxReached = 0
|
|
204
|
-
|
|
205
|
-
for (const char of input) {
|
|
206
|
-
if (char === '{' || char === '[') {
|
|
207
|
-
depth++
|
|
208
|
-
maxReached = Math.max(maxReached, depth)
|
|
209
|
-
} else if (char === '}' || char === ']') {
|
|
210
|
-
depth--
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (maxReached > maxDepth) {
|
|
214
|
-
throw new Error('JSON too deeply nested')
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return JSON.parse(input)
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Filename Sanitization
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
// lib/filename.ts
|
|
226
|
-
|
|
227
|
-
export function sanitizeFilename(filename: string): string {
|
|
228
|
-
// Remove path components
|
|
229
|
-
const basename = filename.split(/[\\/]/).pop() || ''
|
|
230
|
-
|
|
231
|
-
// Remove special characters, keep alphanumeric, dots, dashes, underscores
|
|
232
|
-
const cleaned = basename
|
|
233
|
-
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
234
|
-
.replace(/\.{2,}/g, '.') // No consecutive dots
|
|
235
|
-
.replace(/^\.+|\.+$/g, '') // No leading/trailing dots
|
|
236
|
-
|
|
237
|
-
// Ensure not empty
|
|
238
|
-
if (!cleaned) {
|
|
239
|
-
return 'file'
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Limit length
|
|
243
|
-
if (cleaned.length > 255) {
|
|
244
|
-
const ext = cleaned.split('.').pop() || ''
|
|
245
|
-
const name = cleaned.slice(0, 255 - ext.length - 1)
|
|
246
|
-
return `${name}.${ext}`
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return cleaned
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## When to Use
|
|
254
|
-
|
|
255
|
-
- User input processing
|
|
256
|
-
- File uploads
|
|
257
|
-
- Database queries
|
|
258
|
-
- External URLs
|