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
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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;
|