diff --git a/src/utils/repo-path.ts b/src/utils/repo-path.ts index e47bfc7a4..aaefe17ec 100644 --- a/src/utils/repo-path.ts +++ b/src/utils/repo-path.ts @@ -1,6 +1,6 @@ export const MAX_REPO_PATH_LENGTH = 512 -const REPO_PATH_SEGMENT_PATTERN = /^[a-zA-Z0-9._$+\[\]()-]+$/ +const REPO_PATH_SEGMENT_PATTERN = /^[a-zA-Z0-9@._$+\[\]()-]+$/ export function isValidRepoPath(path: string) { if (path === '') { diff --git a/tests/scoped-docs-paths.test.ts b/tests/scoped-docs-paths.test.ts new file mode 100644 index 000000000..51c920913 --- /dev/null +++ b/tests/scoped-docs-paths.test.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { isValidRepoPath } from '../src/utils/repo-path' +import { resolveDocsPathRedirect } from '../src/utils/docs-redirects' + +const docsPath = + 'reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD' +test('scoped generated reference paths pass both content and route validation', () => { + assert.equal(isValidRepoPath(`docs/${docsPath}.md`), true) + assert.deepEqual( + resolveDocsPathRedirect({ + defaultDocs: 'overview', + docsPath, + frameworks: [], + manifest: { paths: [docsPath], redirects: {} }, + }), + { type: 'render', docsPath }, + ) +}) +test('scoped path support preserves traversal and URL delimiter rejection', () => { + for (const path of [ + '@scope/../secret', + '@scope/./secret', + '@scope//secret', + '@scope/%2e%2e/secret', + '@scope/file?raw=1', + '@scope/file#fragment', + '@scope\\secret', + '//user@host/file', + 'https://user@host/file', + ]) { + assert.equal(isValidRepoPath(path), false, path) + } +})