@stati/core 1.20.1 → 1.20.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/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +9 -1
- package/dist/core/isg/deps.d.ts +9 -19
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +109 -111
- package/dist/core/isg/index.d.ts +1 -1
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +1 -1
- package/dist/core/preview.d.ts.map +1 -1
- package/dist/core/preview.js +9 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/core/utils/paths.utils.d.ts +22 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +33 -1
- package/package.json +1 -1
package/dist/core/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA6B7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAuXD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAgexF"}
|
package/dist/core/dev.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadConfig } from '../config/loader.js';
|
|
|
9
9
|
import { loadCacheManifest, saveCacheManifest, computeNavigationHash } from './isg/index.js';
|
|
10
10
|
import { loadContent } from './content.js';
|
|
11
11
|
import { buildNavigation } from './navigation.js';
|
|
12
|
-
import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, normalizePathForComparison, } from './utils/index.js';
|
|
12
|
+
import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, normalizePathForComparison, isPathWithinDirectory, } from './utils/index.js';
|
|
13
13
|
import { setEnv, getEnv } from '../env.js';
|
|
14
14
|
import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, TEMPLATE_EXTENSION, DEFAULT_OUT_DIR, } from '../constants.js';
|
|
15
15
|
/**
|
|
@@ -446,6 +446,14 @@ export async function createDevServer(options = {}) {
|
|
|
446
446
|
};
|
|
447
447
|
}
|
|
448
448
|
const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
|
|
449
|
+
// Security: Prevent path traversal attacks
|
|
450
|
+
if (!isPathWithinDirectory(outDir, originalFilePath)) {
|
|
451
|
+
return {
|
|
452
|
+
content: '403 - Forbidden',
|
|
453
|
+
mimeType: 'text/plain',
|
|
454
|
+
statusCode: 403,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
449
457
|
// Use the shared pretty URL resolver
|
|
450
458
|
const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
|
|
451
459
|
if (!found || !filePath) {
|
package/dist/core/isg/deps.d.ts
CHANGED
|
@@ -7,14 +7,18 @@ export declare class CircularDependencyError extends Error {
|
|
|
7
7
|
constructor(dependencyChain: string[], message: string);
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Tracks
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Tracks template dependencies for a given page by parsing template content.
|
|
11
|
+
* Only includes templates that are actually referenced (via include, layout, or stati.partials calls).
|
|
12
|
+
* This provides accurate dependency tracking for ISG cache invalidation.
|
|
13
|
+
*
|
|
14
|
+
* The function recursively parses the layout template and all its dependencies to build
|
|
15
|
+
* the complete dependency tree. This ensures that changes to unused partials don't
|
|
16
|
+
* trigger unnecessary page rebuilds.
|
|
13
17
|
*
|
|
14
18
|
* @param page - The page model to track dependencies for
|
|
15
19
|
* @param config - Stati configuration
|
|
16
|
-
* @returns Array of absolute paths to
|
|
17
|
-
* @throws {CircularDependencyError} When circular dependencies are detected
|
|
20
|
+
* @returns Array of absolute paths to actually-used template files (POSIX format)
|
|
21
|
+
* @throws {CircularDependencyError} When circular dependencies are detected in templates
|
|
18
22
|
*
|
|
19
23
|
* @example
|
|
20
24
|
* ```typescript
|
|
@@ -29,20 +33,6 @@ export declare class CircularDependencyError extends Error {
|
|
|
29
33
|
* ```
|
|
30
34
|
*/
|
|
31
35
|
export declare function trackTemplateDependencies(page: PageModel, config: StatiConfig): Promise<string[]>;
|
|
32
|
-
/**
|
|
33
|
-
* Finds all partial dependencies for a given page path.
|
|
34
|
-
* Searches up the directory hierarchy for _* folders containing .eta files.
|
|
35
|
-
*
|
|
36
|
-
* @param pagePath - Relative path to the page from srcDir
|
|
37
|
-
* @param config - Stati configuration
|
|
38
|
-
* @returns Array of absolute paths to partial files
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const partials = await findPartialDependencies('blog/post.md', config);
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
export declare function findPartialDependencies(pagePath: string, config: StatiConfig): Promise<string[]>;
|
|
46
36
|
/**
|
|
47
37
|
* Resolves a template name to its file path.
|
|
48
38
|
* Used for explicit layout specifications in front matter.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnE;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnE;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CA+CnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
|
package/dist/core/isg/deps.js
CHANGED
|
@@ -14,14 +14,18 @@ export class CircularDependencyError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Tracks
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Tracks template dependencies for a given page by parsing template content.
|
|
18
|
+
* Only includes templates that are actually referenced (via include, layout, or stati.partials calls).
|
|
19
|
+
* This provides accurate dependency tracking for ISG cache invalidation.
|
|
20
|
+
*
|
|
21
|
+
* The function recursively parses the layout template and all its dependencies to build
|
|
22
|
+
* the complete dependency tree. This ensures that changes to unused partials don't
|
|
23
|
+
* trigger unnecessary page rebuilds.
|
|
20
24
|
*
|
|
21
25
|
* @param page - The page model to track dependencies for
|
|
22
26
|
* @param config - Stati configuration
|
|
23
|
-
* @returns Array of absolute paths to
|
|
24
|
-
* @throws {CircularDependencyError} When circular dependencies are detected
|
|
27
|
+
* @returns Array of absolute paths to actually-used template files (POSIX format)
|
|
28
|
+
* @throws {CircularDependencyError} When circular dependencies are detected in templates
|
|
25
29
|
*
|
|
26
30
|
* @example
|
|
27
31
|
* ```typescript
|
|
@@ -45,75 +49,28 @@ export async function trackTemplateDependencies(page, config) {
|
|
|
45
49
|
const srcDir = resolveSrcDir(config);
|
|
46
50
|
const relativePath = relative(srcDir, page.sourcePath);
|
|
47
51
|
// Track dependencies with circular detection
|
|
52
|
+
// The visited set will contain all templates that are actually used (not just accessible)
|
|
48
53
|
const visited = new Set();
|
|
49
54
|
const currentPath = new Set();
|
|
50
55
|
// 1. Find the layout file that will be used for this page
|
|
51
56
|
const layoutPath = await discoverLayout(relativePath, config, page.frontMatter.layout, isCollectionIndexPage(page));
|
|
52
57
|
if (layoutPath) {
|
|
53
|
-
|
|
58
|
+
// Normalize to POSIX format for consistent manifest output across platforms
|
|
59
|
+
const absoluteLayoutPath = posix.join(srcDir.replace(/\\/g, '/'), layoutPath);
|
|
54
60
|
deps.push(absoluteLayoutPath);
|
|
55
|
-
//
|
|
56
|
-
|
|
61
|
+
// Recursively traverse the template dependency tree
|
|
62
|
+
// This populates 'visited' with all actually-used templates (not just accessible ones)
|
|
63
|
+
await collectTemplateDependencies(absoluteLayoutPath, srcDir, visited, currentPath);
|
|
57
64
|
}
|
|
58
|
-
// 2.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* @param pagePath - Relative path to the page from srcDir
|
|
68
|
-
* @param config - Stati configuration
|
|
69
|
-
* @returns Array of absolute paths to partial files
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* ```typescript
|
|
73
|
-
* const partials = await findPartialDependencies('blog/post.md', config);
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export async function findPartialDependencies(pagePath, config) {
|
|
77
|
-
// Early return if required config values are missing
|
|
78
|
-
if (!config.srcDir) {
|
|
79
|
-
console.warn('Config srcDir is missing, cannot find partial dependencies');
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
const deps = [];
|
|
83
|
-
const srcDir = resolveSrcDir(config);
|
|
84
|
-
// Get the directory of the current page
|
|
85
|
-
const pageDir = dirname(pagePath);
|
|
86
|
-
const pathSegments = pageDir === '.' ? [] : pageDir.split(/[/\\]/);
|
|
87
|
-
// Build list of directories to search (current dir up to root)
|
|
88
|
-
const dirsToSearch = [];
|
|
89
|
-
// Add directories from current up to root
|
|
90
|
-
if (pathSegments.length > 0) {
|
|
91
|
-
for (let i = pathSegments.length; i >= 0; i--) {
|
|
92
|
-
if (i === 0) {
|
|
93
|
-
dirsToSearch.push(''); // Root directory
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
dirsToSearch.push(pathSegments.slice(0, i).join('/'));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
dirsToSearch.push(''); // Root directory only
|
|
102
|
-
}
|
|
103
|
-
// Search each directory for _* folders containing .eta files
|
|
104
|
-
for (const dir of dirsToSearch) {
|
|
105
|
-
const searchDir = dir ? join(srcDir, dir) : srcDir;
|
|
106
|
-
try {
|
|
107
|
-
// Find all .eta files in _* subdirectories
|
|
108
|
-
// Use posix.join to ensure forward slashes for glob patterns
|
|
109
|
-
const normalizedSearchDir = searchDir.replace(/\\/g, '/');
|
|
110
|
-
const pattern = posix.join(normalizedSearchDir, '_*/**/*.eta');
|
|
111
|
-
const partialFiles = await glob(pattern, { absolute: true });
|
|
112
|
-
deps.push(...partialFiles);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// Continue if directory doesn't exist or can't be read
|
|
116
|
-
continue;
|
|
65
|
+
// 2. Add all actually-used templates from the visited set
|
|
66
|
+
// Filter to only include underscore directories (partials/components)
|
|
67
|
+
// and normalize paths to POSIX format
|
|
68
|
+
for (const templatePath of visited) {
|
|
69
|
+
const normalizedPath = templatePath.replace(/\\/g, '/');
|
|
70
|
+
// Only add partials (files in directories starting with underscore) - layout is already added
|
|
71
|
+
// Use strict pattern to match /_dirname/ where dirname starts with underscore
|
|
72
|
+
if (/\/_[^/]+\//.test(normalizedPath)) {
|
|
73
|
+
deps.push(normalizedPath);
|
|
117
74
|
}
|
|
118
75
|
}
|
|
119
76
|
return deps;
|
|
@@ -143,59 +100,63 @@ export async function resolveTemplatePath(layout, config) {
|
|
|
143
100
|
return null;
|
|
144
101
|
}
|
|
145
102
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
103
|
+
* Recursively collects all template dependencies by parsing template content.
|
|
104
|
+
* Only includes templates that are actually referenced (not just accessible).
|
|
105
|
+
* Uses DFS to traverse the dependency graph and detects circular references.
|
|
148
106
|
*
|
|
149
|
-
* @param templatePath - Absolute path to template file to
|
|
107
|
+
* @param templatePath - Absolute path to template file to analyze
|
|
150
108
|
* @param srcDir - Source directory for resolving relative template paths
|
|
151
|
-
* @param visited - Set
|
|
109
|
+
* @param visited - Set to track all visited templates (accumulated dependencies)
|
|
152
110
|
* @param currentPath - Set of templates in current DFS path (for cycle detection)
|
|
153
111
|
* @throws {CircularDependencyError} When a circular dependency is detected
|
|
154
112
|
*/
|
|
155
|
-
async function
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
if (currentPath.has(
|
|
162
|
-
const chain =
|
|
163
|
-
chain.push(templatePath);
|
|
113
|
+
async function collectTemplateDependencies(templatePath, srcDir, visited, currentPath) {
|
|
114
|
+
// Normalize path for consistent tracking
|
|
115
|
+
const normalizedPath = templatePath.replace(/\\/g, '/');
|
|
116
|
+
// Check for circular dependency FIRST (before visited check)
|
|
117
|
+
// A circular dependency means we're visiting a template that's already in our current DFS path
|
|
118
|
+
// This must be checked before the visited check because a path in currentPath is always in visited
|
|
119
|
+
if (currentPath.has(normalizedPath)) {
|
|
120
|
+
const chain = [...currentPath, normalizedPath];
|
|
164
121
|
throw new CircularDependencyError(chain, `Circular dependency detected in templates: ${chain.join(' -> ')}`);
|
|
165
122
|
}
|
|
123
|
+
// Skip if already processed (but not in current path - those are handled above)
|
|
124
|
+
if (visited.has(normalizedPath)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
166
127
|
// Check if template file exists
|
|
167
128
|
if (!(await pathExists(templatePath))) {
|
|
168
|
-
// Don't treat missing files as circular dependencies
|
|
169
|
-
// They will be handled by the missing file error handling
|
|
170
129
|
return;
|
|
171
130
|
}
|
|
172
|
-
// Add to
|
|
173
|
-
currentPath.add(
|
|
174
|
-
visited.add(
|
|
131
|
+
// Add to tracking sets
|
|
132
|
+
currentPath.add(normalizedPath);
|
|
133
|
+
visited.add(normalizedPath);
|
|
175
134
|
try {
|
|
176
135
|
// Read template content to find includes/extends
|
|
177
136
|
const content = await readFile(templatePath, 'utf-8');
|
|
178
137
|
if (!content) {
|
|
179
|
-
return;
|
|
138
|
+
return;
|
|
180
139
|
}
|
|
140
|
+
// Parse template to find referenced templates
|
|
181
141
|
const dependencies = await parseTemplateDependencies(content, templatePath, srcDir);
|
|
182
|
-
// Recursively
|
|
142
|
+
// Recursively collect dependencies
|
|
183
143
|
for (const depPath of dependencies) {
|
|
184
|
-
await
|
|
144
|
+
await collectTemplateDependencies(depPath, srcDir, visited, currentPath);
|
|
185
145
|
}
|
|
186
146
|
}
|
|
187
147
|
catch (error) {
|
|
188
|
-
//
|
|
189
|
-
if (error instanceof
|
|
190
|
-
|
|
148
|
+
// Re-throw circular dependency errors - these are fatal
|
|
149
|
+
if (error instanceof CircularDependencyError) {
|
|
150
|
+
throw error;
|
|
191
151
|
}
|
|
192
|
-
|
|
193
|
-
|
|
152
|
+
// Log warning but continue - don't fail the entire build for template parsing issues
|
|
153
|
+
if (error instanceof Error) {
|
|
154
|
+
console.warn(`Warning: Could not parse template ${templatePath}: ${error.message}`);
|
|
194
155
|
}
|
|
195
156
|
}
|
|
196
157
|
finally {
|
|
197
158
|
// Remove from current path when backtracking
|
|
198
|
-
currentPath.delete(
|
|
159
|
+
currentPath.delete(normalizedPath);
|
|
199
160
|
}
|
|
200
161
|
}
|
|
201
162
|
/**
|
|
@@ -210,6 +171,17 @@ async function detectCircularDependencies(templatePath, srcDir, visited, current
|
|
|
210
171
|
async function parseTemplateDependencies(content, templatePath, srcDir) {
|
|
211
172
|
const dependencies = [];
|
|
212
173
|
const templateDir = dirname(templatePath);
|
|
174
|
+
// Strip comments from template content to avoid false positives
|
|
175
|
+
// This removes:
|
|
176
|
+
// 1. Eta block comments: <% /* ... */ %> or <% /** ... */ %>
|
|
177
|
+
// 2. JavaScript single-line comments within Eta blocks: <% // ... %>
|
|
178
|
+
// 3. JavaScript block comments within Eta blocks (handles multi-line)
|
|
179
|
+
const contentWithoutComments = content
|
|
180
|
+
// Remove JS block comments (/* ... */) - handles multi-line
|
|
181
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
182
|
+
// Remove JS single-line comments (// ...) but only within Eta blocks
|
|
183
|
+
// This is tricky - we'll be conservative and just remove // comments to end of line
|
|
184
|
+
.replace(/\/\/[^\n]*/g, '');
|
|
213
185
|
// Look for Eta include patterns: <%~ include('template') %>
|
|
214
186
|
const includePatterns = [
|
|
215
187
|
/<%[~-]?\s*include\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
@@ -217,7 +189,7 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
|
|
|
217
189
|
];
|
|
218
190
|
for (const pattern of includePatterns) {
|
|
219
191
|
let match;
|
|
220
|
-
while ((match = pattern.exec(
|
|
192
|
+
while ((match = pattern.exec(contentWithoutComments)) !== null) {
|
|
221
193
|
const includePath = match[1];
|
|
222
194
|
if (includePath) {
|
|
223
195
|
const resolvedPath = await resolveTemplatePathInternal(includePath, srcDir, templateDir);
|
|
@@ -234,7 +206,7 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
|
|
|
234
206
|
];
|
|
235
207
|
for (const pattern of layoutPatterns) {
|
|
236
208
|
let match;
|
|
237
|
-
while ((match = pattern.exec(
|
|
209
|
+
while ((match = pattern.exec(contentWithoutComments)) !== null) {
|
|
238
210
|
const layoutPath = match[1];
|
|
239
211
|
if (layoutPath) {
|
|
240
212
|
const resolvedPath = await resolveTemplatePathInternal(layoutPath, srcDir, templateDir);
|
|
@@ -244,27 +216,52 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
|
|
|
244
216
|
}
|
|
245
217
|
}
|
|
246
218
|
}
|
|
247
|
-
// Look for Stati
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
/stati\.partials\
|
|
219
|
+
// Look for Stati partial patterns - both callable and non-callable
|
|
220
|
+
// Callable: stati.partials.name() or stati.partials['name']()
|
|
221
|
+
// Non-callable: stati.partials.name or stati.partials['name'] (used with <%~ %>)
|
|
222
|
+
const partialPatterns = [
|
|
223
|
+
// Callable patterns (with parentheses)
|
|
224
|
+
/stati\.partials\.(\w+)\s*\(/g, // stati.partials.header(
|
|
225
|
+
/stati\.partials\[['"`]([^'"`]+)['"`]\]\s*\(/g, // stati.partials['header'](
|
|
226
|
+
// Non-callable patterns (without parentheses, used in <%~ stati.partials.name %>)
|
|
227
|
+
// These patterns use lookaheads to distinguish between:
|
|
228
|
+
// - stati.partials.name (partial reference, should match)
|
|
229
|
+
// - stati.partials.name() (function call, should NOT match here - handled above)
|
|
230
|
+
//
|
|
231
|
+
// Pattern breakdown for dot notation: /stati\.partials\.(\w+)(?=\s*[%}\s]|$)(?!\s*\()/g
|
|
232
|
+
// - stati\.partials\. : Literal "stati.partials."
|
|
233
|
+
// - (\w+) : Capture partial name (letters, digits, underscore)
|
|
234
|
+
// - (?=\s*[%}\s]|$) : Positive lookahead - must be followed by whitespace, %, }, or end of string
|
|
235
|
+
// - (?!\s*\() : Negative lookahead - must NOT be followed by "(" (excludes function calls)
|
|
236
|
+
/stati\.partials\.(\w+)(?=\s*[%}\s]|$)(?!\s*\()/g,
|
|
237
|
+
// Pattern breakdown for bracket notation: /stati\.partials\[['"`]([^'"`]+)['"`]\](?=\s*[%}\s]|$)(?!\s*\()/g
|
|
238
|
+
// - stati\.partials\[ : Literal "stati.partials["
|
|
239
|
+
// - ['"`] : Opening quote (single, double, or backtick)
|
|
240
|
+
// - ([^'"`]+) : Capture partial name (any chars except quotes)
|
|
241
|
+
// - ['"`]\] : Closing quote and bracket
|
|
242
|
+
// - (?=\s*[%}\s]|$) : Positive lookahead - must be followed by whitespace, %, }, or end of string
|
|
243
|
+
// - (?!\s*\() : Negative lookahead - must NOT be followed by "(" (excludes function calls)
|
|
244
|
+
/stati\.partials\[['"`]([^'"`]+)['"`]\](?=\s*[%}\s]|$)(?!\s*\()/g,
|
|
253
245
|
];
|
|
254
|
-
|
|
246
|
+
// Use a Set to avoid duplicate partial names
|
|
247
|
+
const foundPartials = new Set();
|
|
248
|
+
for (const pattern of partialPatterns) {
|
|
255
249
|
let match;
|
|
256
|
-
while ((match = pattern.exec(
|
|
250
|
+
while ((match = pattern.exec(contentWithoutComments)) !== null) {
|
|
257
251
|
const partialName = match[1];
|
|
258
252
|
if (partialName) {
|
|
259
|
-
|
|
260
|
-
const partialFileName = `${partialName}${TEMPLATE_EXTENSION}`;
|
|
261
|
-
const resolvedPath = await resolveTemplatePathInternal(partialFileName, srcDir, templateDir);
|
|
262
|
-
if (resolvedPath) {
|
|
263
|
-
dependencies.push(resolvedPath);
|
|
264
|
-
}
|
|
253
|
+
foundPartials.add(partialName);
|
|
265
254
|
}
|
|
266
255
|
}
|
|
267
256
|
}
|
|
257
|
+
// Resolve each unique partial name
|
|
258
|
+
for (const partialName of foundPartials) {
|
|
259
|
+
const partialFileName = `${partialName}${TEMPLATE_EXTENSION}`;
|
|
260
|
+
const resolvedPath = await resolveTemplatePathInternal(partialFileName, srcDir, templateDir);
|
|
261
|
+
if (resolvedPath) {
|
|
262
|
+
dependencies.push(resolvedPath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
268
265
|
return dependencies;
|
|
269
266
|
}
|
|
270
267
|
/**
|
|
@@ -320,7 +317,8 @@ async function resolveTemplatePathInternal(templateRef, srcDir, currentDir) {
|
|
|
320
317
|
const pattern = posix.join(searchDir.replace(/\\/g, '/'), '_*', templateName);
|
|
321
318
|
const matches = await glob(pattern, { absolute: true });
|
|
322
319
|
if (matches.length > 0) {
|
|
323
|
-
|
|
320
|
+
// Normalize to POSIX format for consistent cross-platform path handling
|
|
321
|
+
return matches[0].replace(/\\/g, '/');
|
|
324
322
|
}
|
|
325
323
|
}
|
|
326
324
|
catch {
|
package/dist/core/isg/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export { loadCacheManifest, saveCacheManifest, createEmptyManifest } from './manifest.js';
|
|
10
10
|
export { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './builder.js';
|
|
11
11
|
export { BuildLockManager, withBuildLock } from './build-lock.js';
|
|
12
|
-
export { CircularDependencyError, trackTemplateDependencies,
|
|
12
|
+
export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath } from './deps.js';
|
|
13
13
|
export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, } from './hash.js';
|
|
14
14
|
export { getSafeCurrentTime, parseSafeDate, computeEffectiveTTL, computeNextRebuildAt, isPageFrozen, applyAgingRules, } from './ttl.js';
|
|
15
15
|
export { ISGConfigurationError, validateISGConfig, validatePageISGOverrides, extractNumericOverride, } from './validation.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrF,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrF,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGpG,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC"}
|
package/dist/core/isg/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './builder
|
|
|
13
13
|
// Build locking
|
|
14
14
|
export { BuildLockManager, withBuildLock } from './build-lock.js';
|
|
15
15
|
// Dependency tracking
|
|
16
|
-
export { CircularDependencyError, trackTemplateDependencies,
|
|
16
|
+
export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath } from './deps.js';
|
|
17
17
|
// Hash computation
|
|
18
18
|
export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, } from './hash.js';
|
|
19
19
|
// TTL and aging
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,mBAAmB,CAAC;AAW7D,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAyBD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CAiKxB"}
|
package/dist/core/preview.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { join, extname } from 'node:path';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { loadConfig } from '../config/loader.js';
|
|
5
|
-
import { resolveDevPaths, resolvePrettyUrl, createFallbackLogger, mergeServerOptions, } from './utils/index.js';
|
|
5
|
+
import { resolveDevPaths, resolvePrettyUrl, createFallbackLogger, mergeServerOptions, isPathWithinDirectory, } from './utils/index.js';
|
|
6
6
|
import { DEFAULT_PREVIEW_PORT, DEFAULT_DEV_HOST } from '../constants.js';
|
|
7
7
|
/**
|
|
8
8
|
* Loads and validates configuration for the preview server.
|
|
@@ -70,6 +70,14 @@ export async function createPreviewServer(options = {}) {
|
|
|
70
70
|
*/
|
|
71
71
|
async function serveFile(requestPath) {
|
|
72
72
|
const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
|
|
73
|
+
// Security: Prevent path traversal attacks
|
|
74
|
+
if (!isPathWithinDirectory(outDir, originalFilePath)) {
|
|
75
|
+
return {
|
|
76
|
+
content: '403 - Forbidden',
|
|
77
|
+
mimeType: 'text/plain',
|
|
78
|
+
statusCode: 403,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
73
81
|
// Use the shared pretty URL resolver
|
|
74
82
|
const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
|
|
75
83
|
if (!found || !filePath) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module core/utils
|
|
4
4
|
*/
|
|
5
5
|
export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
|
|
6
|
-
export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, } from './paths.utils.js';
|
|
6
|
+
export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
|
|
7
7
|
export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
|
|
8
8
|
export { propValue } from './template.utils.js';
|
|
9
9
|
export { slugify } from './slugify.utils.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/core/utils/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// File system utilities
|
|
6
6
|
export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
|
|
7
7
|
// Path resolution utilities
|
|
8
|
-
export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, } from './paths.utils.js';
|
|
8
|
+
export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
|
|
9
9
|
// Template discovery utilities
|
|
10
10
|
export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
|
|
11
11
|
// Template utilities
|
|
@@ -91,4 +91,26 @@ export declare function resolveStaticPath(config: StatiConfig, relativePath: str
|
|
|
91
91
|
* ```
|
|
92
92
|
*/
|
|
93
93
|
export declare function normalizePathForComparison(filePath: string, basePath?: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Validates that a resolved path is safely within a base directory.
|
|
96
|
+
* This function prevents path traversal attacks by ensuring that a target path
|
|
97
|
+
* (which may contain `..` sequences or other traversal patterns) resolves to
|
|
98
|
+
* a location within the specified base directory.
|
|
99
|
+
*
|
|
100
|
+
* @param baseDir - The base directory that the target path must stay within
|
|
101
|
+
* @param targetPath - The target path to validate (can be relative or contain `..`)
|
|
102
|
+
* @returns `true` if the resolved target path is within the base directory, `false` otherwise
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* // Safe paths
|
|
107
|
+
* isPathWithinDirectory('/app/dist', '/app/dist/index.html') // true
|
|
108
|
+
* isPathWithinDirectory('/app/dist', '/app/dist/pages/about.html') // true
|
|
109
|
+
*
|
|
110
|
+
* // Path traversal attempts (returns false)
|
|
111
|
+
* isPathWithinDirectory('/app/dist', '/app/dist/../../../etc/passwd') // false
|
|
112
|
+
* isPathWithinDirectory('/app/dist', '/../etc/passwd') // false
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export declare function isPathWithinDirectory(baseDir: string, targetPath: string): boolean;
|
|
94
116
|
//# sourceMappingURL=paths.utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/paths.utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW;;;;EAMlD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAuBtF"}
|
|
1
|
+
{"version":3,"file":"paths.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/paths.utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW;;;;EAMlD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAuBtF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAYlF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, posix } from 'node:path';
|
|
1
|
+
import { join, posix, resolve, sep } from 'node:path';
|
|
2
2
|
import { DEFAULT_SRC_DIR, DEFAULT_OUT_DIR, DEFAULT_STATIC_DIR, CACHE_DIR_NAME, } from '../../constants.js';
|
|
3
3
|
/**
|
|
4
4
|
* File system path resolution utilities for Stati core.
|
|
@@ -128,3 +128,35 @@ export function normalizePathForComparison(filePath, basePath) {
|
|
|
128
128
|
}
|
|
129
129
|
return normalized;
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Validates that a resolved path is safely within a base directory.
|
|
133
|
+
* This function prevents path traversal attacks by ensuring that a target path
|
|
134
|
+
* (which may contain `..` sequences or other traversal patterns) resolves to
|
|
135
|
+
* a location within the specified base directory.
|
|
136
|
+
*
|
|
137
|
+
* @param baseDir - The base directory that the target path must stay within
|
|
138
|
+
* @param targetPath - The target path to validate (can be relative or contain `..`)
|
|
139
|
+
* @returns `true` if the resolved target path is within the base directory, `false` otherwise
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* // Safe paths
|
|
144
|
+
* isPathWithinDirectory('/app/dist', '/app/dist/index.html') // true
|
|
145
|
+
* isPathWithinDirectory('/app/dist', '/app/dist/pages/about.html') // true
|
|
146
|
+
*
|
|
147
|
+
* // Path traversal attempts (returns false)
|
|
148
|
+
* isPathWithinDirectory('/app/dist', '/app/dist/../../../etc/passwd') // false
|
|
149
|
+
* isPathWithinDirectory('/app/dist', '/../etc/passwd') // false
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function isPathWithinDirectory(baseDir, targetPath) {
|
|
153
|
+
// Resolve both paths to absolute paths with all `..` and `.` resolved
|
|
154
|
+
const resolvedBase = resolve(baseDir);
|
|
155
|
+
const resolvedTarget = resolve(targetPath);
|
|
156
|
+
// Normalize the base directory to ensure proper prefix matching
|
|
157
|
+
// Adding sep ensures '/app/dist' doesn't match '/app/dist-other'
|
|
158
|
+
const normalizedBase = resolvedBase.endsWith(sep) ? resolvedBase : resolvedBase + sep;
|
|
159
|
+
// Check if the resolved target starts with the normalized base
|
|
160
|
+
// We also allow exact match (resolvedTarget === resolvedBase) for the root
|
|
161
|
+
return resolvedTarget === resolvedBase || resolvedTarget.startsWith(normalizedBase);
|
|
162
|
+
}
|