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.
Files changed (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cache-3FOEPC2P.mjs +1 -0
  3. package/dist/chunk-ITFGVXPE.mjs +1 -0
  4. package/dist/chunk-TP5KMRD3.mjs +1 -0
  5. package/dist/chunk-UD2LQG2M.mjs +75 -0
  6. package/dist/chunk-Y4RE5KI7.mjs +1 -0
  7. package/dist/client/index.d.mts +1225 -14
  8. package/dist/client/index.d.ts +1225 -14
  9. package/dist/client/index.js +1 -1
  10. package/dist/client/index.mjs +1 -1
  11. package/dist/client/ssr.js +1 -1
  12. package/dist/client/ssr.mjs +1 -1
  13. package/dist/node/cli-entry.js +37 -35
  14. package/dist/node/cli-entry.mjs +1 -1
  15. package/dist/node/index.d.mts +263 -1
  16. package/dist/node/index.d.ts +263 -1
  17. package/dist/node/index.js +34 -32
  18. package/dist/node/index.mjs +1 -1
  19. package/dist/package-TWC3BMZ7.mjs +1 -0
  20. package/dist/search-dialog-YBM4GYDR.mjs +1 -0
  21. package/package.json +46 -76
  22. package/src/client/components/primitives/breadcrumbs.tsx +10 -10
  23. package/src/client/components/primitives/index.ts +17 -16
  24. package/src/client/components/primitives/menu.tsx +11 -14
  25. package/src/client/components/primitives/navbar.tsx +29 -29
  26. package/src/client/components/primitives/navigation-menu.tsx +7 -9
  27. package/src/client/components/primitives/on-this-page.tsx +16 -18
  28. package/src/client/components/primitives/page-nav.tsx +10 -13
  29. package/src/client/components/primitives/search-dialog.tsx +17 -19
  30. package/src/client/components/primitives/sidebar.tsx +8 -10
  31. package/src/client/components/primitives/tabs.tsx +10 -12
  32. package/src/client/components/primitives/tooltip.tsx +3 -5
  33. package/src/client/components/ui-base/breadcrumbs.tsx +12 -15
  34. package/src/client/components/ui-base/copy-markdown.tsx +8 -10
  35. package/src/client/components/ui-base/navbar.tsx +10 -10
  36. package/src/client/components/ui-base/on-this-page.tsx +13 -12
  37. package/src/client/components/ui-base/page-nav.tsx +15 -15
  38. package/src/client/components/ui-base/search-dialog.tsx +13 -20
  39. package/src/client/components/ui-base/sidebar.tsx +9 -9
  40. package/src/client/components/ui-base/tabs.tsx +6 -7
  41. package/src/client/components/ui-base/theme-toggle.tsx +11 -11
  42. package/src/client/hooks/index.ts +12 -12
  43. package/src/client/index.ts +34 -11
  44. package/src/node/config.ts +48 -6
  45. package/src/node/errors.ts +44 -0
  46. package/src/node/index.ts +29 -2
  47. package/src/node/mdx/index.ts +9 -2
  48. package/src/node/plugin/index.ts +72 -4
  49. package/src/node/plugins/index.ts +17 -0
  50. package/src/node/plugins/plugin-errors.ts +62 -0
  51. package/src/node/plugins/plugin-lifecycle.ts +117 -0
  52. package/src/node/plugins/plugin-sandbox.ts +59 -0
  53. package/src/node/plugins/plugin-store.ts +54 -0
  54. package/src/node/plugins/plugin-types.ts +107 -0
  55. package/src/node/plugins/plugin-validator.ts +105 -0
  56. package/src/node/routes/parser.ts +35 -5
  57. package/src/node/schema/config.ts +208 -0
  58. package/src/node/schema/frontmatter.ts +17 -0
  59. package/src/node/security/constants/index.ts +10 -0
  60. package/src/node/security/csp.ts +31 -0
  61. package/src/node/security/headers.ts +27 -0
  62. package/src/node/utils.ts +153 -5
  63. package/tsup.config.ts +0 -6
  64. package/dist/base-ui/index.d.mts +0 -25
  65. package/dist/base-ui/index.d.ts +0 -25
  66. package/dist/base-ui/index.js +0 -1
  67. package/dist/base-ui/index.mjs +0 -1
  68. package/dist/cache-P6WK424C.mjs +0 -1
  69. package/dist/chunk-2DI3OGHV.mjs +0 -1
  70. package/dist/chunk-2Z5T6EAU.mjs +0 -1
  71. package/dist/chunk-64AJ5QLT.mjs +0 -1
  72. package/dist/chunk-DDX52BX4.mjs +0 -1
  73. package/dist/chunk-HRZDSFR5.mjs +0 -1
  74. package/dist/chunk-JD3RSDE4.mjs +0 -1
  75. package/dist/chunk-JZXLCA2E.mjs +0 -1
  76. package/dist/chunk-NBCYHLAA.mjs +0 -1
  77. package/dist/chunk-PPVDMDEL.mjs +0 -1
  78. package/dist/chunk-T3W44KWY.mjs +0 -1
  79. package/dist/chunk-UBE4CKOA.mjs +0 -1
  80. package/dist/chunk-UWT4AJTH.mjs +0 -73
  81. package/dist/chunk-WWJ7WKDI.mjs +0 -1
  82. package/dist/chunk-Y4RRHPXC.mjs +0 -1
  83. package/dist/client/types.d.mts +0 -3
  84. package/dist/client/types.d.ts +0 -3
  85. package/dist/client/types.js +0 -1
  86. package/dist/client/types.mjs +0 -0
  87. package/dist/copy-markdown--9yjpbyy.d.mts +0 -15
  88. package/dist/copy-markdown-l2MYkcG7.d.ts +0 -15
  89. package/dist/hooks/index.d.mts +0 -137
  90. package/dist/hooks/index.d.ts +0 -137
  91. package/dist/hooks/index.js +0 -1
  92. package/dist/hooks/index.mjs +0 -1
  93. package/dist/integrations/index.d.mts +0 -48
  94. package/dist/integrations/index.d.ts +0 -48
  95. package/dist/integrations/index.js +0 -1
  96. package/dist/integrations/index.mjs +0 -1
  97. package/dist/link-DfBwCeZc.d.mts +0 -68
  98. package/dist/link-DfBwCeZc.d.ts +0 -68
  99. package/dist/loading-BwUos0wZ.d.mts +0 -57
  100. package/dist/loading-nlnUD01v.d.ts +0 -57
  101. package/dist/mdx/index.d.mts +0 -178
  102. package/dist/mdx/index.d.ts +0 -178
  103. package/dist/mdx/index.js +0 -1
  104. package/dist/mdx/index.mjs +0 -1
  105. package/dist/primitives/index.d.mts +0 -292
  106. package/dist/primitives/index.d.ts +0 -292
  107. package/dist/primitives/index.js +0 -1
  108. package/dist/primitives/index.mjs +0 -1
  109. package/dist/search-dialog-OONKKC5H.mjs +0 -1
  110. package/dist/types-opDA2E9-.d.mts +0 -394
  111. package/dist/types-opDA2E9-.d.ts +0 -394
  112. package/dist/use-routes-DNwgTRpU.d.ts +0 -29
  113. package/dist/use-routes-DrT80Eom.d.mts +0 -29
@@ -0,0 +1,107 @@
1
+ import type { Plugin as VitePlugin } from 'vite'
2
+ import type { BoltdocsConfig } from '../config'
3
+
4
+ /**
5
+ * Permissions that a plugin can request to access specific Boltdocs capabilities.
6
+ */
7
+ export type PluginPermission =
8
+ | 'fs:read' // Read filesystem
9
+ | 'fs:write' // Write filesystem
10
+ | 'vite:config' // Modify Vite config
11
+ | 'mdx:remark' // Add remark plugins
12
+ | 'mdx:rehype' // Add rehype plugins
13
+ | 'components' // Register MDX components
14
+ | 'hooks:build' // Access build lifecycle hooks
15
+ | 'hooks:dev' // Access dev lifecycle hooks
16
+
17
+ /**
18
+ * Shared context injected into every plugin lifecycle hook.
19
+ */
20
+ export interface PluginContext {
21
+ /** The full, resolved Boltdocs configuration (Readonly) */
22
+ readonly config: BoltdocsConfig
23
+ /** A plugin-specific logger */
24
+ readonly logger: PluginLogger
25
+ /** A shared store for dependency injection and state sharing between plugins */
26
+ readonly store: PluginStore
27
+ /** Metadata about the current plugin */
28
+ readonly meta: PluginMeta
29
+ }
30
+
31
+ /**
32
+ * Simple logger interface for plugins.
33
+ */
34
+ export interface PluginLogger {
35
+ info(message: string): void
36
+ warn(message: string): void
37
+ error(message: string | Error): void
38
+ debug(message: string): void
39
+ }
40
+
41
+ /**
42
+ * A shared key-value store that allows plugins to share state and configuration.
43
+ */
44
+ export interface PluginStore {
45
+ /** Get a value from the store. Keys are namespaced by plugin internally. */
46
+ get<T = unknown>(pluginName: string, key: string): T | undefined
47
+ /** Set a value in the store. */
48
+ set(pluginName: string, key: string, value: unknown): void
49
+ /** Check if a key exists in the store. */
50
+ has(pluginName: string, key: string): boolean
51
+ }
52
+
53
+ /**
54
+ * Metadata for a plugin, used for identification and compatibility checks.
55
+ */
56
+ export interface PluginMeta {
57
+ /** Unique identifier for the plugin */
58
+ name: string
59
+ /** Version of the plugin itself (semver) */
60
+ version?: string
61
+ /** Minimum required version of Boltdocs (semver range) */
62
+ boltdocsVersion?: string
63
+ }
64
+
65
+ /**
66
+ * Lifecycle hooks that a plugin can implement to hook into the build and dev processes.
67
+ */
68
+ export interface PluginLifecycleHooks {
69
+ /** Called before the build process starts */
70
+ beforeBuild?: (ctx: PluginContext) => Promise<void> | void
71
+ /** Called after the build process finishes successfully */
72
+ afterBuild?: (ctx: PluginContext) => Promise<void> | void
73
+ /** Called before the dev server starts */
74
+ beforeDev?: (ctx: PluginContext) => Promise<void> | void
75
+ /** Called after the dev server is ready (configureServer) */
76
+ afterDev?: (ctx: PluginContext) => Promise<void> | void
77
+ /** Called when the final Boltdocs config is resolved */
78
+ configResolved?: (ctx: PluginContext, config: BoltdocsConfig) => void
79
+ /** Called when the build is closing */
80
+ buildEnd?: (ctx: PluginContext) => Promise<void> | void
81
+ }
82
+
83
+ /**
84
+ * The extended, secure Boltdocs plugin interface.
85
+ */
86
+ export interface SecureBoltdocsPlugin {
87
+ /** A unique name for the plugin (e.g., 'boltdocs-plugin-mermaid') */
88
+ name: string
89
+ /** Whether to run this plugin before or after default ones */
90
+ enforce?: 'pre' | 'post'
91
+ /** Version of the plugin (optional, but recommended for security) */
92
+ version?: string
93
+ /** Minimum compatible Boltdocs version (optional, semver range) */
94
+ boltdocsVersion?: string
95
+ /** List of permissions this plugin requires to operate */
96
+ permissions?: PluginPermission[]
97
+ /** Optional remark plugins to add to the MDX pipeline (requires 'mdx:remark' permission) */
98
+ remarkPlugins?: unknown[]
99
+ /** Optional rehype plugins to add to the MDX pipeline (requires 'mdx:rehype' permission) */
100
+ rehypePlugins?: unknown[]
101
+ /** Optional Vite plugins to inject into the build process (requires 'vite:config' permission) */
102
+ vitePlugins?: VitePlugin[]
103
+ /** Optional custom React components to register in MDX. Map of Name -> Module Path. (requires 'components' permission) */
104
+ components?: Record<string, string>
105
+ /** Implementation of lifecycle hooks (requires 'hooks:build' or 'hooks:dev' permissions) */
106
+ hooks?: PluginLifecycleHooks
107
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod'
2
+ import semver from 'semver'
3
+ import path from 'path'
4
+ import { BoltdocsPluginSchema } from '../schema/config'
5
+ import {
6
+ PluginValidationError,
7
+ PluginCompatibilityError,
8
+ } from './plugin-errors'
9
+ import type { SecureBoltdocsPlugin, PluginPermission } from './plugin-types'
10
+
11
+ /**
12
+ * Enhanced Zod schema for secure plugins.
13
+ */
14
+ export const SecurePluginSchema = BoltdocsPluginSchema.extend({
15
+ version: z.string().optional(),
16
+ boltdocsVersion: z.string().optional(),
17
+ permissions: z.array(z.string()).optional(),
18
+ hooks: z
19
+ .object({
20
+ beforeBuild: z.function().optional(),
21
+ afterBuild: z.function().optional(),
22
+ beforeDev: z.function().optional(),
23
+ afterDev: z.function().optional(),
24
+ configResolved: z.function().optional(),
25
+ buildEnd: z.function().optional(),
26
+ })
27
+ .optional(),
28
+ })
29
+
30
+ /**
31
+ * Validates a list of plugins for correctness, security, and compatibility.
32
+ */
33
+ export function validatePlugins(
34
+ plugins: any[],
35
+ boltdocsVersion: string,
36
+ ): SecureBoltdocsPlugin[] {
37
+ const validatedPlugins: SecureBoltdocsPlugin[] = []
38
+ const pluginNames = new Set<string>()
39
+
40
+ for (const rawPlugin of plugins) {
41
+ // 1. Basic Structure Validation
42
+ const result = SecurePluginSchema.safeParse(rawPlugin)
43
+ if (!result.success) {
44
+ throw new PluginValidationError(
45
+ rawPlugin.name || 'unknown',
46
+ result.error.issues
47
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
48
+ .join(', '),
49
+ )
50
+ }
51
+
52
+ const plugin = result.data as SecureBoltdocsPlugin
53
+
54
+ // 2. Name Uniqueness
55
+ if (pluginNames.has(plugin.name)) {
56
+ throw new PluginValidationError(
57
+ plugin.name,
58
+ 'Duplicate plugin name detected',
59
+ )
60
+ }
61
+ pluginNames.add(plugin.name)
62
+
63
+ // 3. Semver Compatibility
64
+ if (
65
+ plugin.boltdocsVersion &&
66
+ !semver.satisfies(boltdocsVersion, plugin.boltdocsVersion)
67
+ ) {
68
+ throw new PluginCompatibilityError(
69
+ plugin.name,
70
+ `Plugin expects Boltdocs version ${plugin.boltdocsVersion}, but current is ${boltdocsVersion}`,
71
+ )
72
+ }
73
+
74
+ // 4. Component Path Security (Path Traversal Prevention)
75
+ if (plugin.components) {
76
+ for (const [compName, compPath] of Object.entries(plugin.components)) {
77
+ if (compPath.includes('..') || path.isAbsolute(compPath)) {
78
+ // Absolute paths are technically okay but we restrict them for consistency/security
79
+ // if they point outside the workspace. For now, we block '..' explicitly.
80
+ if (compPath.includes('..')) {
81
+ throw new PluginValidationError(
82
+ plugin.name,
83
+ `Component '${compName}' has an invalid path: traversal sequences are not allowed.`,
84
+ )
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ validatedPlugins.push(plugin)
91
+ }
92
+
93
+ return validatedPlugins
94
+ }
95
+
96
+ /**
97
+ * Helper to check if a specific permission is granted to a plugin.
98
+ */
99
+ export function hasPermission(
100
+ plugin: SecureBoltdocsPlugin,
101
+ permission: PluginPermission,
102
+ ): boolean {
103
+ if (!plugin.permissions) return false
104
+ return plugin.permissions.includes(permission)
105
+ }
@@ -11,7 +11,10 @@ import {
11
11
  extractNumberPrefix,
12
12
  sanitizeHtml,
13
13
  stripHtmlTags,
14
+ logSecurityEvent,
14
15
  } from '../utils'
16
+ import { MAX_PATH_LENGTH, ALLOWED_PATH_CHARS } from '../security/constants'
17
+ import { EncodingSecurityError, PathTraversalError } from '../errors'
15
18
 
16
19
  /**
17
20
  * Parses a single Markdown/MDX file and extracts its metadata for routing.
@@ -32,8 +35,30 @@ export function parseDocFile(
32
35
  basePath: string,
33
36
  config?: BoltdocsConfig,
34
37
  ): ParsedDocFile {
35
- // Security: Prevent path traversal
36
- const decodedFile = decodeURIComponent(file)
38
+ // Security: Prevent path traversal and malicious encoding
39
+ let decodedFile: string
40
+ try {
41
+ decodedFile = decodeURIComponent(file)
42
+ } catch {
43
+ const fileName = path.basename(file)
44
+ logSecurityEvent('ENCODING_ERROR', 'Invalid character encoding', { file: fileName })
45
+ throw new EncodingSecurityError(
46
+ `Security breach: Invalid characters or encoding in path: ${fileName}`,
47
+ )
48
+ }
49
+
50
+ // Validation: Path length
51
+ if (decodedFile.length > MAX_PATH_LENGTH) {
52
+ const fileName = path.basename(decodedFile)
53
+ logSecurityEvent('PATH_TOO_LONG', 'Path length exceeds limit', {
54
+ length: decodedFile.length,
55
+ file: fileName,
56
+ })
57
+ throw new PathTraversalError(
58
+ `Security breach: Path length exceeds limit of ${MAX_PATH_LENGTH} characters: ${fileName}`,
59
+ )
60
+ }
61
+
37
62
  const absoluteFile = path.resolve(decodedFile)
38
63
  const absoluteDocsDir = path.resolve(docsDir)
39
64
  const relativePath = normalizePath(
@@ -43,10 +68,15 @@ export function parseDocFile(
43
68
  if (
44
69
  relativePath.startsWith('../') ||
45
70
  relativePath === '..' ||
46
- absoluteFile.includes('\0')
71
+ absoluteFile.includes('\0') ||
72
+ !ALLOWED_PATH_CHARS.test(relativePath)
47
73
  ) {
48
- throw new Error(
49
- `Security breach: File is outside of docs directory or contains null bytes: ${file}`,
74
+ const fileName = path.basename(file)
75
+ logSecurityEvent('PATH_TRAVERSAL_ATTEMPT', 'Path traversal or invalid characters detected', {
76
+ path: relativePath,
77
+ })
78
+ throw new PathTraversalError(
79
+ `Security breach: File is outside of docs directory, contains null bytes, or invalid characters: ${fileName}`,
50
80
  )
51
81
  }
52
82
 
@@ -0,0 +1,208 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Zod schema for a single social link.
5
+ */
6
+ export const SocialLinkSchema = z.object({
7
+ icon: z.string().max(50),
8
+ link: z.string().url(),
9
+ })
10
+
11
+ /**
12
+ * Zod schema for footer configuration.
13
+ */
14
+ export const FooterConfigSchema = z.object({
15
+ text: z.string().max(2000).optional(),
16
+ })
17
+
18
+ /**
19
+ * Zod schema for plugin permissions.
20
+ */
21
+ export const PluginPermissionSchema = z.enum([
22
+ 'fs:read',
23
+ 'fs:write',
24
+ 'vite:config',
25
+ 'mdx:remark',
26
+ 'mdx:rehype',
27
+ 'components',
28
+ 'hooks:build',
29
+ 'hooks:dev',
30
+ ])
31
+
32
+ /**
33
+ * Zod schema for a Boltdocs plugin.
34
+ */
35
+ export const BoltdocsPluginSchema = z.object({
36
+ name: z.string(),
37
+ enforce: z.enum(['pre', 'post']).optional(),
38
+ version: z.string().optional(),
39
+ boltdocsVersion: z.string().optional(),
40
+ permissions: z.array(PluginPermissionSchema).optional(),
41
+ remarkPlugins: z.array(z.any()).optional(),
42
+ rehypePlugins: z.array(z.any()).optional(),
43
+ vitePlugins: z.array(z.any()).optional(),
44
+ components: z.record(z.string(), z.string()).optional(),
45
+ hooks: z.record(z.string(), z.any()).optional(),
46
+ })
47
+
48
+ /**
49
+ * Zod schema for theme configuration.
50
+ */
51
+ export const ThemeConfigSchema = z.object({
52
+ title: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
53
+ description: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
54
+ logo: z
55
+ .union([
56
+ z.string(),
57
+ z.object({
58
+ dark: z.string(),
59
+ light: z.string(),
60
+ alt: z.string().optional(),
61
+ width: z.number().optional(),
62
+ height: z.number().optional(),
63
+ }),
64
+ ])
65
+ .optional(),
66
+ navbar: z
67
+ .array(
68
+ z.object({
69
+ label: z.union([z.string(), z.record(z.string(), z.string())]),
70
+ href: z.string(),
71
+ }),
72
+ )
73
+ .optional(),
74
+ sidebar: z
75
+ .record(
76
+ z.string(),
77
+ z.array(
78
+ z.object({
79
+ text: z.string(),
80
+ link: z.string(),
81
+ }),
82
+ ),
83
+ )
84
+ .optional(),
85
+ socialLinks: z.array(SocialLinkSchema).optional(),
86
+ footer: FooterConfigSchema.optional(),
87
+ breadcrumbs: z.boolean().optional(),
88
+ editLink: z.string().refine(val => !val || val.includes(':path'), {
89
+ message: "editLink must contain ':path' placeholder if specified"
90
+ }).optional(),
91
+ communityHelp: z.string().url().optional(),
92
+ version: z.string().max(50).optional(),
93
+ githubRepo: z.string().max(100).optional(),
94
+ favicon: z.string().optional(),
95
+ ogImage: z.string().optional(),
96
+ poweredBy: z.boolean().optional(),
97
+ tabs: z
98
+ .array(
99
+ z.object({
100
+ id: z.string(),
101
+ text: z.union([z.string(), z.record(z.string(), z.string())]),
102
+ icon: z.string().optional(),
103
+ }),
104
+ )
105
+ .optional(),
106
+ codeTheme: z
107
+ .union([z.string(), z.object({ light: z.string(), dark: z.string() })])
108
+ .optional(),
109
+ copyMarkdown: z
110
+ .union([
111
+ z.boolean(),
112
+ z.object({
113
+ text: z.string().optional(),
114
+ icon: z.string().optional(),
115
+ }),
116
+ ])
117
+ .optional(),
118
+ })
119
+
120
+ /**
121
+ * Zod schema for robots.txt configuration.
122
+ */
123
+ export const RobotsConfigSchema = z.union([
124
+ z.string(),
125
+ z.object({
126
+ rules: z
127
+ .array(
128
+ z.object({
129
+ userAgent: z.string(),
130
+ allow: z.union([z.string(), z.array(z.string())]).optional(),
131
+ disallow: z.union([z.string(), z.array(z.string())]).optional(),
132
+ }),
133
+ )
134
+ .optional(),
135
+ sitemaps: z.array(z.string().url()).optional(),
136
+ }),
137
+ ])
138
+
139
+ /**
140
+ * Zod schema for internationalization configuration.
141
+ */
142
+ export const I18nConfigSchema = z.object({
143
+ defaultLocale: z.string(),
144
+ locales: z.record(z.string(), z.string()),
145
+ localeConfigs: z
146
+ .record(
147
+ z.string(),
148
+ z.object({
149
+ label: z.string().optional(),
150
+ direction: z.enum(['ltr', 'rtl']).optional(),
151
+ htmlLang: z.string().optional(),
152
+ calendar: z.string().optional(),
153
+ }),
154
+ )
155
+ .optional(),
156
+ })
157
+
158
+ /**
159
+ * Zod schema for versioning configuration.
160
+ */
161
+ export const VersionsConfigSchema = z.object({
162
+ defaultVersion: z.string(),
163
+ prefix: z.string().optional(),
164
+ versions: z.array(
165
+ z.object({
166
+ label: z.string(),
167
+ path: z.string(),
168
+ }),
169
+ ),
170
+ })
171
+
172
+ /**
173
+ * Zod schema for security configuration.
174
+ */
175
+ export const SecurityConfigSchema = z.object({
176
+ headers: z.record(z.string(), z.string()).optional(),
177
+ enableCSP: z.boolean().optional(),
178
+ customHeaders: z.record(z.string(), z.string()).optional(),
179
+ })
180
+
181
+ /**
182
+ * Zod schema for integrations configuration.
183
+ */
184
+ export const IntegrationsConfigSchema = z.object({
185
+ sandbox: z
186
+ .object({
187
+ enable: z.boolean().optional(),
188
+ config: z.record(z.string(), z.unknown()).optional(),
189
+ })
190
+ .optional(),
191
+ })
192
+
193
+ /**
194
+ * Root Zod schema for Boltdocs project configuration.
195
+ */
196
+ export const BoltdocsConfigSchema = z.object({
197
+ siteUrl: z.string().url().optional(),
198
+ docsDir: z.string().optional(),
199
+ homePage: z.string().optional(),
200
+ theme: ThemeConfigSchema.optional(),
201
+ i18n: I18nConfigSchema.optional(),
202
+ versions: VersionsConfigSchema.optional(),
203
+ plugins: z.array(BoltdocsPluginSchema).optional(),
204
+ integrations: IntegrationsConfigSchema.optional(),
205
+ robots: RobotsConfigSchema.optional(),
206
+ security: SecurityConfigSchema.optional(),
207
+ vite: z.record(z.string(), z.unknown()).optional(),
208
+ })
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Schema for strict frontmatter validation.
5
+ */
6
+ export const FrontmatterSchema = z.object({
7
+ title: z.string().max(200).optional(),
8
+ description: z.string().max(500).optional(),
9
+ sidebarPosition: z.number().optional(),
10
+ sidebarLabel: z.string().max(100).optional(),
11
+ category: z.string().max(50).optional(),
12
+ order: z.number().optional(),
13
+ badge: z.string().max(50).optional(),
14
+ icon: z.string().max(50).optional(),
15
+ })
16
+
17
+ export type FrontmatterData = z.infer<typeof FrontmatterSchema>
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Security limits for file system operations.
3
+ */
4
+ export const MAX_PATH_LENGTH = 260
5
+ export const ALLOWED_PATH_CHARS = /^[a-zA-Z0-9\-_\/\.\(\)]+$/
6
+
7
+ /**
8
+ * Security limits for document metadata (frontmatter).
9
+ */
10
+ export const MAX_FRONTMATTER_SIZE = 10 * 1024 // 10KB
@@ -0,0 +1,31 @@
1
+ import type { BoltdocsConfig } from '../config'
2
+
3
+ /**
4
+ * Generates a Content Security Policy (CSP) header string based on the configuration.
5
+ * Automatically adapts to the environment (development vs production).
6
+ *
7
+ * @param config - The Boltdocs configuration object.
8
+ * @returns The CSP header value.
9
+ */
10
+ export function getCSPHeader(_config: BoltdocsConfig): string {
11
+ const isDev = process.env.NODE_ENV === 'development'
12
+
13
+ const directives: Record<string, string[]> = {
14
+ 'default-src': ["'self'"],
15
+ 'script-src': ["'self'", "'unsafe-inline'"],
16
+ 'style-src': ["'self'", "'unsafe-inline'"],
17
+ 'img-src': ["'self'", "data:", "https:"],
18
+ 'font-src': ["'self'"],
19
+ 'connect-src': ["'self'"],
20
+ }
21
+
22
+ // Relax policies in development to support Vite's HMR (eval-based)
23
+ if (isDev) {
24
+ directives['script-src'] = ["'self'", "'unsafe-eval'", "'unsafe-inline'"]
25
+ directives['style-src'] = ["'self'", "'unsafe-inline'"]
26
+ }
27
+
28
+ return Object.entries(directives)
29
+ .map(([key, values]) => `${key} ${values.join(' ')}`)
30
+ .join('; ')
31
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Standard Security Headers for hardened web applications.
3
+ * Recommended by OWASP and industry best practices to prevent common web attacks.
4
+ *
5
+ * These can be applied as middleware in HTTP servers or as metadata in SSG deployments.
6
+ */
7
+ export const SECURITY_HEADERS = {
8
+ /** Prevents MIME type sniffing by the browser. */
9
+ 'X-Content-Type-Options': 'nosniff',
10
+
11
+ /** Prevents the page from being embedded in iframes (anti-clickjacking). */
12
+ 'X-Frame-Options': 'DENY',
13
+
14
+ /** Enables XSS filtering in modern browsers (legacy support). */
15
+ 'X-XSS-Protection': '1; mode=block',
16
+
17
+ /** Controls how much referrer information is sent with requests. */
18
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
19
+
20
+ /** Restricts access to sensitive browser features (camera, mic, geo). */
21
+ 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
22
+
23
+ /** Enforces HTTPS for the domain and all subdomains for 1 year. */
24
+ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
25
+ } as const;
26
+
27
+ export type SecurityHeaderKey = keyof typeof SECURITY_HEADERS;