@docubook/flame 1.2.0 → 1.2.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/.docu/node/preview.ts +27 -16
- package/.docu/node/security.ts +34 -4
- package/.docu/node/server.ts +7 -6
- package/package.json +4 -4
package/.docu/node/preview.ts
CHANGED
|
@@ -3,19 +3,10 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { logger } from "./logger";
|
|
4
4
|
import { DIST_DIR } from "./paths";
|
|
5
5
|
import { getContentType } from "./utils";
|
|
6
|
+
import { SECURITY_HEADERS, generateNonce, cspHeader, injectNonce } from "./security";
|
|
6
7
|
|
|
7
8
|
const PORT = process.env.PORT || "4173";
|
|
8
9
|
|
|
9
|
-
const SECURITY_HEADERS: Record<string, string> = {
|
|
10
|
-
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
|
11
|
-
"X-Frame-Options": "DENY",
|
|
12
|
-
"X-Content-Type-Options": "nosniff",
|
|
13
|
-
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
14
|
-
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
|
15
|
-
"Content-Security-Policy":
|
|
16
|
-
"default-src 'self'; script-src 'self' 'sha256-4XrQ+xtH2goQdpEv7dRmUWACF3uhF9KRPpgayFex7QU=' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
10
|
logger.buildStart();
|
|
20
11
|
|
|
21
12
|
if (!existsSync(DIST_DIR)) {
|
|
@@ -50,22 +41,42 @@ const notFoundPath = resolve(DIST_DIR, "404.html");
|
|
|
50
41
|
const server = Bun.serve({
|
|
51
42
|
port: PORT,
|
|
52
43
|
|
|
53
|
-
fetch(req) {
|
|
44
|
+
async fetch(req) {
|
|
54
45
|
const url = new URL(req.url);
|
|
55
46
|
const pathname = decodeURIComponent(url.pathname);
|
|
56
47
|
|
|
57
48
|
const filePath = resolveFile(pathname);
|
|
49
|
+
|
|
58
50
|
if (filePath) {
|
|
59
51
|
const contentType = getContentType(filePath);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
if (contentType === "text/html") {
|
|
53
|
+
const nonce = generateNonce();
|
|
54
|
+
const html = await Bun.file(filePath).text();
|
|
55
|
+
const modified = injectNonce(html, nonce);
|
|
56
|
+
return new Response(modified, {
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "text/html",
|
|
59
|
+
...SECURITY_HEADERS,
|
|
60
|
+
"Content-Security-Policy": cspHeader(nonce, true),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return new Response(Bun.file(filePath), {
|
|
65
|
+
headers: { "Content-Type": contentType },
|
|
66
|
+
});
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
if (existsSync(notFoundPath)) {
|
|
66
|
-
|
|
70
|
+
const nonce = generateNonce();
|
|
71
|
+
const html = await Bun.file(notFoundPath).text();
|
|
72
|
+
const modified = injectNonce(html, nonce);
|
|
73
|
+
return new Response(modified, {
|
|
67
74
|
status: 404,
|
|
68
|
-
headers: {
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "text/html",
|
|
77
|
+
...SECURITY_HEADERS,
|
|
78
|
+
"Content-Security-Policy": cspHeader(nonce, true),
|
|
79
|
+
},
|
|
69
80
|
});
|
|
70
81
|
}
|
|
71
82
|
|
package/.docu/node/security.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
1
3
|
export const SECURITY_HEADERS: Record<string, string> = {
|
|
2
4
|
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
|
3
5
|
"X-Frame-Options": "DENY",
|
|
@@ -10,10 +12,13 @@ export function generateNonce(): string {
|
|
|
10
12
|
return crypto.randomUUID();
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function cspHeader(nonce: string): string {
|
|
15
|
+
export function cspHeader(nonce: string, allowEval = false): string {
|
|
16
|
+
const scriptSrc = allowEval
|
|
17
|
+
? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval'`
|
|
18
|
+
: `script-src 'self' 'nonce-${nonce}'`;
|
|
14
19
|
return [
|
|
15
20
|
"default-src 'self'",
|
|
16
|
-
|
|
21
|
+
scriptSrc,
|
|
17
22
|
"style-src 'self' 'unsafe-inline'",
|
|
18
23
|
"img-src 'self' https: data:",
|
|
19
24
|
"font-src 'self' data:",
|
|
@@ -23,13 +28,38 @@ export function cspHeader(nonce: string): string {
|
|
|
23
28
|
].join("; ");
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
export function
|
|
31
|
+
export function isPathSafe(pathname: string, baseDir: string): boolean {
|
|
32
|
+
const decoded = decodeURIComponent(pathname);
|
|
33
|
+
const resolved = resolve(baseDir, decoded.slice(1));
|
|
34
|
+
const baseDirSlash = baseDir.endsWith("/") ? baseDir : baseDir + "/";
|
|
35
|
+
return resolved === baseDir || resolved.startsWith(baseDirSlash);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSlugSafe(slug: string, docsDir: string): boolean {
|
|
39
|
+
const resolved = resolve(docsDir, slug);
|
|
40
|
+
const docsDirSlash = docsDir.endsWith("/") ? docsDir : docsDir + "/";
|
|
41
|
+
return resolved === docsDir || resolved.startsWith(docsDirSlash);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function injectNonce(html: string, nonce: string): string {
|
|
45
|
+
return html.replace(/<script\b(?![^>]*\bsrc\s*=)([^>]*)>/gi, (match) => {
|
|
46
|
+
if (/nonce\s*=/i.test(match)) return match;
|
|
47
|
+
return match.replace(/>$/, ` nonce="${nonce}">`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function htmlResponse(
|
|
52
|
+
html: string,
|
|
53
|
+
nonce: string,
|
|
54
|
+
status = 200,
|
|
55
|
+
allowEval = false
|
|
56
|
+
): Response {
|
|
27
57
|
return new Response(html, {
|
|
28
58
|
status,
|
|
29
59
|
headers: {
|
|
30
60
|
"Content-Type": "text/html",
|
|
31
61
|
...SECURITY_HEADERS,
|
|
32
|
-
"Content-Security-Policy": cspHeader(nonce),
|
|
62
|
+
"Content-Security-Policy": cspHeader(nonce, allowEval),
|
|
33
63
|
},
|
|
34
64
|
});
|
|
35
65
|
}
|
package/.docu/node/server.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { buildClientBundle, computeInlineThemeCss } from "./hydrate";
|
|
|
15
15
|
import { generateSearchIndex } from "./search-indexer";
|
|
16
16
|
import { logger } from "./logger";
|
|
17
17
|
import { initSentry, captureException } from "./sentry";
|
|
18
|
-
import { SECURITY_HEADERS, generateNonce, htmlResponse } from "./security";
|
|
18
|
+
import { SECURITY_HEADERS, generateNonce, isPathSafe, isSlugSafe, htmlResponse } from "./security";
|
|
19
19
|
import { htmlShell as createHtmlShell, hmrScript } from "./html";
|
|
20
20
|
|
|
21
21
|
const docuConfig = loadDocuConfig();
|
|
@@ -95,12 +95,11 @@ function createHtmlResponse(
|
|
|
95
95
|
extraScripts: hmrScript(nonce),
|
|
96
96
|
themeCss: inlineThemeCss,
|
|
97
97
|
});
|
|
98
|
-
return htmlResponse(html, nonce, status);
|
|
98
|
+
return htmlResponse(html, nonce, status, process.env.NODE_ENV !== "production");
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
async function getDocsForSlug(slug: string) {
|
|
102
|
-
|
|
103
|
-
if (!resolved.startsWith(DOCS_DIR)) return null;
|
|
102
|
+
if (!isSlugSafe(slug, DOCS_DIR)) return null;
|
|
104
103
|
|
|
105
104
|
const paths = [
|
|
106
105
|
join(DOCS_DIR, slug, "index.mdx"),
|
|
@@ -218,9 +217,9 @@ function handleIndex(): Response {
|
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
function serveStatic(pathname: string): Response | null {
|
|
220
|
+
if (!isPathSafe(pathname, DIST_DIR)) return null;
|
|
221
221
|
const decoded = decodeURIComponent(pathname);
|
|
222
222
|
const assetPath = resolve(DIST_DIR, decoded.slice(1));
|
|
223
|
-
if (!assetPath.startsWith(DIST_DIR)) return null;
|
|
224
223
|
try {
|
|
225
224
|
const s = statSync(assetPath);
|
|
226
225
|
if (s.isFile()) {
|
|
@@ -234,7 +233,9 @@ function serveStatic(pathname: string): Response | null {
|
|
|
234
233
|
|
|
235
234
|
if (decoded.startsWith("/docs/assets/")) {
|
|
236
235
|
const docsAsset = resolve(DOCS_DIR, "assets", decoded.replace("/docs/assets/", ""));
|
|
237
|
-
|
|
236
|
+
const docsAssetsDir = resolve(DOCS_DIR, "assets");
|
|
237
|
+
const docsAssetsDirSlash = docsAssetsDir.endsWith("/") ? docsAssetsDir : docsAssetsDir + "/";
|
|
238
|
+
if (docsAsset !== docsAssetsDir && !docsAsset.startsWith(docsAssetsDirSlash)) return null;
|
|
238
239
|
try {
|
|
239
240
|
const s = statSync(docsAsset);
|
|
240
241
|
if (s.isFile()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"lucide-react": "^1.14.0",
|
|
52
52
|
"react": "^19.0.0",
|
|
53
53
|
"react-dom": "^19.0.0",
|
|
54
|
-
"@docubook/core": "^1.7.0",
|
|
55
54
|
"@docubook/mdx-content": "^3.2.1",
|
|
56
|
-
"@docubook/
|
|
57
|
-
"@docubook/themes-colors": "^0.10.
|
|
55
|
+
"@docubook/core": "^1.7.0",
|
|
56
|
+
"@docubook/themes-colors": "^0.10.1",
|
|
57
|
+
"@docubook/ui-react": "^0.1.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@sentry/bun": "^9.0.0"
|