@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.
- package/_build/helpers/cachePages.d.ts +8 -0
- package/_build/helpers/cachePages.js +46 -0
- package/_build/helpers/createPage.d.ts +2 -1
- package/_build/helpers/createPage.js +9 -2
- package/_build/helpers/renderPageRuntime.js +16 -4
- package/_build/helpers/styleDiscovery.d.ts +13 -0
- package/_build/helpers/styleDiscovery.js +61 -0
- package/_build/scripts/build-css.d.ts +5 -0
- package/_build/scripts/build-css.js +79 -0
- package/_build/scripts/build-html.js +10 -4
- package/_build/scripts/cli.js +9 -2
- package/_build/scripts/create-static-app.js +26 -26
- package/_build/server/config/index.d.ts +3 -0
- package/_build/server/config/index.js +108 -1
- package/_build/server/config/vite.config.js +18 -1
- package/_build/server/index.d.ts +1 -1
- package/_build/server/index.js +8 -3
- package/_build/server/middleware/hotReload.js +30 -22
- package/_build/server/middleware/runtime.d.ts +4 -0
- package/_build/server/middleware/runtime.js +126 -1
- package/_build/server/middleware/security.d.ts +2 -2
- package/_build/server/middleware/security.js +52 -8
- package/_build/server/routes/api.d.ts +6 -1
- package/_build/server/routes/api.js +52 -1
- package/_build/server/scripts/revalidate.js +82 -9
- package/_build/server/static/hot-reload-client.js +362 -0
- package/_build/server/utils/vite.js +23 -2
- package/package.json +9 -2
|
@@ -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:
|
|
39
|
+
input: sanitizedEntries,
|
|
23
40
|
output: {
|
|
24
41
|
entryFileNames: "[name].js",
|
|
25
42
|
chunkFileNames: "assets/vendor-[hash].js",
|
package/_build/server/index.d.ts
CHANGED
|
@@ -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";
|
package/_build/server/index.js
CHANGED
|
@@ -39,12 +39,17 @@ export const createApp = async () => {
|
|
|
39
39
|
applyRateLimiting(app);
|
|
40
40
|
applyParsing(app);
|
|
41
41
|
applyLogging(app);
|
|
42
|
-
// Hot reload middleware
|
|
43
|
-
|
|
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 {
|
|
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
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
if (fs.existsSync(scriptPath)) {
|
|
37
|
+
if (scriptPath) {
|
|
30
38
|
hotReloadClientScript = fs.readFileSync(scriptPath, 'utf8');
|
|
31
|
-
|
|
39
|
+
console.log(`[HotReload] Hot reload client script loaded from: ${scriptPath}`);
|
|
32
40
|
}
|
|
33
41
|
else {
|
|
34
|
-
throw new Error(`Script not found
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
14
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
|
9
|
-
const
|
|
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
|
-
|
|
13
|
-
exec(buildCommand, (error, stdout, stderr) => {
|
|
78
|
+
execFile("node", [cachePages, ...paths], { env: getSafeEnv() }, (error, stdout, stderr) => {
|
|
14
79
|
if (error) {
|
|
15
|
-
console.error(`
|
|
80
|
+
console.error(`Cache pages error: ${error}`);
|
|
16
81
|
return;
|
|
17
82
|
}
|
|
18
|
-
|
|
19
|
-
|
|
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)
|