secure-coding-rules 2.0.0 → 2.0.1

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/src/prompts.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import { createInterface } from 'node:readline';
7
7
  import { existsSync, readFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
+ import { t } from './i18n.js';
9
10
 
10
11
  // ─── Readline helpers ────────────────────────────────────────────
11
12
 
@@ -24,7 +25,7 @@ function ask(question) {
24
25
 
25
26
  function printOptions(options) {
26
27
  options.forEach((opt, i) => {
27
- const marker = opt.detected ? ' (detected)' : '';
28
+ const marker = opt.detected ? ` (${t('detected')})` : '';
28
29
  console.log(` ${i + 1}) ${opt.label}${marker}`);
29
30
  });
30
31
  }
@@ -32,20 +33,18 @@ function printOptions(options) {
32
33
  async function selectOne(question, options) {
33
34
  console.log(`\n${question}`);
34
35
  printOptions(options);
35
- const answer = await ask(`\nSelect (1-${options.length}): `);
36
+ const answer = await ask(`\n${t('selectPrompt')} (1-${options.length}): `);
36
37
  const idx = parseInt(answer, 10) - 1;
37
38
  if (idx >= 0 && idx < options.length) return options[idx].value;
38
- console.log('Invalid selection, defaulting to first option.');
39
+ console.log(t('invalidSelection'));
39
40
  return options[0].value;
40
41
  }
41
42
 
42
43
  async function selectMultiple(question, options) {
43
44
  console.log(`\n${question}`);
44
45
  printOptions(options);
45
- console.log(` 0) All`);
46
- const answer = await ask(
47
- `\nSelect (comma-separated, e.g. 1,3,5 or 0 for all): `
48
- );
46
+ console.log(` 0) ${t('selectAll')}`);
47
+ const answer = await ask(`\n${t('selectPrompt')} (${t('selectAllComma')}): `);
49
48
 
50
49
  if (answer === '0' || answer.toLowerCase() === 'all') {
51
50
  return options.map((o) => o.value);
@@ -57,7 +56,7 @@ async function selectMultiple(question, options) {
57
56
  .filter((i) => i >= 0 && i < options.length);
58
57
 
59
58
  if (indices.length === 0) {
60
- console.log('No valid selection, selecting all.');
59
+ console.log(t('noValidSelection'));
61
60
  return options.map((o) => o.value);
62
61
  }
63
62
 
@@ -101,16 +100,14 @@ export const SECURITY_CATEGORIES = [
101
100
  { label: 'Frontend: CSRF Protection', value: 'csrf-protection' },
102
101
  { label: 'Frontend: CSP', value: 'csp' },
103
102
  { label: 'Frontend: Secure State Management', value: 'secure-state' },
103
+ { label: 'TypeScript Security', value: 'typescript-security' },
104
+ { label: 'Framework: React / Next.js', value: 'react-security' },
105
+ { label: 'Framework: Express / Node.js', value: 'express-security' },
106
+ { label: 'Framework: Next.js (App Router)', value: 'nextjs-security' },
104
107
  ];
105
108
 
106
109
  // ─── Project state detection ─────────────────────────────────────
107
110
 
108
- /**
109
- * Detect the current project environment:
110
- * - Which AI tool configs already exist
111
- * - What framework is being used (via package.json)
112
- * - Whether this is a new or existing project
113
- */
114
111
  export function detectProjectState(cwd) {
115
112
  const state = {
116
113
  hasPackageJson: existsSync(join(cwd, 'package.json')),
@@ -119,7 +116,6 @@ export function detectProjectState(cwd) {
119
116
  existingRules: {},
120
117
  };
121
118
 
122
- // Detect existing AI tool configs
123
119
  const toolPaths = {
124
120
  claude: 'CLAUDE.md',
125
121
  cursor: '.cursor/rules',
@@ -136,7 +132,6 @@ export function detectProjectState(cwd) {
136
132
  }
137
133
  }
138
134
 
139
- // Detect framework from package.json dependencies
140
135
  if (state.hasPackageJson) {
141
136
  try {
142
137
  const pkg = JSON.parse(
@@ -159,30 +154,27 @@ export function detectProjectState(cwd) {
159
154
  return state;
160
155
  }
161
156
 
162
- /**
163
- * Print a summary of the detected project state
164
- */
165
157
  export function printProjectStatus(state) {
166
- console.log('\n📋 Project Status:');
158
+ console.log(`\n📋 ${t('projectStatus')}`);
167
159
 
168
160
  if (state.detectedTools.length > 0) {
169
161
  const toolNames = state.detectedTools
170
- .map((t) => AI_TOOLS.find((a) => a.value === t)?.label || t)
162
+ .map((tool) => AI_TOOLS.find((a) => a.value === tool)?.label || tool)
171
163
  .join(', ');
172
- console.log(` AI tools detected: ${toolNames}`);
164
+ console.log(` ${t('toolsDetected')} ${toolNames}`);
173
165
  } else {
174
- console.log(' No AI tool configs found (new setup)');
166
+ console.log(` ${t('noToolsFound')}`);
175
167
  }
176
168
 
177
169
  if (state.detectedFramework) {
178
170
  const fwName =
179
171
  FRAMEWORKS.find((f) => f.value === state.detectedFramework)?.label ||
180
172
  state.detectedFramework;
181
- console.log(` Framework detected: ${fwName}`);
173
+ console.log(` ${t('frameworkDetected')} ${fwName}`);
182
174
  }
183
175
 
184
176
  if (!state.hasPackageJson) {
185
- console.log(' No package.json found (works fine - rules will be created in current directory)');
177
+ console.log(` ${t('noPackageJson')}`);
186
178
  }
187
179
  }
188
180
 
@@ -196,28 +188,26 @@ export async function promptUser(args) {
196
188
  const cwd = process.cwd();
197
189
  const state = detectProjectState(cwd);
198
190
 
199
- // --check flag: just show status and exit (must be before auto-mode check)
191
+ // --check flag
200
192
  if (args.includes('--check')) {
201
193
  printProjectStatus(state);
202
- return null; // Signal to index.js to exit early
194
+ return null;
203
195
  }
204
196
 
205
- // Non-interactive mode: --yes flag or non-TTY environment
197
+ // Non-interactive mode
206
198
  if (args.includes('--yes') || args.includes('-y') || !isInteractive()) {
207
199
  if (!isInteractive() && !args.includes('--yes') && !args.includes('-y')) {
208
- console.log('Non-interactive environment detected, using defaults.');
200
+ console.log(t('nonInteractive'));
209
201
  }
210
202
 
211
- // Smart defaults based on detection
212
- const tool =
213
- state.detectedTools.length === 1
214
- ? state.detectedTools[0]
215
- : state.detectedTools.includes('claude')
216
- ? 'claude'
217
- : 'claude';
203
+ const tools =
204
+ state.detectedTools.length > 0
205
+ ? state.detectedTools
206
+ : ['claude'];
218
207
 
219
208
  return {
220
- tool,
209
+ tools,
210
+ outputMode: 'inline',
221
211
  framework: state.detectedFramework || 'vanilla',
222
212
  categories: SECURITY_CATEGORIES.map((c) => c.value),
223
213
  includeFrontend: (state.detectedFramework || 'vanilla') !== 'node',
@@ -225,29 +215,34 @@ export async function promptUser(args) {
225
215
  }
226
216
 
227
217
  // Interactive mode
228
- console.log('\n🔒 Secure Coding Rules - OWASP 2025 Security Rules Generator\n');
218
+ console.log(`\n🔒 ${t('title')}\n`);
229
219
  console.log('─'.repeat(55));
230
220
  printProjectStatus(state);
231
221
 
232
- // AI tool selection - highlight detected ones
233
- const toolOptions = AI_TOOLS.map((t) => ({
234
- ...t,
235
- detected: state.detectedTools.includes(t.value),
222
+ // AI tool selection (multiple)
223
+ const toolOptions = AI_TOOLS.map((tool) => ({
224
+ ...tool,
225
+ detected: state.detectedTools.includes(tool.value),
236
226
  }));
237
227
 
238
- // If only one tool detected, suggest it first
239
- if (state.detectedTools.length === 1) {
240
- const detected = state.detectedTools[0];
241
- const idx = toolOptions.findIndex((t) => t.value === detected);
242
- if (idx > 0) {
243
- const [item] = toolOptions.splice(idx, 1);
244
- toolOptions.unshift(item);
245
- }
246
- }
228
+ const tools = await selectMultiple(t('selectTools'), toolOptions);
247
229
 
248
- const tool = await selectOne('Which AI coding tool do you use?', toolOptions);
230
+ // Output mode selection
231
+ // Only relevant for tools that support both modes (claude, copilot, agents)
232
+ const supportsDirectory = tools.some((t) =>
233
+ ['claude', 'copilot'].includes(t)
234
+ );
249
235
 
250
- // Framework selection - auto-suggest detected
236
+ let outputMode = 'inline';
237
+ if (supportsDirectory) {
238
+ const modeOptions = [
239
+ { label: `${t('outputInline')}`, value: 'inline' },
240
+ { label: `${t('outputDirectory')}`, value: 'directory' },
241
+ ];
242
+ outputMode = await selectOne(t('selectOutputMode'), modeOptions);
243
+ }
244
+
245
+ // Framework selection
251
246
  const frameworkOptions = FRAMEWORKS.map((f) => ({
252
247
  ...f,
253
248
  detected: f.value === state.detectedFramework,
@@ -262,46 +257,53 @@ export async function promptUser(args) {
262
257
  }
263
258
  }
264
259
 
265
- const framework = await selectOne(
266
- 'What is your primary framework?',
267
- frameworkOptions
268
- );
269
-
270
- // Update or fresh install message
271
- if (state.existingRules[tool]) {
272
- console.log(`\n ℹ Existing rules found - will update security section only.`);
273
- }
260
+ const framework = await selectOne(t('selectFramework'), frameworkOptions);
274
261
 
275
- const allCategories = await confirm(
276
- 'Include all OWASP 2025 security categories?'
277
- );
262
+ const allCategories = await confirm(t('includeAll'));
278
263
 
279
264
  let categories;
280
265
  if (allCategories) {
281
266
  categories = SECURITY_CATEGORIES.map((c) => c.value);
282
267
  } else {
283
- categories = await selectMultiple(
284
- 'Select security categories:',
285
- SECURITY_CATEGORIES
286
- );
268
+ categories = await selectMultiple(t('selectCategories'), SECURITY_CATEGORIES);
287
269
  }
288
270
 
289
271
  const includeFrontend =
290
272
  framework !== 'node'
291
- ? await confirm('Include frontend-specific security rules?')
273
+ ? await confirm(t('includeFrontend'))
292
274
  : false;
293
275
 
294
276
  if (includeFrontend) {
295
- const frontendCats = [
296
- 'xss-prevention',
297
- 'csrf-protection',
298
- 'csp',
299
- 'secure-state',
300
- ];
277
+ const frontendCats = ['xss-prevention', 'csrf-protection', 'csp', 'secure-state'];
301
278
  frontendCats.forEach((c) => {
302
279
  if (!categories.includes(c)) categories.push(c);
303
280
  });
304
281
  }
305
282
 
306
- return { tool, framework, categories, includeFrontend };
283
+ // Auto-include framework-specific and TypeScript rules
284
+ autoIncludeExtras(categories, framework);
285
+
286
+ return { tools, outputMode, framework, categories, includeFrontend };
287
+ }
288
+
289
+ /**
290
+ * Auto-include TypeScript and framework-specific rules based on selection
291
+ */
292
+ function autoIncludeExtras(categories, framework) {
293
+ // Always include TypeScript rules (most JS projects use TS now)
294
+ if (!categories.includes('typescript-security')) {
295
+ categories.push('typescript-security');
296
+ }
297
+
298
+ const frameworkMap = {
299
+ react: ['react-security', 'nextjs-security'],
300
+ vue: [],
301
+ node: ['express-security'],
302
+ vanilla: [],
303
+ };
304
+
305
+ const extras = frameworkMap[framework] || [];
306
+ extras.forEach((c) => {
307
+ if (!categories.includes(c)) categories.push(c);
308
+ });
307
309
  }
@@ -0,0 +1,130 @@
1
+ # Express/Node.js Security Rules
2
+
3
+ > Security rules for Express.js and Node.js server applications, covering middleware hardening, input validation, and secure error handling.
4
+
5
+ ## Rules
6
+
7
+ ### 1. Use Helmet Middleware for HTTP Security Headers
8
+ - **DO**: Install and apply `helmet()` as early as possible in the middleware chain. Fine-tune individual headers (CSP, HSTS, X-Frame-Options) based on your application's needs.
9
+ - **DON'T**: Serve responses without security headers. Never rely on the browser's default behavior to protect against clickjacking, MIME sniffing, or missing HSTS.
10
+ - **WHY**: Helmet sets critical HTTP response headers that mitigate entire classes of attacks (XSS, clickjacking, MIME confusion) with minimal configuration effort.
11
+
12
+ ### 2. Configure CORS Explicitly — Never Use Wildcard Origins
13
+ - **DO**: Set specific allowed origins in your CORS configuration. Validate the `Origin` header against a strict allowlist. Restrict `methods` and `allowedHeaders` to only what the client needs.
14
+ - **DON'T**: Use `origin: "*"` or reflect the request `Origin` header back without validation. Never combine `credentials: true` with a wildcard origin.
15
+ - **WHY**: Wildcard CORS allows any website to make cross-origin requests to your API. Combined with credentials, this lets attackers perform authenticated actions on behalf of users via malicious sites.
16
+
17
+ ### 3. Apply Rate Limiting to All Endpoints
18
+ - **DO**: Use `express-rate-limit` (or similar) globally, with stricter limits on authentication, password reset, and payment endpoints. Include `keyGenerator` based on authenticated user ID where possible.
19
+ - **DON'T**: Leave endpoints unprotected from high-volume requests. Never rely solely on IP-based rate limiting behind shared proxies without `trust proxy` configuration.
20
+ - **WHY**: Rate limiting prevents brute-force attacks, credential stuffing, and denial-of-service. Without it, attackers can enumerate users, brute-force passwords, or exhaust server resources.
21
+
22
+ ### 4. Set Body Parser Size Limits
23
+ - **DO**: Configure explicit size limits on `express.json()` and `express.urlencoded()` (e.g., `limit: "100kb"`). Set even stricter limits for file uploads using multer or busboy.
24
+ - **DON'T**: Accept unlimited request body sizes. Never use default parser settings in production without reviewing limits.
25
+ - **WHY**: Oversized payloads can exhaust server memory, cause out-of-memory crashes, or be used in denial-of-service attacks. A 10MB JSON body can block the event loop during parsing.
26
+
27
+ ### 5. Configure `trust proxy` Correctly
28
+ - **DO**: Set `trust proxy` to the exact number of proxies in front of your app (e.g., `app.set("trust proxy", 1)` for one reverse proxy). Validate your setup by logging `req.ip` in staging.
29
+ - **DON'T**: Set `trust proxy` to `true` (trusts all proxies) in production. Never leave it unconfigured when behind a load balancer.
30
+ - **WHY**: Incorrect `trust proxy` settings make `req.ip` unreliable. Attackers can spoof `X-Forwarded-For` headers to bypass IP-based rate limiting and access controls.
31
+
32
+ ### 6. Never Expose Stack Traces or Internal Details in Error Responses
33
+ - **DO**: Use a centralized error handler that returns generic error messages to clients and logs full error details (stack trace, query, context) server-side only.
34
+ - **DON'T**: Send `err.stack`, `err.message`, or database error details in API responses. Never use Express's default error handler in production.
35
+ - **WHY**: Stack traces reveal file paths, library versions, database schemas, and internal logic. Attackers use this information for targeted exploitation.
36
+
37
+ ### 7. Use Parameterized Queries for All Database Operations
38
+ - **DO**: Use parameterized queries, prepared statements, or ORM query builders (Knex, Prisma, Sequelize) for all database operations. Validate and sanitize inputs before they reach the query layer.
39
+ - **DON'T**: Concatenate or template-literal user input into SQL or NoSQL query strings. Never build MongoDB queries with unsanitized `req.body` objects.
40
+ - **WHY**: SQL and NoSQL injection remain top attack vectors. Parameterized queries separate code from data, making injection structurally impossible regardless of input content.
41
+
42
+ ## Code Examples
43
+
44
+ ### Bad Practice
45
+ ```javascript
46
+ const express = require("express");
47
+ const app = express();
48
+
49
+ // No helmet, no security headers
50
+ app.use(express.json()); // No size limit
51
+
52
+ // Wildcard CORS — any site can call this API
53
+ const cors = require("cors");
54
+ app.use(cors({ origin: "*", credentials: true }));
55
+
56
+ // Stack trace leaked in error response
57
+ app.use((err, req, res, next) => {
58
+ res.status(500).json({ error: err.message, stack: err.stack });
59
+ });
60
+
61
+ // SQL injection via string concatenation
62
+ app.get("/api/users", async (req, res) => {
63
+ const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;
64
+ const users = await db.raw(query);
65
+ res.json(users);
66
+ });
67
+
68
+ // No rate limiting on login
69
+ app.post("/api/login", async (req, res) => {
70
+ const user = await authenticate(req.body.email, req.body.password);
71
+ res.json({ token: generateToken(user) });
72
+ });
73
+ ```
74
+
75
+ ### Good Practice
76
+ ```javascript
77
+ const express = require("express");
78
+ const helmet = require("helmet");
79
+ const cors = require("cors");
80
+ const rateLimit = require("express-rate-limit");
81
+
82
+ const app = express();
83
+
84
+ // Security headers
85
+ app.use(helmet());
86
+
87
+ // Strict CORS
88
+ app.use(cors({
89
+ origin: ["https://app.example.com"],
90
+ methods: ["GET", "POST", "PUT", "DELETE"],
91
+ credentials: true,
92
+ }));
93
+
94
+ // Body size limit
95
+ app.use(express.json({ limit: "100kb" }));
96
+
97
+ // Trust proxy (behind one reverse proxy)
98
+ app.set("trust proxy", 1);
99
+
100
+ // Global rate limit
101
+ app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
102
+
103
+ // Strict rate limit on auth
104
+ const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 });
105
+ app.post("/api/login", authLimiter, async (req, res) => {
106
+ const user = await authenticate(req.body.email, req.body.password);
107
+ res.json({ token: generateToken(user) });
108
+ });
109
+
110
+ // Parameterized query (Knex example)
111
+ app.get("/api/users", async (req, res) => {
112
+ const users = await db("users").where("name", req.query.name);
113
+ res.json(users);
114
+ });
115
+
116
+ // Centralized error handler — no stack trace leakage
117
+ app.use((err, req, res, next) => {
118
+ console.error(err); // Full details server-side only
119
+ res.status(err.status || 500).json({ error: "Internal server error" });
120
+ });
121
+ ```
122
+
123
+ ## Quick Checklist
124
+ - [ ] `helmet()` is applied before all route handlers
125
+ - [ ] CORS `origin` is set to specific allowed domains, not `"*"`
126
+ - [ ] `express-rate-limit` is applied globally and with stricter limits on auth endpoints
127
+ - [ ] `express.json()` and `express.urlencoded()` have explicit `limit` set
128
+ - [ ] `trust proxy` is set to the correct number of proxies, not `true`
129
+ - [ ] Error handler returns generic messages; stack traces are logged server-side only
130
+ - [ ] All database queries use parameterized queries or ORM query builders
@@ -0,0 +1,149 @@
1
+ # Next.js Security Rules
2
+
3
+ > Security rules for Next.js applications using the App Router, covering Server Actions, middleware authentication, environment variable safety, and secure data fetching patterns.
4
+
5
+ ## Rules
6
+
7
+ ### 1. Validate All Inputs in Server Actions with Schema Validation
8
+ - **DO**: Use Zod or a similar library to validate every input in Server Actions. Treat Server Actions as public API endpoints — validate, sanitize, and authorize before processing.
9
+ - **DON'T**: Trust form data or arguments passed to Server Actions without validation. Never assume the client-side form structure limits what can be submitted.
10
+ - **WHY**: Server Actions are HTTP POST endpoints under the hood. Attackers can call them directly with arbitrary payloads, bypassing all client-side validation and form structure.
11
+
12
+ ### 2. Enforce Authentication and Authorization in `middleware.ts`
13
+ - **DO**: Use `middleware.ts` to protect routes by verifying session tokens or JWTs before the request reaches any page or API route. Define public routes explicitly via matcher config.
14
+ - **DON'T**: Rely on individual page components or layouts to check authentication. Never skip authorization checks assuming middleware already handled them.
15
+ - **WHY**: Middleware runs at the edge before any rendering occurs, providing a centralized and reliable enforcement point. Component-level checks are too late — data may already be fetched and serialized.
16
+
17
+ ### 3. Understand `NEXT_PUBLIC_` Environment Variable Exposure
18
+ - **DO**: Only prefix environment variables with `NEXT_PUBLIC_` when the value is intentionally public. Keep API keys, database URLs, and secrets without the prefix so they remain server-only.
19
+ - **DON'T**: Add `NEXT_PUBLIC_` to secrets or internal service URLs. Never store sensitive values in `NEXT_PUBLIC_` variables "for convenience" in client components.
20
+ - **WHY**: `NEXT_PUBLIC_` variables are inlined into the client-side JavaScript bundle at build time. They are visible to anyone who views your page source — this is by design and cannot be reversed.
21
+
22
+ ### 4. Validate HTTP Methods in API Routes
23
+ - **DO**: Explicitly check and handle each HTTP method in API route handlers. Use Next.js App Router's named exports (`GET`, `POST`, `PUT`, `DELETE`) to restrict allowed methods.
24
+ - **DON'T**: Handle all methods in a single catch-all handler without method validation. Never allow `GET` requests to perform state-changing operations.
25
+ - **WHY**: API routes that accept any method can be exploited via CSRF (GET requests with image tags), method confusion, or unintended side effects from HEAD/OPTIONS requests.
26
+
27
+ ### 5. Never Include Sensitive Data in ISR/SSG Pages
28
+ - **DO**: Fetch only public data during static generation (`generateStaticParams`, page-level data fetching). Load user-specific or sensitive data client-side or in server components that are not cached.
29
+ - **DON'T**: Include tokens, internal IDs, admin data, or PII in statically generated pages. Never pass sensitive data through `searchParams` that end up in cached pages.
30
+ - **WHY**: ISR/SSG pages are cached as static HTML files and served from CDN edges. Sensitive data in these pages is shared across all users and persists until the next revalidation.
31
+
32
+ ### 6. Use `next/headers` and `next/cookies` Securely
33
+ - **DO**: Set cookies with `httpOnly`, `secure`, `sameSite: "strict"`, and appropriate `path` / `maxAge`. Read cookies and headers only in server components or route handlers.
34
+ - **DON'T**: Store sensitive tokens in cookies without `httpOnly` and `secure` flags. Never trust cookie values without server-side validation.
35
+ - **WHY**: Cookies without `httpOnly` are accessible to JavaScript and vulnerable to XSS theft. Without `secure`, cookies transmit over HTTP. Without `sameSite`, cookies are vulnerable to CSRF.
36
+
37
+ ### 7. Protect Server-Only Code with `server-only` Package
38
+ - **DO**: Add `import "server-only"` at the top of files containing database queries, secrets access, or internal business logic. This causes a build error if the file is imported by a client component.
39
+ - **DON'T**: Rely on convention alone to separate server and client code. Never assume that a file without `"use client"` is safe from client bundling.
40
+ - **WHY**: Next.js's bundler may include server-side files in the client bundle if they are transitively imported by a client component. The `server-only` package provides a compile-time guarantee that prevents accidental exposure.
41
+
42
+ ## Code Examples
43
+
44
+ ### Bad Practice
45
+ ```typescript
46
+ // Server Action without validation
47
+ "use server";
48
+ async function updateProfile(formData: FormData) {
49
+ const name = formData.get("name") as string;
50
+ await db.user.update({ where: { id: userId }, data: { name } }); // No validation
51
+ }
52
+
53
+ // Secret exposed via NEXT_PUBLIC_
54
+ // .env
55
+ // NEXT_PUBLIC_DB_URL=postgresql://admin:password@db.internal:5432/prod
56
+ // NEXT_PUBLIC_API_SECRET=sk-secret-key-12345
57
+
58
+ // API route accepting any method
59
+ export async function handler(req: NextRequest) {
60
+ // GET, POST, DELETE all hit the same code path
61
+ const data = await req.json();
62
+ await db.user.delete({ where: { id: data.id } });
63
+ return Response.json({ success: true });
64
+ }
65
+
66
+ // Sensitive data in ISR page
67
+ export const revalidate = 3600;
68
+ export default async function AdminPage() {
69
+ const users = await db.user.findMany({
70
+ select: { id: true, email: true, ssn: true }, // SSN in static page!
71
+ });
72
+ return <UserTable users={users} />;
73
+ }
74
+ ```
75
+
76
+ ### Good Practice
77
+ ```typescript
78
+ // Server Action with Zod validation and authorization
79
+ "use server";
80
+ import { z } from "zod";
81
+ import { auth } from "@/lib/auth";
82
+
83
+ const UpdateProfileSchema = z.object({
84
+ name: z.string().min(1).max(100).trim(),
85
+ });
86
+
87
+ async function updateProfile(formData: FormData) {
88
+ const session = await auth();
89
+ if (!session?.user) throw new Error("Unauthorized");
90
+ const result = UpdateProfileSchema.safeParse({ name: formData.get("name") });
91
+ if (!result.success) throw new Error("Invalid input");
92
+ await db.user.update({
93
+ where: { id: session.user.id },
94
+ data: { name: result.data.name },
95
+ });
96
+ }
97
+
98
+ // middleware.ts — centralized auth
99
+ import { NextResponse } from "next/server";
100
+ import type { NextRequest } from "next/server";
101
+
102
+ const publicPaths = ["/login", "/register", "/api/health"];
103
+
104
+ export function middleware(request: NextRequest) {
105
+ if (publicPaths.some((p) => request.nextUrl.pathname.startsWith(p))) {
106
+ return NextResponse.next();
107
+ }
108
+ const token = request.cookies.get("session-token")?.value;
109
+ if (!token) return NextResponse.redirect(new URL("/login", request.url));
110
+ return NextResponse.next();
111
+ }
112
+
113
+ export const config = { matcher: ["/((?!_next/static|favicon.ico).*)"] };
114
+
115
+ // Environment variables: no NEXT_PUBLIC_ prefix for secrets
116
+ // DATABASE_URL=postgresql://... (server-only, no prefix)
117
+ // NEXT_PUBLIC_APP_URL=https://app.example.com (safe to expose)
118
+
119
+ // server-only guard
120
+ import "server-only";
121
+ import { db } from "@/lib/db";
122
+
123
+ export async function getSecretConfig() {
124
+ return db.config.findFirst(); // Build error if imported by client component
125
+ }
126
+
127
+ // Secure cookie setting
128
+ import { cookies } from "next/headers";
129
+
130
+ async function setSessionCookie(token: string) {
131
+ const cookieStore = await cookies();
132
+ cookieStore.set("session-token", token, {
133
+ httpOnly: true,
134
+ secure: true,
135
+ sameSite: "strict",
136
+ maxAge: 60 * 60 * 24, // 1 day
137
+ path: "/",
138
+ });
139
+ }
140
+ ```
141
+
142
+ ## Quick Checklist
143
+ - [ ] All Server Actions validate inputs with Zod or equivalent schema validation
144
+ - [ ] `middleware.ts` enforces authentication on all protected routes
145
+ - [ ] No secrets or internal URLs use the `NEXT_PUBLIC_` prefix
146
+ - [ ] API routes use named method exports (`GET`, `POST`) instead of catch-all handlers
147
+ - [ ] ISR/SSG pages contain only public, non-sensitive data
148
+ - [ ] Cookies are set with `httpOnly`, `secure`, and `sameSite` flags
149
+ - [ ] Server-only files use `import "server-only"` as a compile-time guard