@docubook/flame 1.3.2 → 1.3.4

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.
@@ -52,11 +52,7 @@ export class BuildPluginBuilder implements PluginBuilder {
52
52
  try {
53
53
  const result = cb(context);
54
54
  if (result) {
55
- if (Array.isArray(result)) {
56
- items.push(...result);
57
- } else {
58
- items.push(result);
59
- }
55
+ this.collectItems(items, result, "injectBody");
60
56
  }
61
57
  } catch (err) {
62
58
  throw new Error(
@@ -83,11 +79,7 @@ export class BuildPluginBuilder implements PluginBuilder {
83
79
  try {
84
80
  const result = cb(context);
85
81
  if (result) {
86
- if (Array.isArray(result)) {
87
- items.push(...result);
88
- } else {
89
- items.push(result);
90
- }
82
+ this.collectItems(items, result, "injectHead");
91
83
  }
92
84
  } catch (err) {
93
85
  throw new Error(
@@ -462,4 +454,28 @@ export class BuildPluginBuilder implements PluginBuilder {
462
454
  transformHtml(callback: (html: string, context: PageContext) => Awaitable<string>): void {
463
455
  this._transformHtml.push(callback);
464
456
  }
457
+
458
+ /**
459
+ * Collect items from a callback result, filtering only valid strings.
460
+ * Non-string items and unexpected types are logged as warnings.
461
+ */
462
+ private collectItems(items: string[], result: string | string[], hookName: string): void {
463
+ if (Array.isArray(result)) {
464
+ for (const item of result) {
465
+ if (typeof item === "string") {
466
+ items.push(item);
467
+ } else {
468
+ console.warn(
469
+ `[plugin] ${hookName} callback returned non-string item (got ${typeof item}), skipping`
470
+ );
471
+ }
472
+ }
473
+ } else if (typeof result === "string") {
474
+ items.push(result);
475
+ } else {
476
+ console.warn(
477
+ `[plugin] ${hookName} callback returned unexpected type (got ${typeof result}), expected string or string[], skipping`
478
+ );
479
+ }
480
+ }
465
481
  }
@@ -167,6 +167,9 @@ export interface PluginBuilder {
167
167
  * Register a callback that returns HTML strings to inject inside `<head>`.
168
168
  * Results from all plugins are merged and deduplicated.
169
169
  *
170
+ * ⚠️ Sanitize any user-controlled or external data before injecting.
171
+ * Plugin-provided strings are injected raw into the final HTML.
172
+ *
170
173
  * Use for: analytics snippets, meta tags, stylesheet links.
171
174
  *
172
175
  * @param callback - Returns a single HTML string or an array. Called once per page.
@@ -182,6 +185,9 @@ export interface PluginBuilder {
182
185
  * Register a callback that returns HTML strings to inject before `</body>`.
183
186
  * Results from all plugins are merged and deduplicated.
184
187
  *
188
+ * ⚠️ Sanitize any user-controlled or external data before injecting.
189
+ * Plugin-provided strings are injected raw into the final HTML.
190
+ *
185
191
  * Use for: chat widgets, live-script loaders, deferred scripts.
186
192
  *
187
193
  * @param callback - Returns a single HTML string or an array. Called once per page.
@@ -28,7 +28,8 @@ function createHtmlResponse(
28
28
  description: string,
29
29
  body: string,
30
30
  status: number,
31
- state: ServerState
31
+ state: ServerState,
32
+ depth = 0
32
33
  ): Response {
33
34
  const nonce = generateNonce();
34
35
  const favicon = state.docuConfig.meta?.favicon || "/favicon.ico";
@@ -42,6 +43,7 @@ function createHtmlResponse(
42
43
  nonce,
43
44
  extraScripts: hmrScript(nonce),
44
45
  themeCss: state.inlineThemeCss,
46
+ depth,
45
47
  });
46
48
  return htmlResponse(html, nonce, status, process.env.NODE_ENV !== "production");
47
49
  }
@@ -140,6 +142,9 @@ async function renderDocsServerPage(
140
142
 
141
143
  const body = renderToString(page);
142
144
 
145
+ // Match build.ts depth calculation: slug.split("/").length, fallback to 1 for empty
146
+ const depth = slug.length || 1;
147
+
143
148
  if (state.builder) {
144
149
  const ctx: PageContext = {
145
150
  slug: slug.join("/") || "/",
@@ -164,12 +169,13 @@ async function renderDocsServerPage(
164
169
  themeCss: state.inlineThemeCss,
165
170
  headExtra,
166
171
  bodyExtra,
172
+ depth,
167
173
  });
168
174
  html = await state.builder.runTransformHtmlChain(html, ctx);
169
175
  return htmlResponse(html, nonce, 200, process.env.NODE_ENV !== "production");
170
176
  }
171
177
 
172
- return createHtmlResponse(title, description, body, 200, state);
178
+ return createHtmlResponse(title, description, body, 200, state, depth);
173
179
  }
174
180
 
175
181
  function renderPage(
@@ -178,7 +184,8 @@ function renderPage(
178
184
  description: string,
179
185
  status: number,
180
186
  state: ServerState,
181
- props: Record<string, unknown> = {}
187
+ props: Record<string, unknown> = {},
188
+ depth = 0
182
189
  ): Response {
183
190
  const page = React.createElement(
184
191
  DocsLayout,
@@ -186,19 +193,20 @@ function renderPage(
186
193
  React.createElement(Component, props)
187
194
  );
188
195
  const body = renderToString(page);
189
- return createHtmlResponse(title, description, body, status, state);
196
+ return createHtmlResponse(title, description, body, status, state, depth);
190
197
  }
191
198
 
192
199
  export async function handleDocsIndex(state: ServerState): Promise<Response> {
193
200
  const doc = await getDocsForSlug("", state);
194
- if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404, state);
201
+ if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404, state, {}, 1);
195
202
  return renderDocsServerPage(doc, [], "/docs", state);
196
203
  }
197
204
 
198
205
  export async function handleDocsRoute(slug: string[], state: ServerState): Promise<Response> {
199
206
  const path = slug.join("/");
200
207
  const doc = await getDocsForSlug(path, state);
201
- if (!doc) return renderPage(NotFoundPage, "404 - Not Found", "", 404, state);
208
+ if (!doc)
209
+ return renderPage(NotFoundPage, "404 - Not Found", "", 404, state, {}, slug.length || 1);
202
210
  return renderDocsServerPage(doc, slug, `/docs/${path}`, state);
203
211
  }
204
212
 
@@ -214,8 +222,8 @@ export function handleIndex(state: ServerState): Response {
214
222
  );
215
223
  }
216
224
 
217
- export function handleNotFound(state: ServerState): Response {
218
- return renderPage(NotFoundPage, "404 - Not Found", "", 404, state);
225
+ export function handleNotFound(state: ServerState, depth = 0): Response {
226
+ return renderPage(NotFoundPage, "404 - Not Found", "", 404, state, {}, depth);
219
227
  }
220
228
 
221
229
  export function serveStatic(pathname: string): Response | null {
@@ -15,6 +15,7 @@ import {
15
15
  serverErrorResponse,
16
16
  type ServerState,
17
17
  } from "./server-routes";
18
+ import { SECURITY_HEADERS, cspHeader, generateNonce } from "./security";
18
19
 
19
20
  const docuConfig = loadDocuConfig();
20
21
 
@@ -107,13 +108,30 @@ const server = Bun.serve({
107
108
  hostname: server.hostname!,
108
109
  });
109
110
  if (pluginResponse) {
111
+ // Wrap plugin response with security headers.
112
+ // Plugin's own headers take precedence over defaults.
113
+ const securedHeaders = new Headers(pluginResponse.headers);
114
+ for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
115
+ if (!securedHeaders.has(key)) {
116
+ securedHeaders.set(key, value);
117
+ }
118
+ }
119
+ const contentType = securedHeaders.get("Content-Type") || "";
120
+ if (contentType.includes("text/html") && !securedHeaders.has("Content-Security-Policy")) {
121
+ securedHeaders.set("Content-Security-Policy", cspHeader(generateNonce(), true));
122
+ }
123
+ const securedResponse = new Response(pluginResponse.body, {
124
+ status: pluginResponse.status,
125
+ statusText: pluginResponse.statusText,
126
+ headers: securedHeaders,
127
+ });
110
128
  logger.request(
111
129
  req.method,
112
130
  pathname,
113
- pluginResponse.status,
131
+ securedResponse.status,
114
132
  Math.round(performance.now() - startTime)
115
133
  );
116
- return pluginResponse;
134
+ return securedResponse;
117
135
  }
118
136
  }
119
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/flame",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,9 +53,9 @@
53
53
  "react-dom": "^19.2.7",
54
54
  "unified": "^11.0.0",
55
55
  "@docubook/core": "^1.7.2",
56
- "@docubook/mdx-content": "^3.2.2",
56
+ "@docubook/themes-colors": "^0.10.2",
57
57
  "@docubook/ui-react": "^0.1.4",
58
- "@docubook/themes-colors": "^0.10.2"
58
+ "@docubook/mdx-content": "^3.2.2"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@sentry/bun": "^10.0.0"
@@ -17,7 +17,7 @@
17
17
  "actions": [
18
18
  {
19
19
  "text": "Get Started",
20
- "link": "/docs/getting-started/introduction",
20
+ "link": "/docs",
21
21
  "theme": "primary"
22
22
  },
23
23
  {