@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,261 +0,0 @@
|
|
|
1
|
-
# Secrets Management Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for secure handling of secrets and credentials.
|
|
4
|
-
|
|
5
|
-
## Environment Variables
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/env.ts
|
|
9
|
-
import { z } from 'zod'
|
|
10
|
-
|
|
11
|
-
const envSchema = z.object({
|
|
12
|
-
// Database
|
|
13
|
-
DATABASE_URL: z.string().url(),
|
|
14
|
-
|
|
15
|
-
// Auth
|
|
16
|
-
NEXTAUTH_SECRET: z.string().min(32),
|
|
17
|
-
NEXTAUTH_URL: z.string().url(),
|
|
18
|
-
|
|
19
|
-
// Stripe
|
|
20
|
-
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
|
|
21
|
-
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
|
|
22
|
-
NEXT_PUBLIC_STRIPE_KEY: z.string().startsWith('pk_'),
|
|
23
|
-
|
|
24
|
-
// Email
|
|
25
|
-
RESEND_API_KEY: z.string().startsWith('re_'),
|
|
26
|
-
|
|
27
|
-
// Optional
|
|
28
|
-
SENTRY_DSN: z.string().url().optional()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
// Validate at startup
|
|
32
|
-
function validateEnv() {
|
|
33
|
-
const parsed = envSchema.safeParse(process.env)
|
|
34
|
-
|
|
35
|
-
if (!parsed.success) {
|
|
36
|
-
console.error('Invalid environment variables:')
|
|
37
|
-
console.error(parsed.error.flatten().fieldErrors)
|
|
38
|
-
throw new Error('Invalid environment configuration')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return parsed.data
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const env = validateEnv()
|
|
45
|
-
|
|
46
|
-
// Type-safe access
|
|
47
|
-
// env.DATABASE_URL - TypeScript knows this is a string
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Runtime Secret Loading
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
// lib/secrets.ts
|
|
54
|
-
interface SecretStore {
|
|
55
|
-
get(key: string): Promise<string | undefined>
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Local development
|
|
59
|
-
class EnvSecretStore implements SecretStore {
|
|
60
|
-
async get(key: string) {
|
|
61
|
-
return process.env[key]
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// AWS Secrets Manager
|
|
66
|
-
class AwsSecretStore implements SecretStore {
|
|
67
|
-
private client: SecretsManagerClient
|
|
68
|
-
|
|
69
|
-
constructor() {
|
|
70
|
-
this.client = new SecretsManagerClient({})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async get(key: string) {
|
|
74
|
-
const command = new GetSecretValueCommand({ SecretId: key })
|
|
75
|
-
const response = await this.client.send(command)
|
|
76
|
-
return response.SecretString
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Factory
|
|
81
|
-
export function createSecretStore(): SecretStore {
|
|
82
|
-
if (process.env.NODE_ENV === 'development') {
|
|
83
|
-
return new EnvSecretStore()
|
|
84
|
-
}
|
|
85
|
-
return new AwsSecretStore()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Usage
|
|
89
|
-
const secrets = createSecretStore()
|
|
90
|
-
const apiKey = await secrets.get('STRIPE_SECRET_KEY')
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Encrypted Secrets
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
// lib/crypto.ts
|
|
97
|
-
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto'
|
|
98
|
-
import { promisify } from 'util'
|
|
99
|
-
|
|
100
|
-
const scryptAsync = promisify(scrypt)
|
|
101
|
-
|
|
102
|
-
export async function encryptSecret(plaintext: string, password: string): Promise<string> {
|
|
103
|
-
const salt = randomBytes(16)
|
|
104
|
-
const key = (await scryptAsync(password, salt, 32)) as Buffer
|
|
105
|
-
const iv = randomBytes(16)
|
|
106
|
-
|
|
107
|
-
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
108
|
-
const encrypted = Buffer.concat([
|
|
109
|
-
cipher.update(plaintext, 'utf8'),
|
|
110
|
-
cipher.final()
|
|
111
|
-
])
|
|
112
|
-
const authTag = cipher.getAuthTag()
|
|
113
|
-
|
|
114
|
-
// Combine: salt + iv + authTag + encrypted
|
|
115
|
-
const combined = Buffer.concat([salt, iv, authTag, encrypted])
|
|
116
|
-
return combined.toString('base64')
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function decryptSecret(ciphertext: string, password: string): Promise<string> {
|
|
120
|
-
const combined = Buffer.from(ciphertext, 'base64')
|
|
121
|
-
|
|
122
|
-
const salt = combined.subarray(0, 16)
|
|
123
|
-
const iv = combined.subarray(16, 32)
|
|
124
|
-
const authTag = combined.subarray(32, 48)
|
|
125
|
-
const encrypted = combined.subarray(48)
|
|
126
|
-
|
|
127
|
-
const key = (await scryptAsync(password, salt, 32)) as Buffer
|
|
128
|
-
|
|
129
|
-
const decipher = createDecipheriv('aes-256-gcm', key, iv)
|
|
130
|
-
decipher.setAuthTag(authTag)
|
|
131
|
-
|
|
132
|
-
const decrypted = Buffer.concat([
|
|
133
|
-
decipher.update(encrypted),
|
|
134
|
-
decipher.final()
|
|
135
|
-
])
|
|
136
|
-
|
|
137
|
-
return decrypted.toString('utf8')
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## API Key Storage
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
// lib/api-keys.ts
|
|
145
|
-
import { randomBytes, createHash } from 'crypto'
|
|
146
|
-
import { prisma } from '@/lib/db'
|
|
147
|
-
|
|
148
|
-
export async function createApiKey(userId: string, name: string) {
|
|
149
|
-
// Generate key: prefix_randomBytes
|
|
150
|
-
const keyValue = `bs_${randomBytes(24).toString('hex')}`
|
|
151
|
-
|
|
152
|
-
// Hash for storage (never store plain key)
|
|
153
|
-
const keyHash = createHash('sha256').update(keyValue).digest('hex')
|
|
154
|
-
|
|
155
|
-
// Store last 4 chars for display
|
|
156
|
-
const keyHint = keyValue.slice(-4)
|
|
157
|
-
|
|
158
|
-
await prisma.apiKey.create({
|
|
159
|
-
data: {
|
|
160
|
-
userId,
|
|
161
|
-
name,
|
|
162
|
-
keyHash,
|
|
163
|
-
keyHint
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Only return the full key once!
|
|
168
|
-
return keyValue
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function validateApiKey(key: string) {
|
|
172
|
-
const keyHash = createHash('sha256').update(key).digest('hex')
|
|
173
|
-
|
|
174
|
-
const apiKey = await prisma.apiKey.findUnique({
|
|
175
|
-
where: { keyHash },
|
|
176
|
-
include: { user: true }
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
if (!apiKey || apiKey.revokedAt) {
|
|
180
|
-
return null
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Update last used
|
|
184
|
-
await prisma.apiKey.update({
|
|
185
|
-
where: { id: apiKey.id },
|
|
186
|
-
data: { lastUsedAt: new Date() }
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
return apiKey.user
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## Secure Token Generation
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
// lib/tokens.ts
|
|
197
|
-
import { randomBytes } from 'crypto'
|
|
198
|
-
|
|
199
|
-
// URL-safe token
|
|
200
|
-
export function generateToken(bytes = 32): string {
|
|
201
|
-
return randomBytes(bytes).toString('base64url')
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Numeric OTP
|
|
205
|
-
export function generateOtp(length = 6): string {
|
|
206
|
-
const max = Math.pow(10, length) - 1
|
|
207
|
-
const randomNum = parseInt(randomBytes(4).toString('hex'), 16)
|
|
208
|
-
return String(randomNum % (max + 1)).padStart(length, '0')
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Short code (for user input)
|
|
212
|
-
export function generateCode(length = 8): string {
|
|
213
|
-
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // No confusing chars
|
|
214
|
-
let code = ''
|
|
215
|
-
|
|
216
|
-
for (let i = 0; i < length; i++) {
|
|
217
|
-
const randomIndex = randomBytes(1)[0] % chars.length
|
|
218
|
-
code += chars[randomIndex]
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return code
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Secret Rotation
|
|
226
|
-
|
|
227
|
-
```typescript
|
|
228
|
-
// lib/rotation.ts
|
|
229
|
-
import { prisma } from '@/lib/db'
|
|
230
|
-
|
|
231
|
-
export async function rotateEncryptionKey() {
|
|
232
|
-
const oldKey = process.env.ENCRYPTION_KEY!
|
|
233
|
-
const newKey = process.env.NEW_ENCRYPTION_KEY!
|
|
234
|
-
|
|
235
|
-
// Find all encrypted records
|
|
236
|
-
const records = await prisma.secretData.findMany()
|
|
237
|
-
|
|
238
|
-
for (const record of records) {
|
|
239
|
-
// Decrypt with old key
|
|
240
|
-
const plaintext = await decryptSecret(record.encryptedValue, oldKey)
|
|
241
|
-
|
|
242
|
-
// Re-encrypt with new key
|
|
243
|
-
const newEncrypted = await encryptSecret(plaintext, newKey)
|
|
244
|
-
|
|
245
|
-
// Update record
|
|
246
|
-
await prisma.secretData.update({
|
|
247
|
-
where: { id: record.id },
|
|
248
|
-
data: { encryptedValue: newEncrypted }
|
|
249
|
-
})
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
console.log(`Rotated ${records.length} secrets`)
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
## When to Use
|
|
257
|
-
|
|
258
|
-
- API keys
|
|
259
|
-
- Database credentials
|
|
260
|
-
- Encryption keys
|
|
261
|
-
- Third-party tokens
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
# Input Validation & Security Patterns
|
|
2
|
-
|
|
3
|
-
Battle-tested patterns for secure input handling.
|
|
4
|
-
|
|
5
|
-
## Zod Schema Validation
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/validations/user.ts
|
|
9
|
-
import { z } from 'zod'
|
|
10
|
-
|
|
11
|
-
export const CreateUserSchema = z.object({
|
|
12
|
-
email: z.string().email('Invalid email address'),
|
|
13
|
-
name: z.string()
|
|
14
|
-
.min(1, 'Name is required')
|
|
15
|
-
.max(100, 'Name too long')
|
|
16
|
-
.regex(/^[a-zA-Z\s'-]+$/, 'Invalid characters in name'),
|
|
17
|
-
password: z.string()
|
|
18
|
-
.min(8, 'Password must be at least 8 characters')
|
|
19
|
-
.regex(/[A-Z]/, 'Password must contain uppercase letter')
|
|
20
|
-
.regex(/[a-z]/, 'Password must contain lowercase letter')
|
|
21
|
-
.regex(/[0-9]/, 'Password must contain number')
|
|
22
|
-
.regex(/[^A-Za-z0-9]/, 'Password must contain special character'),
|
|
23
|
-
age: z.number()
|
|
24
|
-
.int('Age must be a whole number')
|
|
25
|
-
.min(13, 'Must be at least 13 years old')
|
|
26
|
-
.max(120, 'Invalid age')
|
|
27
|
-
.optional()
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
export const UpdateProfileSchema = z.object({
|
|
31
|
-
name: z.string().min(1).max(100).optional(),
|
|
32
|
-
bio: z.string().max(500).optional(),
|
|
33
|
-
website: z.string().url().optional().or(z.literal(''))
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
export type CreateUserInput = z.infer<typeof CreateUserSchema>
|
|
37
|
-
export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Server Action with Validation
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
// actions/user.ts
|
|
44
|
-
'use server'
|
|
45
|
-
|
|
46
|
-
import { CreateUserSchema } from '@/lib/validations/user'
|
|
47
|
-
import { prisma } from '@/lib/db'
|
|
48
|
-
|
|
49
|
-
type ActionResult =
|
|
50
|
-
| { success: true; data: { id: string } }
|
|
51
|
-
| { success: false; error: string; fieldErrors?: Record<string, string[]> }
|
|
52
|
-
|
|
53
|
-
export async function createUser(formData: FormData): Promise<ActionResult> {
|
|
54
|
-
// Parse and validate
|
|
55
|
-
const raw = {
|
|
56
|
-
email: formData.get('email'),
|
|
57
|
-
name: formData.get('name'),
|
|
58
|
-
password: formData.get('password'),
|
|
59
|
-
age: formData.get('age') ? Number(formData.get('age')) : undefined
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const result = CreateUserSchema.safeParse(raw)
|
|
63
|
-
|
|
64
|
-
if (!result.success) {
|
|
65
|
-
return {
|
|
66
|
-
success: false,
|
|
67
|
-
error: 'Validation failed',
|
|
68
|
-
fieldErrors: result.error.flatten().fieldErrors
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Hash password before storing
|
|
73
|
-
const hashedPassword = await hashPassword(result.data.password)
|
|
74
|
-
|
|
75
|
-
const user = await prisma.user.create({
|
|
76
|
-
data: {
|
|
77
|
-
...result.data,
|
|
78
|
-
password: hashedPassword
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
return { success: true, data: { id: user.id } }
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Sanitizing User Input
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
// lib/sanitize.ts
|
|
90
|
-
import DOMPurify from 'isomorphic-dompurify'
|
|
91
|
-
|
|
92
|
-
// Sanitize HTML content (for rich text)
|
|
93
|
-
export function sanitizeHtml(dirty: string): string {
|
|
94
|
-
return DOMPurify.sanitize(dirty, {
|
|
95
|
-
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
|
|
96
|
-
ALLOWED_ATTR: ['href', 'target', 'rel']
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Strip all HTML (for plain text fields)
|
|
101
|
-
export function stripHtml(input: string): string {
|
|
102
|
-
return DOMPurify.sanitize(input, { ALLOWED_TAGS: [] })
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Sanitize for SQL-safe strings (use with parameterized queries)
|
|
106
|
-
export function escapeString(input: string): string {
|
|
107
|
-
return input
|
|
108
|
-
.replace(/\\/g, '\\\\')
|
|
109
|
-
.replace(/'/g, "\\'")
|
|
110
|
-
.replace(/"/g, '\\"')
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// URL validation and sanitization
|
|
114
|
-
export function sanitizeUrl(url: string): string | null {
|
|
115
|
-
try {
|
|
116
|
-
const parsed = new URL(url)
|
|
117
|
-
// Only allow http/https
|
|
118
|
-
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
119
|
-
return null
|
|
120
|
-
}
|
|
121
|
-
return parsed.toString()
|
|
122
|
-
} catch {
|
|
123
|
-
return null
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## Rate Limiting
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
// lib/rate-limit.ts
|
|
132
|
-
import { Ratelimit } from '@upstash/ratelimit'
|
|
133
|
-
import { Redis } from '@upstash/redis'
|
|
134
|
-
|
|
135
|
-
// Using Upstash Redis for serverless
|
|
136
|
-
const redis = new Redis({
|
|
137
|
-
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
138
|
-
token: process.env.UPSTASH_REDIS_REST_TOKEN!
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
// Different rate limiters for different purposes
|
|
142
|
-
export const authLimiter = new Ratelimit({
|
|
143
|
-
redis,
|
|
144
|
-
limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 attempts per minute
|
|
145
|
-
prefix: 'ratelimit:auth'
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
export const apiLimiter = new Ratelimit({
|
|
149
|
-
redis,
|
|
150
|
-
limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 requests per minute
|
|
151
|
-
prefix: 'ratelimit:api'
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
// Usage in API route
|
|
155
|
-
export async function POST(request: Request) {
|
|
156
|
-
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
|
|
157
|
-
|
|
158
|
-
const { success, limit, remaining, reset } = await authLimiter.limit(ip)
|
|
159
|
-
|
|
160
|
-
if (!success) {
|
|
161
|
-
return new Response('Too many requests', {
|
|
162
|
-
status: 429,
|
|
163
|
-
headers: {
|
|
164
|
-
'X-RateLimit-Limit': limit.toString(),
|
|
165
|
-
'X-RateLimit-Remaining': remaining.toString(),
|
|
166
|
-
'X-RateLimit-Reset': reset.toString()
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Continue with handler...
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## CSRF Protection
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
// lib/csrf.ts
|
|
179
|
-
import { cookies } from 'next/headers'
|
|
180
|
-
import { randomBytes, createHmac } from 'crypto'
|
|
181
|
-
|
|
182
|
-
const SECRET = process.env.CSRF_SECRET!
|
|
183
|
-
|
|
184
|
-
export async function generateCsrfToken(): Promise<string> {
|
|
185
|
-
const token = randomBytes(32).toString('hex')
|
|
186
|
-
const signature = createHmac('sha256', SECRET).update(token).digest('hex')
|
|
187
|
-
const fullToken = `${token}.${signature}`
|
|
188
|
-
|
|
189
|
-
const cookieStore = await cookies()
|
|
190
|
-
cookieStore.set('csrf-token', fullToken, {
|
|
191
|
-
httpOnly: true,
|
|
192
|
-
secure: process.env.NODE_ENV === 'production',
|
|
193
|
-
sameSite: 'strict',
|
|
194
|
-
maxAge: 60 * 60 // 1 hour
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
return token // Only return the token part to the client
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export async function validateCsrfToken(clientToken: string): Promise<boolean> {
|
|
201
|
-
const cookieStore = await cookies()
|
|
202
|
-
const cookieToken = cookieStore.get('csrf-token')?.value
|
|
203
|
-
|
|
204
|
-
if (!cookieToken) return false
|
|
205
|
-
|
|
206
|
-
const [storedToken, storedSignature] = cookieToken.split('.')
|
|
207
|
-
const expectedSignature = createHmac('sha256', SECRET)
|
|
208
|
-
.update(storedToken)
|
|
209
|
-
.digest('hex')
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
storedToken === clientToken &&
|
|
213
|
-
storedSignature === expectedSignature
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Security Headers Middleware
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
// middleware.ts
|
|
222
|
-
import { NextResponse } from 'next/server'
|
|
223
|
-
import type { NextRequest } from 'next/server'
|
|
224
|
-
|
|
225
|
-
export function middleware(request: NextRequest) {
|
|
226
|
-
const response = NextResponse.next()
|
|
227
|
-
|
|
228
|
-
// Security headers
|
|
229
|
-
response.headers.set('X-Frame-Options', 'DENY')
|
|
230
|
-
response.headers.set('X-Content-Type-Options', 'nosniff')
|
|
231
|
-
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
|
|
232
|
-
response.headers.set('X-XSS-Protection', '1; mode=block')
|
|
233
|
-
|
|
234
|
-
// Content Security Policy
|
|
235
|
-
response.headers.set(
|
|
236
|
-
'Content-Security-Policy',
|
|
237
|
-
[
|
|
238
|
-
"default-src 'self'",
|
|
239
|
-
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
240
|
-
"style-src 'self' 'unsafe-inline'",
|
|
241
|
-
"img-src 'self' data: https:",
|
|
242
|
-
"font-src 'self'",
|
|
243
|
-
"connect-src 'self' https://api.stripe.com",
|
|
244
|
-
"frame-ancestors 'none'"
|
|
245
|
-
].join('; ')
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
return response
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
## Environment Variable Validation
|
|
253
|
-
|
|
254
|
-
```typescript
|
|
255
|
-
// lib/env.ts
|
|
256
|
-
import { z } from 'zod'
|
|
257
|
-
|
|
258
|
-
const envSchema = z.object({
|
|
259
|
-
DATABASE_URL: z.string().url(),
|
|
260
|
-
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
|
|
261
|
-
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
|
|
262
|
-
NEXT_PUBLIC_URL: z.string().url(),
|
|
263
|
-
NODE_ENV: z.enum(['development', 'production', 'test'])
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
// Validate at build/startup time
|
|
267
|
-
export const env = envSchema.parse(process.env)
|
|
268
|
-
```
|