@raystack/chronicle 0.11.0 → 0.11.2
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/dist/cli/index.js +14 -12
- package/package.json +1 -1
- package/src/lib/image-utils.test.ts +8 -0
- package/src/lib/remark-resolve-images.ts +7 -6
- package/src/server/api/image.ts +4 -2
- package/src/server/entry-server.tsx +17 -14
- package/src/server/utils/safe-path.test.ts +34 -0
- package/src/server/utils/safe-path.ts +6 -1
- package/src/server/vite-config.ts +1 -0
- package/src/themes/default/Page.tsx +1 -1
- package/src/themes/paper/Page.module.css +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -72,17 +72,18 @@ var init_image_utils = () => {};
|
|
|
72
72
|
import path4 from "node:path";
|
|
73
73
|
import { visit } from "unist-util-visit";
|
|
74
74
|
function resolveUrl(src, dir) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
const normalized = src.replace(/\\/g, "/");
|
|
76
|
+
if (/^[a-z][a-z0-9+\-.]*:/i.test(normalized))
|
|
77
|
+
return normalized;
|
|
78
|
+
if (normalized.startsWith("//"))
|
|
79
|
+
return normalized;
|
|
80
|
+
if (normalized.startsWith("#"))
|
|
81
|
+
return normalized;
|
|
82
|
+
if (normalized.startsWith("/_content/"))
|
|
83
|
+
return normalized;
|
|
84
|
+
if (normalized.startsWith("/"))
|
|
85
|
+
return `/_content${normalized}`;
|
|
86
|
+
return `/_content/${path4.posix.normalize(path4.posix.join(dir, normalized))}`;
|
|
86
87
|
}
|
|
87
88
|
function optimizeUrl(url, optimize) {
|
|
88
89
|
if (optimize && isLocalImage(url) && !isSvg(url))
|
|
@@ -406,7 +407,8 @@ async function createViteConfig(options) {
|
|
|
406
407
|
plugins: [
|
|
407
408
|
nitro({
|
|
408
409
|
serverDir: path6.resolve(packageRoot, "src/server"),
|
|
409
|
-
...preset && { preset }
|
|
410
|
+
...preset && { preset },
|
|
411
|
+
ignore: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"]
|
|
410
412
|
}),
|
|
411
413
|
mdx({
|
|
412
414
|
default: defineFumadocsConfig({
|
package/package.json
CHANGED
|
@@ -53,6 +53,14 @@ describe('buildOptimizedUrl', () => {
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
describe('buildOptimizedUrl with backslashes', () => {
|
|
57
|
+
test('backslashes in input are not double-encoded', () => {
|
|
58
|
+
const url = buildOptimizedUrl('/_content/docs/imgs\\screenshot.png', 640);
|
|
59
|
+
expect(url).toContain('imgs%5Cscreenshot.png');
|
|
60
|
+
expect(url).not.toContain('%255C');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
56
64
|
describe('constants', () => {
|
|
57
65
|
test('ALLOWED_WIDTHS is sorted ascending', () => {
|
|
58
66
|
for (let i = 1; i < ALLOWED_WIDTHS.length; i++) {
|
|
@@ -8,13 +8,14 @@ import { MdxNodeType } from './mdx-utils'
|
|
|
8
8
|
import { isLocalImage, isSvg, buildOptimizedUrl, DEFAULT_WIDTH } from './image-utils'
|
|
9
9
|
|
|
10
10
|
function resolveUrl(src: string, dir: string): string {
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
if (
|
|
14
|
-
if (
|
|
11
|
+
const normalized = src.replace(/\\/g, '/')
|
|
12
|
+
if (/^[a-z][a-z0-9+\-.]*:/i.test(normalized)) return normalized
|
|
13
|
+
if (normalized.startsWith('//')) return normalized
|
|
14
|
+
if (normalized.startsWith('#')) return normalized
|
|
15
|
+
if (normalized.startsWith('/_content/')) return normalized
|
|
15
16
|
|
|
16
|
-
if (
|
|
17
|
-
return `/_content/${path.posix.normalize(path.posix.join(dir,
|
|
17
|
+
if (normalized.startsWith('/')) return `/_content${normalized}`
|
|
18
|
+
return `/_content/${path.posix.normalize(path.posix.join(dir, normalized))}`
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
interface RemarkResolveImagesOptions {
|
package/src/server/api/image.ts
CHANGED
|
@@ -52,14 +52,16 @@ async function evictIfNeeded(storage: ReturnType<typeof useStorage>) {
|
|
|
52
52
|
export default defineHandler(async event => {
|
|
53
53
|
const storage = useStorage(STORAGE_KEY)
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const rawUrl = event.url.searchParams.get('url')
|
|
56
56
|
const wParam = event.url.searchParams.get('w')
|
|
57
57
|
const qParam = event.url.searchParams.get('q')
|
|
58
58
|
|
|
59
|
-
if (!
|
|
59
|
+
if (!rawUrl || !wParam) {
|
|
60
60
|
throw new HTTPError({ status: StatusCodes.BAD_REQUEST, message: 'Missing url or w parameter' })
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const url = rawUrl.replace(/\\/g, '/')
|
|
64
|
+
|
|
63
65
|
if (!url.startsWith('/_content/')) {
|
|
64
66
|
throw new HTTPError({ status: StatusCodes.BAD_REQUEST, message: 'Only local content images allowed' })
|
|
65
67
|
}
|
|
@@ -14,6 +14,7 @@ import { getFirstApiUrl } from '@/lib/api-routes';
|
|
|
14
14
|
import { StatusCodes } from 'http-status-codes';
|
|
15
15
|
import { resolveDocsRedirect } from '@/lib/tree-utils';
|
|
16
16
|
import { isLocalImage, isSvg, buildOptimizedUrl, DEFAULT_WIDTH } from '@/lib/image-utils';
|
|
17
|
+
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
|
17
18
|
import { useNitroApp } from 'nitro/app';
|
|
18
19
|
import { App } from './App';
|
|
19
20
|
|
|
@@ -136,20 +137,22 @@ export default {
|
|
|
136
137
|
</head>
|
|
137
138
|
<body>
|
|
138
139
|
<div id="root">
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
141
|
+
<StaticRouter location={pathname}>
|
|
142
|
+
<ReactRouterProvider>
|
|
143
|
+
<PageProvider
|
|
144
|
+
initialConfig={config}
|
|
145
|
+
initialTree={tree}
|
|
146
|
+
initialPage={pageData}
|
|
147
|
+
initialApiSpecs={apiSpecs}
|
|
148
|
+
initialVersion={route.version}
|
|
149
|
+
loadMdx={async () => ({ content: null, toc: [] })}
|
|
150
|
+
>
|
|
151
|
+
<App />
|
|
152
|
+
</PageProvider>
|
|
153
|
+
</ReactRouterProvider>
|
|
154
|
+
</StaticRouter>
|
|
155
|
+
</QueryClientProvider>
|
|
153
156
|
</div>
|
|
154
157
|
</body>
|
|
155
158
|
</html>,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { safePath } from '@/server/utils/safe-path';
|
|
4
|
+
|
|
5
|
+
describe('safePath', () => {
|
|
6
|
+
const base = '/app/content';
|
|
7
|
+
|
|
8
|
+
test('resolves valid path within base', () => {
|
|
9
|
+
expect(safePath(base, '/docs/intro.mdx')).toBe(path.resolve(base, 'docs/intro.mdx'));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('returns null for path traversal', () => {
|
|
13
|
+
expect(safePath(base, '/../etc/passwd')).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('normalizes backslashes to forward slashes', () => {
|
|
17
|
+
const result = safePath(base, '/docs\\imgs\\screenshot.png');
|
|
18
|
+
expect(result).toBe(path.resolve(base, 'docs/imgs/screenshot.png'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('decodes URI-encoded characters', () => {
|
|
22
|
+
const result = safePath(base, '/docs/my%20image.png');
|
|
23
|
+
expect(result).toBe(path.resolve(base, 'docs/my image.png'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('strips query string before resolving', () => {
|
|
27
|
+
const result = safePath(base, '/docs/img.png?v=1');
|
|
28
|
+
expect(result).toBe(path.resolve(base, 'docs/img.png'));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('returns null for malformed percent-encoding', () => {
|
|
32
|
+
expect(safePath(base, '/docs/%E0%A4%')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -5,7 +5,12 @@ import path from 'node:path';
|
|
|
5
5
|
* Returns null if the resolved path escapes the base directory.
|
|
6
6
|
*/
|
|
7
7
|
export function safePath(baseDir: string, urlPath: string): string | null {
|
|
8
|
-
|
|
8
|
+
let decoded: string;
|
|
9
|
+
try {
|
|
10
|
+
decoded = decodeURIComponent(urlPath.split('?')[0]).replace(/\\/g, '/');
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
9
14
|
const resolved = path.resolve(baseDir, '.' + decoded);
|
|
10
15
|
if (
|
|
11
16
|
!resolved.startsWith(path.resolve(baseDir) + path.sep) &&
|
|
@@ -73,6 +73,7 @@ export async function createViteConfig(
|
|
|
73
73
|
nitro({
|
|
74
74
|
serverDir: path.resolve(packageRoot, 'src/server'),
|
|
75
75
|
...(preset && { preset }),
|
|
76
|
+
ignore: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
|
|
76
77
|
}),
|
|
77
78
|
mdx({
|
|
78
79
|
default: defineFumadocsConfig({
|
|
@@ -12,7 +12,7 @@ export function Page({ page }: ThemePageProps) {
|
|
|
12
12
|
<Flex className={styles.page}>
|
|
13
13
|
<article className={styles.article} data-article-content>
|
|
14
14
|
{page.frontmatter.title && (
|
|
15
|
-
<Headline size="
|
|
15
|
+
<Headline size="t4" render={<h1 />} className={styles.title}>
|
|
16
16
|
{page.frontmatter.title}
|
|
17
17
|
</Headline>
|
|
18
18
|
)}
|
|
@@ -94,9 +94,9 @@
|
|
|
94
94
|
|
|
95
95
|
.articleTitle {
|
|
96
96
|
font-family: var(--paper-font-body);
|
|
97
|
-
font-size: var(--rs-
|
|
97
|
+
font-size: var(--rs-font-size-t4);
|
|
98
98
|
font-weight: var(--rs-font-weight-medium);
|
|
99
|
-
line-height: var(--rs-
|
|
99
|
+
line-height: var(--rs-line-height-t4);
|
|
100
100
|
letter-spacing: var(--rs-letter-spacing-t1);
|
|
101
101
|
text-align: center;
|
|
102
102
|
color: var(--rs-color-foreground-base-primary);
|