boltdocs 2.4.1 → 2.5.0
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/CHANGELOG.md +16 -0
- package/dist/cache-3FOEPC2P.mjs +1 -0
- package/dist/chunk-ITFGVXPE.mjs +1 -0
- package/dist/chunk-TP5KMRD3.mjs +1 -0
- package/dist/chunk-UD2LQG2M.mjs +75 -0
- package/dist/chunk-Y4RE5KI7.mjs +1 -0
- package/dist/client/index.d.mts +1225 -14
- package/dist/client/index.d.ts +1225 -14
- package/dist/client/index.js +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/client/ssr.js +1 -1
- package/dist/client/ssr.mjs +1 -1
- package/dist/node/cli-entry.js +37 -35
- package/dist/node/cli-entry.mjs +1 -1
- package/dist/node/index.d.mts +263 -1
- package/dist/node/index.d.ts +263 -1
- package/dist/node/index.js +34 -32
- package/dist/node/index.mjs +1 -1
- package/dist/package-TWC3BMZ7.mjs +1 -0
- package/dist/search-dialog-YBM4GYDR.mjs +1 -0
- package/package.json +46 -76
- package/src/client/components/primitives/breadcrumbs.tsx +10 -10
- package/src/client/components/primitives/index.ts +17 -16
- package/src/client/components/primitives/menu.tsx +11 -14
- package/src/client/components/primitives/navbar.tsx +29 -29
- package/src/client/components/primitives/navigation-menu.tsx +7 -9
- package/src/client/components/primitives/on-this-page.tsx +16 -18
- package/src/client/components/primitives/page-nav.tsx +10 -13
- package/src/client/components/primitives/search-dialog.tsx +17 -19
- package/src/client/components/primitives/sidebar.tsx +8 -10
- package/src/client/components/primitives/tabs.tsx +10 -12
- package/src/client/components/primitives/tooltip.tsx +3 -5
- package/src/client/components/ui-base/breadcrumbs.tsx +12 -15
- package/src/client/components/ui-base/copy-markdown.tsx +8 -10
- package/src/client/components/ui-base/navbar.tsx +10 -10
- package/src/client/components/ui-base/on-this-page.tsx +13 -12
- package/src/client/components/ui-base/page-nav.tsx +15 -15
- package/src/client/components/ui-base/search-dialog.tsx +13 -20
- package/src/client/components/ui-base/sidebar.tsx +9 -9
- package/src/client/components/ui-base/tabs.tsx +6 -7
- package/src/client/components/ui-base/theme-toggle.tsx +11 -11
- package/src/client/hooks/index.ts +12 -12
- package/src/client/index.ts +34 -11
- package/src/node/config.ts +48 -6
- package/src/node/errors.ts +44 -0
- package/src/node/index.ts +29 -2
- package/src/node/mdx/index.ts +9 -2
- package/src/node/plugin/index.ts +72 -4
- package/src/node/plugins/index.ts +17 -0
- package/src/node/plugins/plugin-errors.ts +62 -0
- package/src/node/plugins/plugin-lifecycle.ts +117 -0
- package/src/node/plugins/plugin-sandbox.ts +59 -0
- package/src/node/plugins/plugin-store.ts +54 -0
- package/src/node/plugins/plugin-types.ts +107 -0
- package/src/node/plugins/plugin-validator.ts +105 -0
- package/src/node/routes/parser.ts +35 -5
- package/src/node/schema/config.ts +208 -0
- package/src/node/schema/frontmatter.ts +17 -0
- package/src/node/security/constants/index.ts +10 -0
- package/src/node/security/csp.ts +31 -0
- package/src/node/security/headers.ts +27 -0
- package/src/node/utils.ts +153 -5
- package/tsup.config.ts +0 -6
- package/dist/base-ui/index.d.mts +0 -25
- package/dist/base-ui/index.d.ts +0 -25
- package/dist/base-ui/index.js +0 -1
- package/dist/base-ui/index.mjs +0 -1
- package/dist/cache-P6WK424C.mjs +0 -1
- package/dist/chunk-2DI3OGHV.mjs +0 -1
- package/dist/chunk-2Z5T6EAU.mjs +0 -1
- package/dist/chunk-64AJ5QLT.mjs +0 -1
- package/dist/chunk-DDX52BX4.mjs +0 -1
- package/dist/chunk-HRZDSFR5.mjs +0 -1
- package/dist/chunk-JD3RSDE4.mjs +0 -1
- package/dist/chunk-JZXLCA2E.mjs +0 -1
- package/dist/chunk-NBCYHLAA.mjs +0 -1
- package/dist/chunk-PPVDMDEL.mjs +0 -1
- package/dist/chunk-T3W44KWY.mjs +0 -1
- package/dist/chunk-UBE4CKOA.mjs +0 -1
- package/dist/chunk-UWT4AJTH.mjs +0 -73
- package/dist/chunk-WWJ7WKDI.mjs +0 -1
- package/dist/chunk-Y4RRHPXC.mjs +0 -1
- package/dist/client/types.d.mts +0 -3
- package/dist/client/types.d.ts +0 -3
- package/dist/client/types.js +0 -1
- package/dist/client/types.mjs +0 -0
- package/dist/copy-markdown--9yjpbyy.d.mts +0 -15
- package/dist/copy-markdown-l2MYkcG7.d.ts +0 -15
- package/dist/hooks/index.d.mts +0 -137
- package/dist/hooks/index.d.ts +0 -137
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/index.mjs +0 -1
- package/dist/integrations/index.d.mts +0 -48
- package/dist/integrations/index.d.ts +0 -48
- package/dist/integrations/index.js +0 -1
- package/dist/integrations/index.mjs +0 -1
- package/dist/link-DfBwCeZc.d.mts +0 -68
- package/dist/link-DfBwCeZc.d.ts +0 -68
- package/dist/loading-BwUos0wZ.d.mts +0 -57
- package/dist/loading-nlnUD01v.d.ts +0 -57
- package/dist/mdx/index.d.mts +0 -178
- package/dist/mdx/index.d.ts +0 -178
- package/dist/mdx/index.js +0 -1
- package/dist/mdx/index.mjs +0 -1
- package/dist/primitives/index.d.mts +0 -292
- package/dist/primitives/index.d.ts +0 -292
- package/dist/primitives/index.js +0 -1
- package/dist/primitives/index.mjs +0 -1
- package/dist/search-dialog-OONKKC5H.mjs +0 -1
- package/dist/types-opDA2E9-.d.mts +0 -394
- package/dist/types-opDA2E9-.d.ts +0 -394
- package/dist/use-routes-DNwgTRpU.d.ts +0 -29
- package/dist/use-routes-DrT80Eom.d.mts +0 -29
package/src/node/config.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import { loadConfigFromFile, type Plugin as VitePlugin } from 'vite'
|
|
4
|
+
import { BoltdocsConfigSchema } from './schema/config'
|
|
5
|
+
import { ValidationError } from './errors'
|
|
6
|
+
import type {
|
|
7
|
+
PluginLifecycleHooks,
|
|
8
|
+
PluginPermission,
|
|
9
|
+
} from './plugins/plugin-types'
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Represents a single social link in the configuration.
|
|
@@ -174,6 +180,12 @@ export interface BoltdocsPlugin {
|
|
|
174
180
|
name: string
|
|
175
181
|
/** Whether to run this plugin before or after default ones (optional) */
|
|
176
182
|
enforce?: 'pre' | 'post'
|
|
183
|
+
/** Version of the plugin (optional) */
|
|
184
|
+
version?: string
|
|
185
|
+
/** Minimum compatible Boltdocs version (optional, semver range) */
|
|
186
|
+
boltdocsVersion?: string
|
|
187
|
+
/** List of permissions this plugin requires to operate */
|
|
188
|
+
permissions?: PluginPermission[]
|
|
177
189
|
/** Optional remark plugins to add to the MDX pipeline */
|
|
178
190
|
remarkPlugins?: unknown[]
|
|
179
191
|
/** Optional rehype plugins to add to the MDX pipeline */
|
|
@@ -182,6 +194,8 @@ export interface BoltdocsPlugin {
|
|
|
182
194
|
vitePlugins?: VitePlugin[]
|
|
183
195
|
/** Optional custom React components to register in MDX. Map of Name -> Module Path. */
|
|
184
196
|
components?: Record<string, string>
|
|
197
|
+
/** Implementation of lifecycle hooks */
|
|
198
|
+
hooks?: PluginLifecycleHooks
|
|
185
199
|
}
|
|
186
200
|
|
|
187
201
|
/**
|
|
@@ -197,6 +211,18 @@ export interface BoltdocsIntegrationsConfig {
|
|
|
197
211
|
}
|
|
198
212
|
}
|
|
199
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Configuration for security-related settings.
|
|
216
|
+
*/
|
|
217
|
+
export interface BoltdocsSecurityConfig {
|
|
218
|
+
/** Map of standard security headers to override or supplement defaults */
|
|
219
|
+
headers?: Record<string, string>
|
|
220
|
+
/** Whether to enable Content Security Policy (CSP) generation (default: false) */
|
|
221
|
+
enableCSP?: boolean
|
|
222
|
+
/** Additional custom headers to inject into responses */
|
|
223
|
+
customHeaders?: Record<string, string>
|
|
224
|
+
}
|
|
225
|
+
|
|
200
226
|
/**
|
|
201
227
|
* The root configuration object for Boltdocs.
|
|
202
228
|
*/
|
|
@@ -219,6 +245,8 @@ export interface BoltdocsConfig {
|
|
|
219
245
|
integrations?: BoltdocsIntegrationsConfig
|
|
220
246
|
/** Configuration for the robots.txt file */
|
|
221
247
|
robots?: BoltdocsRobotsConfig
|
|
248
|
+
/** Security-related settings and headers */
|
|
249
|
+
security?: BoltdocsSecurityConfig
|
|
222
250
|
/** Low-level Vite configuration overrides */
|
|
223
251
|
vite?: import('vite').InlineConfig
|
|
224
252
|
}
|
|
@@ -241,6 +269,7 @@ interface RawUserConfig
|
|
|
241
269
|
Partial<BoltdocsThemeConfig> {
|
|
242
270
|
favicon?: string
|
|
243
271
|
ogImage?: string
|
|
272
|
+
security?: BoltdocsSecurityConfig
|
|
244
273
|
}
|
|
245
274
|
|
|
246
275
|
/**
|
|
@@ -297,7 +326,6 @@ export async function resolveConfig(
|
|
|
297
326
|
}
|
|
298
327
|
}
|
|
299
328
|
|
|
300
|
-
// Robust merging strategy
|
|
301
329
|
const themeConfigFromTop: BoltdocsThemeConfig = {
|
|
302
330
|
title: userConfig.title,
|
|
303
331
|
description: userConfig.description,
|
|
@@ -319,18 +347,14 @@ export async function resolveConfig(
|
|
|
319
347
|
editLink: userConfig.editLink,
|
|
320
348
|
}
|
|
321
349
|
|
|
322
|
-
// User can define properties at top level or inside themeConfig/theme
|
|
323
350
|
const userThemeConfig: BoltdocsThemeConfig = {
|
|
324
351
|
...themeConfigFromTop,
|
|
325
352
|
...(userConfig.theme || {}),
|
|
326
353
|
}
|
|
327
354
|
|
|
328
|
-
// Clean undefined properties
|
|
329
355
|
const cleanThemeConfig = Object.fromEntries(
|
|
330
356
|
Object.entries(userThemeConfig).filter(([_, v]) => v !== undefined),
|
|
331
357
|
) as BoltdocsThemeConfig
|
|
332
|
-
|
|
333
|
-
// Transform old navbar items if necessary
|
|
334
358
|
if (cleanThemeConfig.navbar) {
|
|
335
359
|
cleanThemeConfig.navbar = cleanThemeConfig.navbar.map((item: any) => ({
|
|
336
360
|
label: item.label || item.text || '',
|
|
@@ -338,7 +362,7 @@ export async function resolveConfig(
|
|
|
338
362
|
}))
|
|
339
363
|
}
|
|
340
364
|
|
|
341
|
-
|
|
365
|
+
const finalConfig: BoltdocsConfig = {
|
|
342
366
|
docsDir: path.resolve(docsDir),
|
|
343
367
|
homePage: userConfig.homePage,
|
|
344
368
|
theme: {
|
|
@@ -351,6 +375,24 @@ export async function resolveConfig(
|
|
|
351
375
|
plugins: userConfig.plugins || [],
|
|
352
376
|
integrations: userConfig.integrations,
|
|
353
377
|
robots: userConfig.robots,
|
|
378
|
+
security: userConfig.security,
|
|
354
379
|
vite: userConfig.vite,
|
|
355
380
|
}
|
|
381
|
+
|
|
382
|
+
// Validate the final configuration
|
|
383
|
+
const validation = BoltdocsConfigSchema.safeParse(finalConfig)
|
|
384
|
+
if (!validation.success) {
|
|
385
|
+
const errorMessages = validation.error.issues
|
|
386
|
+
.map((err: any) => {
|
|
387
|
+
const path = err.path.join('.')
|
|
388
|
+
return ` - ${path}: ${err.message}`
|
|
389
|
+
})
|
|
390
|
+
.join('\n')
|
|
391
|
+
|
|
392
|
+
throw new ValidationError(
|
|
393
|
+
`Invalid Boltdocs configuration:\n${errorMessages}`,
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return validation.data as BoltdocsConfig
|
|
356
398
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all security-related violations in Boltdocs.
|
|
3
|
+
*/
|
|
4
|
+
export class SecurityViolationError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'SecurityViolationError';
|
|
8
|
+
// Ensure the prototype is set correctly for instanceof checks in older TS versions
|
|
9
|
+
Object.setPrototypeOf(this, SecurityViolationError.prototype);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Specifically for directory traversal attempts and related filesystem breaches.
|
|
15
|
+
*/
|
|
16
|
+
export class PathTraversalError extends SecurityViolationError {
|
|
17
|
+
constructor(message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'PathTraversalError';
|
|
20
|
+
Object.setPrototypeOf(this, PathTraversalError.prototype);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Specifically for invalid or malicious character encoding in paths.
|
|
26
|
+
*/
|
|
27
|
+
export class EncodingSecurityError extends SecurityViolationError {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'EncodingSecurityError';
|
|
31
|
+
Object.setPrototypeOf(this, EncodingSecurityError.prototype);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Specifically for schema or size validation failures in inputs (like frontmatter).
|
|
37
|
+
*/
|
|
38
|
+
export class ValidationError extends SecurityViolationError {
|
|
39
|
+
constructor(message: string) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'ValidationError';
|
|
42
|
+
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/node/index.ts
CHANGED
|
@@ -3,9 +3,11 @@ import react from '@vitejs/plugin-react'
|
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite'
|
|
4
4
|
import { boltdocsPlugin } from './plugin/index'
|
|
5
5
|
import { boltdocsMdxPlugin } from './mdx/index'
|
|
6
|
+
import { SECURITY_HEADERS } from './security/headers'
|
|
7
|
+
import { getCSPHeader } from './security/csp'
|
|
6
8
|
import type { BoltdocsPluginOptions } from './plugin/index'
|
|
7
9
|
|
|
8
|
-
import { resolveConfig
|
|
10
|
+
import { resolveConfig } from './config'
|
|
9
11
|
|
|
10
12
|
export default async function boltdocs(
|
|
11
13
|
options?: BoltdocsPluginOptions,
|
|
@@ -31,6 +33,15 @@ export async function createViteConfig(
|
|
|
31
33
|
mode: 'development' | 'production' = 'development',
|
|
32
34
|
): Promise<InlineConfig> {
|
|
33
35
|
const config = await resolveConfig('docs', root)
|
|
36
|
+
const isProd = mode === 'production'
|
|
37
|
+
|
|
38
|
+
// Prepare security headers
|
|
39
|
+
const securityHeaders: Record<string, string> = isProd
|
|
40
|
+
? { ...SECURITY_HEADERS }
|
|
41
|
+
: {}
|
|
42
|
+
if (config.security?.enableCSP) {
|
|
43
|
+
securityHeaders['Content-Security-Policy'] = getCSPHeader(config)
|
|
44
|
+
}
|
|
34
45
|
|
|
35
46
|
const viteConfig: InlineConfig = {
|
|
36
47
|
root,
|
|
@@ -43,6 +54,20 @@ export async function createViteConfig(
|
|
|
43
54
|
homePage: config.homePage,
|
|
44
55
|
}),
|
|
45
56
|
],
|
|
57
|
+
server: {
|
|
58
|
+
headers: {
|
|
59
|
+
...securityHeaders,
|
|
60
|
+
...config.vite?.server?.headers,
|
|
61
|
+
},
|
|
62
|
+
...config.vite?.server,
|
|
63
|
+
},
|
|
64
|
+
preview: {
|
|
65
|
+
headers: {
|
|
66
|
+
...securityHeaders,
|
|
67
|
+
...config.vite?.preview?.headers,
|
|
68
|
+
},
|
|
69
|
+
...config.vite?.preview,
|
|
70
|
+
},
|
|
46
71
|
...config.vite,
|
|
47
72
|
}
|
|
48
73
|
|
|
@@ -53,5 +78,7 @@ export type { BoltdocsPluginOptions }
|
|
|
53
78
|
export { generateStaticPages } from './ssg'
|
|
54
79
|
export type { SSGOptions } from './ssg'
|
|
55
80
|
export type { RouteMeta } from './routes'
|
|
56
|
-
export type { BoltdocsConfig, BoltdocsThemeConfig } from './config'
|
|
81
|
+
export type { BoltdocsConfig, BoltdocsThemeConfig, BoltdocsPlugin } from './config'
|
|
57
82
|
export { resolveConfig, defineConfig } from './config'
|
|
83
|
+
|
|
84
|
+
export * from './plugins'
|
package/src/node/mdx/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { mdxCache, MDX_PLUGIN_VERSION } from './cache'
|
|
|
10
10
|
import { remarkShiki } from './remark-shiki'
|
|
11
11
|
import { rehypeShiki } from './rehype-shiki'
|
|
12
12
|
import { remarkCodeMeta } from './remark-code-meta'
|
|
13
|
+
import { PluginSandbox } from '../plugins'
|
|
13
14
|
|
|
14
15
|
let mdxCacheLoaded = false
|
|
15
16
|
let hits = 0
|
|
@@ -31,9 +32,15 @@ export function boltdocsMdxPlugin(
|
|
|
31
32
|
compiler = mdxPlugin,
|
|
32
33
|
): Plugin {
|
|
33
34
|
const extraRemarkPlugins =
|
|
34
|
-
config?.plugins?.flatMap((p) =>
|
|
35
|
+
config?.plugins?.flatMap((p) => {
|
|
36
|
+
const caps = PluginSandbox.getSanitizedCapabilities(p as any)
|
|
37
|
+
return caps.remarkPlugins || []
|
|
38
|
+
}) || []
|
|
35
39
|
const extraRehypePlugins =
|
|
36
|
-
config?.plugins?.flatMap((p) =>
|
|
40
|
+
config?.plugins?.flatMap((p) => {
|
|
41
|
+
const caps = PluginSandbox.getSanitizedCapabilities(p as any)
|
|
42
|
+
return caps.rehypePlugins || []
|
|
43
|
+
}) || []
|
|
37
44
|
|
|
38
45
|
const baseMdxPlugin = compiler({
|
|
39
46
|
remarkPlugins: [
|
package/src/node/plugin/index.ts
CHANGED
|
@@ -10,7 +10,15 @@ import { generateEntryCode } from './entry'
|
|
|
10
10
|
import { injectHtmlMeta, getHtmlTemplate } from './html'
|
|
11
11
|
import { generateRobotsTxt } from '../ssg/robots'
|
|
12
12
|
import { generateSearchData } from '../search'
|
|
13
|
+
import { SECURITY_HEADERS } from '../security/headers'
|
|
14
|
+
import { getCSPHeader } from '../security/csp'
|
|
13
15
|
import fs from 'fs'
|
|
16
|
+
import {
|
|
17
|
+
PluginLifecycleManager,
|
|
18
|
+
validatePlugins,
|
|
19
|
+
PluginSandbox,
|
|
20
|
+
type SecureBoltdocsPlugin,
|
|
21
|
+
} from '../plugins'
|
|
14
22
|
|
|
15
23
|
export * from './types'
|
|
16
24
|
|
|
@@ -32,9 +40,10 @@ export function boltdocsPlugin(
|
|
|
32
40
|
let config: BoltdocsConfig = passedConfig!
|
|
33
41
|
let viteConfig: ResolvedConfig
|
|
34
42
|
let isBuild = false
|
|
43
|
+
let lifecycle: PluginLifecycleManager
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
// Use a placeholder for extra plugins that will be populated once config is resolved
|
|
46
|
+
let resolvedExtraVitePlugins: Plugin[] = []
|
|
38
47
|
|
|
39
48
|
return [
|
|
40
49
|
{
|
|
@@ -54,6 +63,30 @@ export function boltdocsPlugin(
|
|
|
54
63
|
config = await resolveConfig(docsDir)
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
// --- NEW: Secure Plugin Initialization ---
|
|
67
|
+
const boltdocsVersion = (await import('../../../package.json')).version
|
|
68
|
+
const validatedPlugins = validatePlugins(
|
|
69
|
+
(config.plugins || []) as SecureBoltdocsPlugin[],
|
|
70
|
+
boltdocsVersion,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Replace config plugins with validated ones
|
|
74
|
+
config.plugins = validatedPlugins as any
|
|
75
|
+
|
|
76
|
+
// Initialize lifecycle manager
|
|
77
|
+
lifecycle = new PluginLifecycleManager(validatedPlugins, config)
|
|
78
|
+
|
|
79
|
+
// Populate validated extra Vite plugins
|
|
80
|
+
resolvedExtraVitePlugins = validatedPlugins.flatMap((p) => {
|
|
81
|
+
const caps = PluginSandbox.getSanitizedCapabilities(p)
|
|
82
|
+
return (caps.vitePlugins || []) as Plugin[]
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Run beforeBuild if building
|
|
86
|
+
if (isBuild) {
|
|
87
|
+
await lifecycle.runHook('beforeBuild')
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
return {
|
|
58
91
|
optimizeDeps: {
|
|
59
92
|
include: ['react', 'react-dom'],
|
|
@@ -74,9 +107,30 @@ export function boltdocsPlugin(
|
|
|
74
107
|
|
|
75
108
|
configResolved(resolved) {
|
|
76
109
|
viteConfig = resolved
|
|
110
|
+
lifecycle?.runHook('configResolved', config)
|
|
77
111
|
},
|
|
78
112
|
|
|
79
|
-
configureServer(server) {
|
|
113
|
+
async configureServer(server) {
|
|
114
|
+
await lifecycle?.runHook('beforeDev')
|
|
115
|
+
|
|
116
|
+
// Security: Apply hardened headers and CSP
|
|
117
|
+
server.middlewares.use((_req, res, next) => {
|
|
118
|
+
const isProd = process.env.NODE_ENV === 'production'
|
|
119
|
+
|
|
120
|
+
if (isProd) {
|
|
121
|
+
Object.entries(SECURITY_HEADERS).forEach(([header, value]) => {
|
|
122
|
+
res.setHeader(header, value)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Inject CSP if enabled in configuration
|
|
127
|
+
if (config.security?.enableCSP) {
|
|
128
|
+
res.setHeader('Content-Security-Policy', getCSPHeader(config))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
next()
|
|
132
|
+
})
|
|
133
|
+
|
|
80
134
|
// Serve robots.txt from config
|
|
81
135
|
server.middlewares.use((req, res, next) => {
|
|
82
136
|
if (req.url === '/robots.txt') {
|
|
@@ -263,6 +317,8 @@ export function boltdocsPlugin(
|
|
|
263
317
|
server.watcher.on('add', (f) => handleFileEvent(f, 'add'))
|
|
264
318
|
server.watcher.on('unlink', (f) => handleFileEvent(f, 'unlink'))
|
|
265
319
|
server.watcher.on('change', (f) => handleFileEvent(f, 'change'))
|
|
320
|
+
|
|
321
|
+
await lifecycle?.runHook('afterDev')
|
|
266
322
|
},
|
|
267
323
|
|
|
268
324
|
resolveId(id) {
|
|
@@ -368,6 +424,9 @@ export default DefaultLayout;`
|
|
|
368
424
|
|
|
369
425
|
const { flushCache } = await import('../cache')
|
|
370
426
|
await flushCache()
|
|
427
|
+
|
|
428
|
+
await lifecycle?.runHook('afterBuild')
|
|
429
|
+
await lifecycle?.runHook('buildEnd')
|
|
371
430
|
},
|
|
372
431
|
},
|
|
373
432
|
ViteImageOptimizer({
|
|
@@ -386,6 +445,15 @@ export default DefaultLayout;`
|
|
|
386
445
|
] as any,
|
|
387
446
|
},
|
|
388
447
|
}),
|
|
389
|
-
|
|
448
|
+
// Re-bind to use the lazily populated extra plugins
|
|
449
|
+
{
|
|
450
|
+
name: 'vite-plugin-boltdocs-extra-plugins',
|
|
451
|
+
async configResolved() {
|
|
452
|
+
// This is a dummy plugin to ensure the extra plugins are integrated
|
|
453
|
+
// Actually, Vite doesn't support changing the plugin list dynamically easily
|
|
454
|
+
// but we can return them in the original array if we initialize them early.
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
...(() => resolvedExtraVitePlugins)(),
|
|
390
458
|
]
|
|
391
459
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './plugin-types'
|
|
2
|
+
export * from './plugin-errors'
|
|
3
|
+
export * from './plugin-store'
|
|
4
|
+
export * from './plugin-validator'
|
|
5
|
+
export * from './plugin-sandbox'
|
|
6
|
+
export * from './plugin-lifecycle'
|
|
7
|
+
|
|
8
|
+
import type { SecureBoltdocsPlugin } from './plugin-types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utility to create a Boltdocs plugin with full type safety.
|
|
12
|
+
*/
|
|
13
|
+
export function createPlugin(
|
|
14
|
+
plugin: SecureBoltdocsPlugin,
|
|
15
|
+
): SecureBoltdocsPlugin {
|
|
16
|
+
return plugin
|
|
17
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all plugin-related errors in Boltdocs.
|
|
3
|
+
*/
|
|
4
|
+
export class PluginError extends Error {
|
|
5
|
+
public readonly pluginName: string
|
|
6
|
+
|
|
7
|
+
constructor(pluginName: string, message: string) {
|
|
8
|
+
super(`[plugin:${pluginName}] ${message}`)
|
|
9
|
+
this.name = 'PluginError'
|
|
10
|
+
this.pluginName = pluginName
|
|
11
|
+
Object.setPrototypeOf(this, PluginError.prototype)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Specifically for schema or structure validation failures.
|
|
17
|
+
*/
|
|
18
|
+
export class PluginValidationError extends PluginError {
|
|
19
|
+
constructor(pluginName: string, message: string) {
|
|
20
|
+
super(pluginName, `Validation failed: ${message}`)
|
|
21
|
+
this.name = 'PluginValidationError'
|
|
22
|
+
Object.setPrototypeOf(this, PluginValidationError.prototype)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Specifically for version mismatch or compatibility issues.
|
|
28
|
+
*/
|
|
29
|
+
export class PluginCompatibilityError extends PluginError {
|
|
30
|
+
constructor(pluginName: string, message: string) {
|
|
31
|
+
super(pluginName, `Compatibility error: ${message}`)
|
|
32
|
+
this.name = 'PluginCompatibilityError'
|
|
33
|
+
Object.setPrototypeOf(this, PluginCompatibilityError.prototype)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Specifically for attempts to use capabilities without proper permissions.
|
|
39
|
+
*/
|
|
40
|
+
export class PluginPermissionError extends PluginError {
|
|
41
|
+
constructor(pluginName: string, permission: string) {
|
|
42
|
+
super(pluginName, `Missing required permission: '${permission}'`)
|
|
43
|
+
this.name = 'PluginPermissionError'
|
|
44
|
+
Object.setPrototypeOf(this, PluginPermissionError.prototype)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Specifically for errors that occur during the execution of a lifecycle hook.
|
|
50
|
+
*/
|
|
51
|
+
export class PluginHookError extends PluginError {
|
|
52
|
+
public readonly hookName: string
|
|
53
|
+
|
|
54
|
+
constructor(pluginName: string, hookName: string, originalError: Error) {
|
|
55
|
+
super(pluginName, `Error in hook '${hookName}': ${originalError.message}`)
|
|
56
|
+
this.name = 'PluginHookError'
|
|
57
|
+
this.hookName = hookName
|
|
58
|
+
// Prepend the hook name to the stack if possible
|
|
59
|
+
this.stack = originalError.stack
|
|
60
|
+
Object.setPrototypeOf(this, PluginHookError.prototype)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { BoltdocsConfig } from '../config'
|
|
2
|
+
import { PluginHookError } from './plugin-errors'
|
|
3
|
+
import {
|
|
4
|
+
PluginLifecycleHooks,
|
|
5
|
+
SecureBoltdocsPlugin,
|
|
6
|
+
PluginContext,
|
|
7
|
+
PluginLogger,
|
|
8
|
+
} from './plugin-types'
|
|
9
|
+
import { BoltdocsPluginStore } from './plugin-store'
|
|
10
|
+
import { PluginSandbox } from './plugin-sandbox'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages the lifecycle of all loaded plugins, ensuring hooks are executed
|
|
14
|
+
* in the correct order with proper error isolation and context.
|
|
15
|
+
*/
|
|
16
|
+
export class PluginLifecycleManager {
|
|
17
|
+
private plugins: SecureBoltdocsPlugin[]
|
|
18
|
+
private config: BoltdocsConfig
|
|
19
|
+
private store: BoltdocsPluginStore
|
|
20
|
+
|
|
21
|
+
constructor(plugins: SecureBoltdocsPlugin[], config: BoltdocsConfig) {
|
|
22
|
+
this.plugins = plugins
|
|
23
|
+
this.config = config
|
|
24
|
+
this.store = new BoltdocsPluginStore()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Runs a specific hook for all plugins that implement it.
|
|
29
|
+
*/
|
|
30
|
+
public async runHook(
|
|
31
|
+
hookName: keyof PluginLifecycleHooks,
|
|
32
|
+
...args: any[]
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const sortedPlugins = this.getSortedPlugins()
|
|
35
|
+
|
|
36
|
+
for (const plugin of sortedPlugins) {
|
|
37
|
+
if (!plugin.hooks?.[hookName]) continue
|
|
38
|
+
|
|
39
|
+
const context = this.createContext(plugin)
|
|
40
|
+
const isBuildHook = hookName.toLowerCase().includes('build')
|
|
41
|
+
const isDevHook = hookName.toLowerCase().includes('dev')
|
|
42
|
+
const permission = isBuildHook
|
|
43
|
+
? 'hooks:build'
|
|
44
|
+
: isDevHook
|
|
45
|
+
? 'hooks:dev'
|
|
46
|
+
: undefined
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (permission) {
|
|
50
|
+
await PluginSandbox.executeWithIsolation(
|
|
51
|
+
plugin,
|
|
52
|
+
permission,
|
|
53
|
+
hookName,
|
|
54
|
+
() => (plugin.hooks![hookName] as any)(context, ...args),
|
|
55
|
+
)
|
|
56
|
+
} else {
|
|
57
|
+
// Hooks like configResolved might not require specific permissions or are always allowed
|
|
58
|
+
await (plugin.hooks![hookName] as any)(context, ...args)
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Isolate error: logging it but allowing other plugins to continue
|
|
62
|
+
const hookError = new PluginHookError(
|
|
63
|
+
plugin.name,
|
|
64
|
+
hookName,
|
|
65
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
66
|
+
)
|
|
67
|
+
context.logger.error(hookError)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sorts plugins based on their 'enforce' property (pre -> normal -> post).
|
|
74
|
+
*/
|
|
75
|
+
private getSortedPlugins(): SecureBoltdocsPlugin[] {
|
|
76
|
+
const pre = this.plugins.filter((p) => p.enforce === 'pre')
|
|
77
|
+
const normal = this.plugins.filter((p) => !p.enforce)
|
|
78
|
+
const post = this.plugins.filter((p) => p.enforce === 'post')
|
|
79
|
+
return [...pre, ...normal, ...post]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a specialized context for a plugin.
|
|
84
|
+
*/
|
|
85
|
+
private createContext(plugin: SecureBoltdocsPlugin): PluginContext {
|
|
86
|
+
return {
|
|
87
|
+
config: Object.freeze({ ...this.config }),
|
|
88
|
+
meta: {
|
|
89
|
+
name: plugin.name,
|
|
90
|
+
version: plugin.version,
|
|
91
|
+
boltdocsVersion: plugin.boltdocsVersion,
|
|
92
|
+
},
|
|
93
|
+
store: {
|
|
94
|
+
get: (p, k) => this.store.get(p, k),
|
|
95
|
+
set: (p, k, v) => this.store.set(p, k, v),
|
|
96
|
+
has: (p, k) => this.store.has(p, k),
|
|
97
|
+
},
|
|
98
|
+
logger: this.createLogger(plugin.name),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a namespaced logger for a plugin.
|
|
104
|
+
*/
|
|
105
|
+
private createLogger(pluginName: string): PluginLogger {
|
|
106
|
+
const prefix = `[plugin:${pluginName}]`
|
|
107
|
+
return {
|
|
108
|
+
info: (msg) => console.log(`${prefix} INFO: ${msg}`),
|
|
109
|
+
warn: (msg) => console.warn(`${prefix} WARN: ${msg}`),
|
|
110
|
+
error: (msg) => {
|
|
111
|
+
const message = msg instanceof Error ? msg.message : msg
|
|
112
|
+
console.error(`${prefix} ERROR: ${message}`)
|
|
113
|
+
},
|
|
114
|
+
debug: (msg) => console.debug(`${prefix} DEBUG: ${msg}`),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PluginPermissionError } from './plugin-errors'
|
|
2
|
+
import { hasPermission } from './plugin-validator'
|
|
3
|
+
import type { SecureBoltdocsPlugin, PluginPermission } from './plugin-types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Sandbox provides a protective layer that ensures plugins only use
|
|
7
|
+
* the capabilities they have explicitly requested permissions for.
|
|
8
|
+
*/
|
|
9
|
+
export class PluginSandbox {
|
|
10
|
+
/**
|
|
11
|
+
* Validates if a plugin has the required permission for a capability.
|
|
12
|
+
* Throws a PluginPermissionError if not granted.
|
|
13
|
+
*/
|
|
14
|
+
public static checkPermission(
|
|
15
|
+
plugin: SecureBoltdocsPlugin,
|
|
16
|
+
permission: PluginPermission
|
|
17
|
+
): void {
|
|
18
|
+
if (!hasPermission(plugin, permission)) {
|
|
19
|
+
throw new PluginPermissionError(plugin.name, permission)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filters a plugin's capabilities based on its declared permissions.
|
|
25
|
+
* This is used when aggregating plugins (remark, rehype, vite, components).
|
|
26
|
+
*/
|
|
27
|
+
public static getSanitizedCapabilities(plugin: SecureBoltdocsPlugin) {
|
|
28
|
+
return {
|
|
29
|
+
remarkPlugins: hasPermission(plugin, 'mdx:remark') ? plugin.remarkPlugins : [],
|
|
30
|
+
rehypePlugins: hasPermission(plugin, 'mdx:rehype') ? plugin.rehypePlugins : [],
|
|
31
|
+
vitePlugins: hasPermission(plugin, 'vite:config') ? plugin.vitePlugins : [],
|
|
32
|
+
components: hasPermission(plugin, 'components') ? plugin.components : {},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wraps a hook execution with permission checks and error isolation.
|
|
38
|
+
*/
|
|
39
|
+
public static async executeWithIsolation<T>(
|
|
40
|
+
plugin: SecureBoltdocsPlugin,
|
|
41
|
+
requiredPermission: PluginPermission,
|
|
42
|
+
hookName: string,
|
|
43
|
+
action: () => Promise<T> | T
|
|
44
|
+
): Promise<T | undefined> {
|
|
45
|
+
try {
|
|
46
|
+
this.checkPermission(plugin, requiredPermission)
|
|
47
|
+
return await action()
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof PluginPermissionError) {
|
|
50
|
+
// Log skip instead of failing hard for permissions in some contexts
|
|
51
|
+
console.warn(`[boltdocs] Skipping hook '${hookName}' for plugin '${plugin.name}': ${error.message}`)
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Re-throw other errors to be caught by the LifecycleManager
|
|
56
|
+
throw error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PluginStore } from './plugin-types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implementation of the shared plugin store.
|
|
5
|
+
* Uses a namespaced approach to prevent key collisions between plugins.
|
|
6
|
+
*/
|
|
7
|
+
export class BoltdocsPluginStore implements PluginStore {
|
|
8
|
+
private data = new Map<string, unknown>()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Internal helper to create a namespaced key.
|
|
12
|
+
*/
|
|
13
|
+
private getNamespaceKey(pluginName: string, key: string): string {
|
|
14
|
+
return `${pluginName}:${key}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves a value from the store. Returns a deep clone to prevent mutations (side-effects).
|
|
19
|
+
*/
|
|
20
|
+
public get<T = unknown>(pluginName: string, key: string): T | undefined {
|
|
21
|
+
const nsKey = this.getNamespaceKey(pluginName, key)
|
|
22
|
+
const value = this.data.get(nsKey)
|
|
23
|
+
|
|
24
|
+
if (value === undefined) return undefined
|
|
25
|
+
|
|
26
|
+
// For safety, return a deep clone if it's an object
|
|
27
|
+
if (typeof value === 'object' && value !== null) {
|
|
28
|
+
return JSON.parse(JSON.stringify(value)) as T
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value as T
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stores a value in the store. Key is automatically namespaced.
|
|
36
|
+
*/
|
|
37
|
+
public set(pluginName: string, key: string, value: unknown): void {
|
|
38
|
+
const nsKey = this.getNamespaceKey(pluginName, key)
|
|
39
|
+
// We also store a clone to ensure the store state is immutable from the outside
|
|
40
|
+
const storedValue = typeof value === 'object' && value !== null
|
|
41
|
+
? JSON.parse(JSON.stringify(value))
|
|
42
|
+
: value
|
|
43
|
+
|
|
44
|
+
this.data.set(nsKey, storedValue)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks for the existence of a key in the store.
|
|
49
|
+
*/
|
|
50
|
+
public has(pluginName: string, key: string): boolean {
|
|
51
|
+
const nsKey = this.getNamespaceKey(pluginName, key)
|
|
52
|
+
return this.data.has(nsKey)
|
|
53
|
+
}
|
|
54
|
+
}
|