@bouygues-telecom/staticjs 1.0.0 → 1.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.
@@ -5,6 +5,12 @@ import { loadCacheEntries } from "../../helpers/cachePages.js";
5
5
  import { CONFIG } from "./index";
6
6
  // Load cache entries using the refactored helper function
7
7
  const entries = loadCacheEntries(CONFIG.PROJECT_ROOT);
8
+ // Sanitize entry keys for Rollup: strip dynamic segments [param] and use parent folder name
9
+ // e.g., "partials/dynamic/[id]" -> "partials/dynamic"
10
+ const sanitizedEntries = Object.fromEntries(Object.entries(entries).map(([key, value]) => [
11
+ key.replace(/\/\[[^\]]+\]$/, ''),
12
+ value
13
+ ]));
8
14
  export default defineConfig(({ mode }) => {
9
15
  // Load environment variables from .env files
10
16
  // const env = loadEnv(mode, CONFIG.PROJECT_ROOT, '');
@@ -15,11 +21,22 @@ export default defineConfig(({ mode }) => {
15
21
  "@": path.resolve(CONFIG.PROJECT_ROOT, "src")
16
22
  },
17
23
  },
24
+ css: {
25
+ // Enable CSS source maps for development
26
+ devSourcemap: true,
27
+ preprocessorOptions: {
28
+ scss: {
29
+ // Use modern API and allow importing from src directory
30
+ api: "modern-compiler",
31
+ loadPaths: [path.resolve(CONFIG.PROJECT_ROOT, "src")],
32
+ },
33
+ },
34
+ },
18
35
  build: {
19
36
  outDir: path.resolve(CONFIG.PROJECT_ROOT, CONFIG.BUILD_DIR),
20
37
  emptyOutDir: false,
21
38
  rollupOptions: {
22
- input: entries,
39
+ input: sanitizedEntries,
23
40
  output: {
24
41
  entryFileNames: "[name].js",
25
42
  chunkFileNames: "assets/vendor-[hash].js",
@@ -13,7 +13,7 @@ export declare const createApp: () => Promise<Express>;
13
13
  * @returns Promise<Express> - Running Express application
14
14
  */
15
15
  export declare const startStaticJSServer: () => Promise<Express>;
16
- export { isDevelopment } from "./config/index.js";
16
+ export { CONFIG, DEFAULT_CONFIG, isDevelopment } from "./config/index.js";
17
17
  export type { ServerConfig } from "./config/index.js";
18
18
  export { initializeViteServer } from "./utils/vite.js";
19
19
  export { setupProcessHandlers, startServer } from "./utils/startup.js";
@@ -39,12 +39,17 @@ export const createApp = async () => {
39
39
  applyRateLimiting(app);
40
40
  applyParsing(app);
41
41
  applyLogging(app);
42
- // Hot reload middleware (development mode only) - MUST be before runtime
43
- applyHotReload(app);
42
+ // Hot reload static middleware MUST be applied before Vite to ensure proper serving
43
+ if (isDevelopment) {
44
+ const { hotReloadStaticMiddleware } = await import('./middleware/hotReload.js');
45
+ app.use(hotReloadStaticMiddleware);
46
+ }
44
47
  // Initialize Vite server and register JavaScript routes BEFORE runtime middleware
45
48
  if (isDevelopment) {
46
49
  await initializeViteServer(app);
47
50
  }
51
+ // Hot reload injection middleware (development mode only) - MUST be before runtime
52
+ applyHotReload(app);
48
53
  // Runtime rendering middleware (development mode only)
49
54
  // JavaScript routes are now registered before this middleware
50
55
  applyRuntime(app);
@@ -105,7 +110,7 @@ export const startStaticJSServer = async () => {
105
110
  }
106
111
  };
107
112
  // Export additional utilities for external use
108
- export { isDevelopment } from "./config/index.js";
113
+ export { CONFIG, DEFAULT_CONFIG, isDevelopment } from "./config/index.js";
109
114
  export { initializeViteServer } from "./utils/vite.js";
110
115
  export { setupProcessHandlers, startServer } from "./utils/startup.js";
111
116
  // Only start the server when this module is run directly (not when imported)
@@ -4,7 +4,11 @@
4
4
  */
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
- import { isDevelopment } from '../index.js';
7
+ import { fileURLToPath } from 'url';
8
+ import { isDevelopment } from '../config/index.js';
9
+ // ES module equivalent of __dirname
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
8
12
  // Cache for rendered pages and module cache invalidation
9
13
  let hotReloadClientScript = null;
10
14
  /**
@@ -13,25 +17,29 @@ let hotReloadClientScript = null;
13
17
  const loadHotReloadScript = () => {
14
18
  if (!hotReloadClientScript) {
15
19
  try {
16
- // Always resolve to the centralized server location
17
- // Find the project root by looking for the lib/server directory structure
18
- let projectRoot = process.cwd();
19
- while (projectRoot !== path.dirname(projectRoot)) {
20
- const libServerDir = path.join(projectRoot, 'lib/server');
21
- const packageJson = path.join(projectRoot, 'package.json');
22
- // Look for lib/server directory structure and package.json to ensure we're at the main project root
23
- if (fs.existsSync(libServerDir) && fs.existsSync(packageJson)) {
20
+ // Simplified path resolution - now that build copies static files to _build
21
+ const possiblePaths = [
22
+ // Primary: Relative to compiled middleware (_build/server/middleware -> _build/server/static)
23
+ // Works for both development and npm dependency scenarios
24
+ path.resolve(__dirname, '../static/hot-reload-client.js'),
25
+ // Fallback: Source directory (for development when running from source)
26
+ path.resolve(__dirname, '../../server/static/hot-reload-client.js'),
27
+ // Safety net: Explicit node_modules path (when used as npm dependency)
28
+ path.join(process.cwd(), 'node_modules/@bouygues-telecom/staticjs/_build/server/static/hot-reload-client.js')
29
+ ];
30
+ let scriptPath = null;
31
+ for (const possiblePath of possiblePaths) {
32
+ if (fs.existsSync(possiblePath)) {
33
+ scriptPath = possiblePath;
24
34
  break;
25
35
  }
26
- projectRoot = path.dirname(projectRoot);
27
36
  }
28
- const scriptPath = path.join(projectRoot, 'lib/server/static/hot-reload-client.js');
29
- if (fs.existsSync(scriptPath)) {
37
+ if (scriptPath) {
30
38
  hotReloadClientScript = fs.readFileSync(scriptPath, 'utf8');
31
- // Hot reload client script loaded
39
+ console.log(`[HotReload] Hot reload client script loaded from: ${scriptPath}`);
32
40
  }
33
41
  else {
34
- throw new Error(`Script not found at ${scriptPath}`);
42
+ throw new Error(`Script not found. Tried paths: ${possiblePaths.join(', ')}`);
35
43
  }
36
44
  }
37
45
  catch (error) {
@@ -51,14 +59,16 @@ export const hotReloadStaticMiddleware = (req, res, next) => {
51
59
  if (!isDevelopment) {
52
60
  return next();
53
61
  }
54
- // Serve hot reload client script
55
- if (req.path === '/hot-reload-client.js') {
62
+ // Serve hot reload client script - handle this BEFORE any other middleware
63
+ if (req.path === '/hot-reload-client.js' || req.url === '/hot-reload-client.js') {
64
+ console.log('[HotReload] Serving hot reload client script');
56
65
  const script = loadHotReloadScript();
57
- res.setHeader('Content-Type', 'application/javascript');
66
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
58
67
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
59
68
  res.setHeader('Pragma', 'no-cache');
60
69
  res.setHeader('Expires', '0');
61
- res.send(script);
70
+ res.setHeader('X-Content-Type-Options', 'nosniff');
71
+ res.status(200).send(script);
62
72
  return;
63
73
  }
64
74
  next();
@@ -127,10 +137,8 @@ export const applyHotReload = (app) => {
127
137
  // Skipping hot reload middleware in production mode
128
138
  return;
129
139
  }
130
- // Applying hot reload middleware
131
- // Apply static file serving middleware first
132
- app.use(hotReloadStaticMiddleware);
133
- // Apply injection middleware
140
+ // Applying hot reload injection middleware only
141
+ // (static middleware is applied separately before Vite)
134
142
  app.use(hotReloadInjectionMiddleware);
135
143
  };
136
144
  /**
@@ -17,6 +17,10 @@ export declare const runtimeRenderingMiddleware: (req: Request, res: Response, n
17
17
  * Register JavaScript serving middleware for each page
18
18
  */
19
19
  export declare const registerJavaScriptMiddleware: (app: Express, viteServer: ViteDevServer) => void;
20
+ /**
21
+ * Register CSS serving middleware for each page with styles
22
+ */
23
+ export declare const registerCSSMiddleware: (app: Express, viteServer: ViteDevServer) => void;
20
24
  /**
21
25
  * Apply runtime middleware to Express app (development mode only)
22
26
  */
@@ -5,7 +5,9 @@
5
5
  import { renderPageRuntime } from "../../helpers/renderPageRuntime.js";
6
6
  import { isDevelopment } from "../index.js";
7
7
  import { CONFIG } from "../config/index.js";
8
+ import { loadStylesCache } from "../../helpers/cachePages.js";
8
9
  import fs from "fs";
10
+ import path from "path";
9
11
  // Cache for rendered pages and module cache invalidation
10
12
  let pageCache = new Map();
11
13
  let lastCacheInvalidation = Date.now();
@@ -172,10 +174,133 @@ export const registerJavaScriptMiddleware = (app, viteServer) => {
172
174
  });
173
175
  }
174
176
  else if (pageName.includes('[') || pageName.includes(']')) {
175
- // Dynamic routes are handled differently - could add logging here if needed
177
+ // Dynamic routes: register JS route at parent path (e.g., partials/dynamic/[id] -> /partials/dynamic.js)
178
+ const jsRoute = `/${pageName.replace(/\/\[[^\]]+\]$/, '')}.js`;
179
+ app.get(jsRoute, async (req, res) => {
180
+ try {
181
+ const pageContent = fs.readFileSync(pagesCache[pageName], 'utf8');
182
+ const firstLine = pageContent.split('\n')[0];
183
+ if (firstLine.includes('no scripts')) {
184
+ return res.status(404).json({
185
+ success: false,
186
+ error: 'Not Found',
187
+ message: `JavaScript disabled for ${pageName}`,
188
+ });
189
+ }
190
+ const stats = fs.statSync(pagesCache[pageName]);
191
+ const fileModTime = stats.mtime.getTime();
192
+ if (fileModTime > lastCacheInvalidation && viteServer.moduleGraph) {
193
+ const moduleId = pagesCache[pageName];
194
+ const module = viteServer.moduleGraph.getModuleById(moduleId);
195
+ if (module) {
196
+ viteServer.moduleGraph.invalidateModule(module);
197
+ }
198
+ }
199
+ const transformStartTime = Date.now();
200
+ const result = await viteServer.transformRequest(`${pagesCache[pageName]}?t=${transformStartTime}`, {
201
+ ssr: false
202
+ });
203
+ if (result && result.code) {
204
+ const crypto = await import('crypto');
205
+ const codeHash = crypto.createHash('md5').update(result.code).digest('hex').slice(0, 8);
206
+ res.setHeader('Content-Type', 'application/javascript');
207
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
208
+ res.setHeader('Pragma', 'no-cache');
209
+ res.setHeader('Expires', '0');
210
+ res.setHeader('X-Timestamp', Date.now().toString());
211
+ res.setHeader('X-Code-Hash', codeHash);
212
+ res.setHeader('X-File-Modified', fileModTime.toString());
213
+ return res.send(result.code);
214
+ }
215
+ else {
216
+ return res.status(404).json({
217
+ success: false,
218
+ error: 'Not Found',
219
+ message: `No JavaScript generated for ${pageName}`,
220
+ });
221
+ }
222
+ }
223
+ catch (error) {
224
+ console.error(`Error compiling JavaScript for ${pageName}:`, error);
225
+ return res.status(500).json({
226
+ success: false,
227
+ error: 'Internal Server Error',
228
+ message: `Failed to compile JavaScript for ${pageName}`,
229
+ });
230
+ }
231
+ });
176
232
  }
177
233
  });
178
234
  };
235
+ /**
236
+ * Register CSS serving middleware for each page with styles
237
+ */
238
+ export const registerCSSMiddleware = (app, viteServer) => {
239
+ const stylesCache = loadStylesCache(CONFIG.PROJECT_ROOT);
240
+ // Register a route for each page's CSS file
241
+ Object.entries(stylesCache).forEach(([pageName, styleFiles]) => {
242
+ // Handle dynamic routes - use parent path
243
+ const cssRoute = `/${pageName.replace(/\/\[[^\]]+\]$/, "")}.css`;
244
+ app.get(cssRoute, async (req, res) => {
245
+ try {
246
+ const compiledStyles = [];
247
+ for (const styleFile of styleFiles) {
248
+ // Check file modification time for cache invalidation
249
+ const stats = fs.statSync(styleFile);
250
+ const fileModTime = stats.mtime.getTime();
251
+ if (fileModTime > lastCacheInvalidation && viteServer.moduleGraph) {
252
+ const module = viteServer.moduleGraph.getModuleById(styleFile);
253
+ if (module) {
254
+ viteServer.moduleGraph.invalidateModule(module);
255
+ }
256
+ }
257
+ // Transform the style file using Vite
258
+ const transformStartTime = Date.now();
259
+ const result = await viteServer.transformRequest(`${styleFile}?t=${transformStartTime}`, { ssr: false });
260
+ if (result && result.code) {
261
+ // Vite transforms SCSS to JS that exports CSS
262
+ // We need to extract the actual CSS content
263
+ // For SCSS files, Vite returns CSS wrapped in JS
264
+ const cssMatch = result.code.match(/__vite__css\s*=\s*"([^"]*)"/) ||
265
+ result.code.match(/export\s+default\s+"([^"]*)"/) ||
266
+ result.code.match(/"([^"]*\\n[^"]*)"/);
267
+ if (cssMatch && cssMatch[1]) {
268
+ // Unescape the CSS string
269
+ const css = cssMatch[1]
270
+ .replace(/\\n/g, "\n")
271
+ .replace(/\\"/g, '"')
272
+ .replace(/\\\\/g, "\\");
273
+ compiledStyles.push(`/* Source: ${path.basename(styleFile)} */\n${css}`);
274
+ }
275
+ }
276
+ }
277
+ if (compiledStyles.length > 0) {
278
+ const finalCss = compiledStyles.join("\n\n");
279
+ res.setHeader("Content-Type", "text/css");
280
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
281
+ res.setHeader("Pragma", "no-cache");
282
+ res.setHeader("Expires", "0");
283
+ return res.send(finalCss);
284
+ }
285
+ else {
286
+ return res.status(404).json({
287
+ success: false,
288
+ error: "Not Found",
289
+ message: `No CSS generated for ${pageName}`,
290
+ });
291
+ }
292
+ }
293
+ catch (error) {
294
+ console.error(`Error compiling CSS for ${pageName}:`, error);
295
+ return res.status(500).json({
296
+ success: false,
297
+ error: "Internal Server Error",
298
+ message: `Failed to compile CSS for ${pageName}`,
299
+ });
300
+ }
301
+ });
302
+ });
303
+ };
179
304
  /**
180
305
  * Apply runtime middleware to Express app (development mode only)
181
306
  */
@@ -10,8 +10,8 @@ import { Express } from "express";
10
10
  */
11
11
  export declare const securityMiddleware: (req: import("http").IncomingMessage, res: import("http").ServerResponse, next: (err?: unknown) => void) => void;
12
12
  /**
13
- * CORS configuration for development
14
- * Allows cross-origin requests in development mode
13
+ * CORS configuration
14
+ * Uses origin validator for secure cross-origin request handling
15
15
  */
16
16
  export declare const corsMiddleware: (req: cors.CorsRequest, res: {
17
17
  statusCode?: number | undefined;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import helmet from "helmet";
6
6
  import cors from "cors";
7
- import { isDevelopment } from "../index.js";
7
+ import { CONFIG, isDevelopment } from "../config/index.js";
8
8
  /**
9
9
  * Security headers middleware using helmet
10
10
  * Configures appropriate security headers for the application
@@ -13,7 +13,7 @@ export const securityMiddleware = helmet({
13
13
  contentSecurityPolicy: {
14
14
  directives: {
15
15
  defaultSrc: ["'self'"],
16
- styleSrc: ["'self'", "'unsafe-inline'"],
16
+ styleSrc: ["'self'", "'unsafe-inline'", "https://assets.bouyguestelecom.fr"],
17
17
  scriptSrc: ["'self'"],
18
18
  imgSrc: ["'self'", "data:", "https:"],
19
19
  },
@@ -21,19 +21,63 @@ export const securityMiddleware = helmet({
21
21
  crossOriginEmbedderPolicy: false, // Allow embedding for development
22
22
  });
23
23
  /**
24
- * CORS configuration for development
25
- * Allows cross-origin requests in development mode
24
+ * Get allowed origins for CORS
25
+ * In development: allows localhost origins if none configured
26
+ * In production: only allows explicitly configured origins
27
+ */
28
+ const getAllowedOrigins = () => {
29
+ if (CONFIG.CORS_ORIGINS.length > 0) {
30
+ return CONFIG.CORS_ORIGINS;
31
+ }
32
+ // Default localhost origins for development
33
+ if (isDevelopment) {
34
+ return [
35
+ `http://localhost:${CONFIG.PORT}`,
36
+ `http://127.0.0.1:${CONFIG.PORT}`,
37
+ 'http://localhost:3000',
38
+ 'http://127.0.0.1:3000',
39
+ ];
40
+ }
41
+ // In production with no configured origins, deny all cross-origin requests
42
+ return [];
43
+ };
44
+ /**
45
+ * CORS origin validator
46
+ * Validates request origin against allowed origins list
47
+ */
48
+ const corsOriginValidator = (origin, callback) => {
49
+ const allowedOrigins = getAllowedOrigins();
50
+ // Allow requests with no origin (same-origin, curl, etc.)
51
+ if (!origin) {
52
+ callback(null, true);
53
+ return;
54
+ }
55
+ // Check if origin is in allowed list
56
+ if (allowedOrigins.includes(origin)) {
57
+ callback(null, true);
58
+ return;
59
+ }
60
+ // In development, log rejected origins for debugging
61
+ if (isDevelopment) {
62
+ console.warn(`[CORS] Rejected origin: ${origin}. Allowed: ${allowedOrigins.join(', ') || 'none'}`);
63
+ }
64
+ callback(new Error('Not allowed by CORS'), false);
65
+ };
66
+ /**
67
+ * CORS configuration
68
+ * Uses origin validator for secure cross-origin request handling
26
69
  */
27
70
  export const corsMiddleware = cors({
28
- origin: true,
71
+ origin: corsOriginValidator,
29
72
  credentials: true,
73
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
74
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
75
+ maxAge: 86400, // 24 hours preflight cache
30
76
  });
31
77
  /**
32
78
  * Apply security middleware to Express app
33
79
  */
34
80
  export const applySecurity = (app) => {
35
81
  app.use(securityMiddleware);
36
- if (isDevelopment) {
37
- app.use(corsMiddleware);
38
- }
82
+ app.use(corsMiddleware);
39
83
  };
@@ -2,7 +2,7 @@
2
2
  * API route handlers
3
3
  * Handles health check, pages listing, and revalidate endpoints
4
4
  */
5
- import { Request, Response, Express } from "express";
5
+ import { Request, Response, NextFunction, Express } from "express";
6
6
  /**
7
7
  * Health check endpoint
8
8
  * Returns server status and basic information
@@ -13,6 +13,11 @@ export declare const healthCheck: (req: Request, res: Response) => void;
13
13
  * In development mode, uses readPages helper; in production, scans static directory
14
14
  */
15
15
  export declare const listPages: (req: Request, res: Response) => Promise<void>;
16
+ /**
17
+ * Authentication middleware for revalidate endpoint
18
+ * Requires API key in production, optional in development
19
+ */
20
+ export declare const revalidateAuth: (req: Request, res: Response, next: NextFunction) => void;
16
21
  /**
17
22
  * Revalidate endpoint with enhanced error handling and rate limiting
18
23
  */
@@ -8,6 +8,7 @@ import { readdir } from "fs/promises";
8
8
  import { extname, join } from "path";
9
9
  import { CONFIG, isDevelopment } from "../config/index.js";
10
10
  import { revalidateLimiter } from "../middleware/rateLimiting.js";
11
+ import crypto from "crypto";
11
12
  /**
12
13
  * Health check endpoint
13
14
  * Returns server status and basic information
@@ -65,6 +66,56 @@ export const listPages = async (req, res) => {
65
66
  res.status(500).json(errorResponse);
66
67
  }
67
68
  };
69
+ /**
70
+ * Authentication middleware for revalidate endpoint
71
+ * Requires API key in production, optional in development
72
+ */
73
+ export const revalidateAuth = (req, res, next) => {
74
+ const apiKey = CONFIG.REVALIDATE_API_KEY;
75
+ // In development, allow requests without API key if none is configured
76
+ if (isDevelopment && !apiKey) {
77
+ return next();
78
+ }
79
+ // In production, API key is required
80
+ if (!apiKey) {
81
+ console.error('[Security] REVALIDATE_API_KEY not configured in production');
82
+ res.status(503).json({
83
+ success: false,
84
+ error: 'Revalidation endpoint not configured',
85
+ });
86
+ return;
87
+ }
88
+ // Check for API key in Authorization header (Bearer token) or X-API-Key header
89
+ const authHeader = req.headers.authorization;
90
+ const apiKeyHeader = req.headers['x-api-key'];
91
+ let providedKey;
92
+ if (authHeader?.startsWith('Bearer ')) {
93
+ providedKey = authHeader.slice(7);
94
+ }
95
+ else if (typeof apiKeyHeader === 'string') {
96
+ providedKey = apiKeyHeader;
97
+ }
98
+ if (!providedKey) {
99
+ res.status(401).json({
100
+ success: false,
101
+ error: 'Authentication required',
102
+ message: 'Provide API key via Authorization: Bearer <key> or X-API-Key header',
103
+ });
104
+ return;
105
+ }
106
+ // Use timing-safe comparison to prevent timing attacks
107
+ const isValid = providedKey.length === apiKey.length &&
108
+ crypto.timingSafeEqual(Buffer.from(providedKey), Buffer.from(apiKey));
109
+ if (!isValid) {
110
+ console.warn('[Security] Invalid API key attempt for revalidate endpoint');
111
+ res.status(403).json({
112
+ success: false,
113
+ error: 'Invalid API key',
114
+ });
115
+ return;
116
+ }
117
+ next();
118
+ };
68
119
  /**
69
120
  * Revalidate endpoint with enhanced error handling and rate limiting
70
121
  */
@@ -120,5 +171,5 @@ async function getAvailablePages() {
120
171
  export const registerApiRoutes = (app) => {
121
172
  app.get('/health', healthCheck);
122
173
  app.get('/api/pages', listPages);
123
- app.post('/revalidate', revalidateLimiter, revalidateEndpoint);
174
+ app.post('/revalidate', revalidateLimiter, revalidateAuth, revalidateEndpoint);
124
175
  };
@@ -1,24 +1,97 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import path from "path";
5
+ import fs from "fs";
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ /**
8
+ * Maximum allowed path length to prevent buffer overflow attacks
9
+ */
10
+ const MAX_PATH_LENGTH = 256;
11
+ /**
12
+ * Strict path validation:
13
+ * - Must be a string
14
+ * - Must match safe characters only (alphanumeric, underscore, hyphen)
15
+ * - No slashes allowed (paths are relative page names, not file paths)
16
+ * - Must not be empty
17
+ * - Must not exceed max length
18
+ */
19
+ const isValidPath = (p) => {
20
+ if (typeof p !== "string")
21
+ return false;
22
+ if (p.length === 0 || p.length > MAX_PATH_LENGTH)
23
+ return false;
24
+ // Only allow alphanumeric, underscores, and hyphens - NO slashes
25
+ if (!/^[a-zA-Z0-9_-]+$/.test(p))
26
+ return false;
27
+ return true;
28
+ };
29
+ /**
30
+ * Validate that a path resolves within the expected pages directory
31
+ */
32
+ const isPathWithinPagesDir = (pageName, projectRoot) => {
33
+ const pagesDir = path.resolve(projectRoot, 'src/pages');
34
+ const resolvedPath = path.resolve(pagesDir, pageName);
35
+ // Ensure the resolved path starts with the pages directory
36
+ if (!resolvedPath.startsWith(pagesDir + path.sep)) {
37
+ return false;
38
+ }
39
+ // Verify the page directory actually exists
40
+ try {
41
+ return fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory();
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ };
47
+ /**
48
+ * Get safe environment variables for child processes
49
+ * Only passes necessary, non-sensitive variables
50
+ */
51
+ const getSafeEnv = () => {
52
+ return {
53
+ PATH: process.env.PATH,
54
+ HOME: process.env.HOME,
55
+ NODE_ENV: process.env.NODE_ENV,
56
+ // Add other safe variables as needed
57
+ };
58
+ };
6
59
  export const revalidate = (req, res) => {
7
60
  try {
8
- const paths = req?.body?.paths || [];
9
- const pathsArg = paths.length > 0 ? paths.join(" ") : "";
61
+ const rawPaths = req?.body?.paths;
62
+ const projectRoot = process.cwd();
63
+ // Validate and filter paths
64
+ const paths = Array.isArray(rawPaths)
65
+ ? rawPaths
66
+ .filter(isValidPath)
67
+ .filter((p) => isPathWithinPagesDir(p, projectRoot))
68
+ : [];
69
+ // Log any rejected paths for security monitoring
70
+ if (Array.isArray(rawPaths)) {
71
+ const rejectedPaths = rawPaths.filter((p) => !isValidPath(p) || (typeof p === 'string' && !isPathWithinPagesDir(p, projectRoot)));
72
+ if (rejectedPaths.length > 0) {
73
+ console.warn('[Security] Rejected invalid revalidation paths:', rejectedPaths);
74
+ }
75
+ }
10
76
  const cachePages = path.resolve(__dirname, "../../../helpers/cachePages.js");
11
77
  const buildHtmlConfig = path.resolve(__dirname, "../../../scripts/build-html.js");
12
- const buildCommand = `NODE_TLS_REJECT_UNAUTHORIZED=0 node ${cachePages} ${pathsArg && `${pathsArg}`} && npx tsx ${buildHtmlConfig}`;
13
- exec(buildCommand, (error, stdout, stderr) => {
78
+ execFile("node", [cachePages, ...paths], { env: getSafeEnv() }, (error, stdout, stderr) => {
14
79
  if (error) {
15
- console.error(`Exec error: ${error}`);
80
+ console.error(`Cache pages error: ${error}`);
16
81
  return;
17
82
  }
18
- if (!error) {
19
- console.log(`stdout: ${stdout}`);
83
+ console.log(`stdout: ${stdout}`);
84
+ if (stderr)
20
85
  console.error(`stderr: ${stderr}`);
21
- }
86
+ execFile("npx", ["tsx", buildHtmlConfig], { env: getSafeEnv() }, (error, stdout, stderr) => {
87
+ if (error) {
88
+ console.error(`Build HTML error: ${error}`);
89
+ return;
90
+ }
91
+ console.log(`stdout: ${stdout}`);
92
+ if (stderr)
93
+ console.error(`stderr: ${stderr}`);
94
+ });
22
95
  });
23
96
  res
24
97
  .status(200)