@raystack/chronicle 0.11.1 → 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 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
- if (/^[a-z][a-z0-9+\-.]*:/i.test(src))
76
- return src;
77
- if (src.startsWith("//"))
78
- return src;
79
- if (src.startsWith("#"))
80
- return src;
81
- if (src.startsWith("/_content/"))
82
- return src;
83
- if (src.startsWith("/"))
84
- return `/_content${src}`;
85
- return `/_content/${path4.posix.normalize(path4.posix.join(dir, src))}`;
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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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
- if (/^[a-z][a-z0-9+\-.]*:/i.test(src)) return src
12
- if (src.startsWith('//')) return src
13
- if (src.startsWith('#')) return src
14
- if (src.startsWith('/_content/')) return src
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 (src.startsWith('/')) return `/_content${src}`
17
- return `/_content/${path.posix.normalize(path.posix.join(dir, src))}`
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 {
@@ -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 url = event.url.searchParams.get('url')
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 (!url || !wParam) {
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
- <StaticRouter location={pathname}>
140
- <ReactRouterProvider>
141
- <PageProvider
142
- initialConfig={config}
143
- initialTree={tree}
144
- initialPage={pageData}
145
- initialApiSpecs={apiSpecs}
146
- initialVersion={route.version}
147
- loadMdx={async () => ({ content: null, toc: [] })}
148
- >
149
- <App />
150
- </PageProvider>
151
- </ReactRouterProvider>
152
- </StaticRouter>
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
- const decoded = decodeURIComponent(urlPath.split('?')[0]);
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) &&
@@ -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="t2" render={<h1 />} className={styles.title}>
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-space-8);
97
+ font-size: var(--rs-font-size-t4);
98
98
  font-weight: var(--rs-font-weight-medium);
99
- line-height: var(--rs-space-10);
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);