ship-safe 1.0.1 → 3.0.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/README.md +281 -23
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/cli/bin/ship-safe.js +44 -2
- package/cli/commands/fix.js +216 -0
- package/cli/commands/guard.js +297 -0
- package/cli/commands/mcp.js +303 -0
- package/cli/commands/scan.js +231 -39
- package/cli/utils/entropy.js +126 -0
- package/cli/utils/output.js +10 -1
- package/cli/utils/patterns.js +376 -24
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/ship-safeignore-template +50 -0
- package/configs/supabase/rls-templates.sql +242 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/package.json +11 -2
- package/snippets/README.md +89 -25
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiting with Upstash Redis
|
|
3
|
+
* =================================
|
|
4
|
+
*
|
|
5
|
+
* Copy this file to your Next.js project.
|
|
6
|
+
*
|
|
7
|
+
* WHY RATE LIMITING MATTERS:
|
|
8
|
+
* - Prevents brute force attacks on auth endpoints
|
|
9
|
+
* - Protects against API abuse and scraping
|
|
10
|
+
* - Controls costs for AI/LLM endpoints
|
|
11
|
+
* - Required for production apps
|
|
12
|
+
*
|
|
13
|
+
* SETUP:
|
|
14
|
+
* 1. Create account at upstash.com
|
|
15
|
+
* 2. Create a Redis database
|
|
16
|
+
* 3. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to .env
|
|
17
|
+
* 4. npm install @upstash/ratelimit @upstash/redis
|
|
18
|
+
*
|
|
19
|
+
* USAGE:
|
|
20
|
+
* import { ratelimit, checkRateLimit } from '@/lib/ratelimit';
|
|
21
|
+
*
|
|
22
|
+
* // In API route
|
|
23
|
+
* const { success, remaining } = await checkRateLimit(request);
|
|
24
|
+
* if (!success) {
|
|
25
|
+
* return new Response('Too many requests', { status: 429 });
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
30
|
+
import { Redis } from '@upstash/redis';
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// REDIS CLIENT
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
const redis = new Redis({
|
|
37
|
+
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
38
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// RATE LIMITERS FOR DIFFERENT USE CASES
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* General API rate limiter
|
|
47
|
+
* 60 requests per minute per IP
|
|
48
|
+
*/
|
|
49
|
+
export const ratelimit = new Ratelimit({
|
|
50
|
+
redis,
|
|
51
|
+
limiter: Ratelimit.slidingWindow(60, '1 m'),
|
|
52
|
+
analytics: true,
|
|
53
|
+
prefix: 'ratelimit:api',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Auth endpoints rate limiter (stricter)
|
|
58
|
+
* 5 requests per minute per IP
|
|
59
|
+
* Prevents brute force attacks
|
|
60
|
+
*/
|
|
61
|
+
export const authRatelimit = new Ratelimit({
|
|
62
|
+
redis,
|
|
63
|
+
limiter: Ratelimit.slidingWindow(5, '1 m'),
|
|
64
|
+
analytics: true,
|
|
65
|
+
prefix: 'ratelimit:auth',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* AI/LLM endpoints rate limiter
|
|
70
|
+
* 10 requests per minute per user
|
|
71
|
+
* Prevents cost explosion
|
|
72
|
+
*/
|
|
73
|
+
export const aiRatelimit = new Ratelimit({
|
|
74
|
+
redis,
|
|
75
|
+
limiter: Ratelimit.slidingWindow(10, '1 m'),
|
|
76
|
+
analytics: true,
|
|
77
|
+
prefix: 'ratelimit:ai',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Expensive operations rate limiter
|
|
82
|
+
* 100 requests per day per user
|
|
83
|
+
* For operations like exports, reports
|
|
84
|
+
*/
|
|
85
|
+
export const expensiveRatelimit = new Ratelimit({
|
|
86
|
+
redis,
|
|
87
|
+
limiter: Ratelimit.slidingWindow(100, '1 d'),
|
|
88
|
+
analytics: true,
|
|
89
|
+
prefix: 'ratelimit:expensive',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// HELPER FUNCTIONS
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get identifier for rate limiting
|
|
98
|
+
* Uses user ID if authenticated, otherwise IP address
|
|
99
|
+
*/
|
|
100
|
+
export function getIdentifier(request: Request, userId?: string): string {
|
|
101
|
+
if (userId) {
|
|
102
|
+
return `user:${userId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get IP from headers (works with Vercel, Cloudflare, etc.)
|
|
106
|
+
const forwarded = request.headers.get('x-forwarded-for');
|
|
107
|
+
const ip = forwarded ? forwarded.split(',')[0].trim() : 'unknown';
|
|
108
|
+
|
|
109
|
+
return `ip:${ip}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check rate limit and return result
|
|
114
|
+
* Returns success, remaining requests, and reset time
|
|
115
|
+
*/
|
|
116
|
+
export async function checkRateLimit(
|
|
117
|
+
request: Request,
|
|
118
|
+
userId?: string,
|
|
119
|
+
limiter: Ratelimit = ratelimit
|
|
120
|
+
): Promise<{
|
|
121
|
+
success: boolean;
|
|
122
|
+
remaining: number;
|
|
123
|
+
reset: number;
|
|
124
|
+
limit: number;
|
|
125
|
+
}> {
|
|
126
|
+
const identifier = getIdentifier(request, userId);
|
|
127
|
+
|
|
128
|
+
const { success, remaining, reset, limit } = await limiter.limit(identifier);
|
|
129
|
+
|
|
130
|
+
return { success, remaining, reset, limit };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rate limit middleware for Next.js API routes
|
|
135
|
+
* Returns Response if rate limited, undefined if OK
|
|
136
|
+
*/
|
|
137
|
+
export async function rateLimitMiddleware(
|
|
138
|
+
request: Request,
|
|
139
|
+
userId?: string,
|
|
140
|
+
limiter: Ratelimit = ratelimit
|
|
141
|
+
): Promise<Response | undefined> {
|
|
142
|
+
const { success, remaining, reset, limit } = await checkRateLimit(
|
|
143
|
+
request,
|
|
144
|
+
userId,
|
|
145
|
+
limiter
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (!success) {
|
|
149
|
+
return new Response(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
error: 'Too many requests',
|
|
152
|
+
message: 'Please try again later',
|
|
153
|
+
retryAfter: Math.ceil((reset - Date.now()) / 1000),
|
|
154
|
+
}),
|
|
155
|
+
{
|
|
156
|
+
status: 429,
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
'X-RateLimit-Limit': limit.toString(),
|
|
160
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
161
|
+
'X-RateLimit-Reset': reset.toString(),
|
|
162
|
+
'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// =============================================================================
|
|
172
|
+
// USAGE EXAMPLES
|
|
173
|
+
// =============================================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Example: API route with rate limiting
|
|
177
|
+
*
|
|
178
|
+
* // app/api/data/route.ts
|
|
179
|
+
* import { rateLimitMiddleware } from '@/lib/ratelimit';
|
|
180
|
+
*
|
|
181
|
+
* export async function GET(request: Request) {
|
|
182
|
+
* // Check rate limit
|
|
183
|
+
* const rateLimitResponse = await rateLimitMiddleware(request);
|
|
184
|
+
* if (rateLimitResponse) return rateLimitResponse;
|
|
185
|
+
*
|
|
186
|
+
* // Your API logic here
|
|
187
|
+
* return Response.json({ data: 'Hello!' });
|
|
188
|
+
* }
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Example: Auth route with stricter limits
|
|
193
|
+
*
|
|
194
|
+
* // app/api/auth/login/route.ts
|
|
195
|
+
* import { rateLimitMiddleware, authRatelimit } from '@/lib/ratelimit';
|
|
196
|
+
*
|
|
197
|
+
* export async function POST(request: Request) {
|
|
198
|
+
* // Stricter rate limit for auth
|
|
199
|
+
* const rateLimitResponse = await rateLimitMiddleware(request, undefined, authRatelimit);
|
|
200
|
+
* if (rateLimitResponse) return rateLimitResponse;
|
|
201
|
+
*
|
|
202
|
+
* // Login logic here
|
|
203
|
+
* }
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Example: AI endpoint with user-based limits
|
|
208
|
+
*
|
|
209
|
+
* // app/api/ai/generate/route.ts
|
|
210
|
+
* import { rateLimitMiddleware, aiRatelimit } from '@/lib/ratelimit';
|
|
211
|
+
* import { auth } from '@/lib/auth';
|
|
212
|
+
*
|
|
213
|
+
* export async function POST(request: Request) {
|
|
214
|
+
* const session = await auth();
|
|
215
|
+
* if (!session?.user) {
|
|
216
|
+
* return new Response('Unauthorized', { status: 401 });
|
|
217
|
+
* }
|
|
218
|
+
*
|
|
219
|
+
* // Rate limit by user ID for AI endpoints
|
|
220
|
+
* const rateLimitResponse = await rateLimitMiddleware(
|
|
221
|
+
* request,
|
|
222
|
+
* session.user.id,
|
|
223
|
+
* aiRatelimit
|
|
224
|
+
* );
|
|
225
|
+
* if (rateLimitResponse) return rateLimitResponse;
|
|
226
|
+
*
|
|
227
|
+
* // AI logic here
|
|
228
|
+
* }
|
|
229
|
+
*/
|