@assistant-ui/mcp-docs-server 0.1.17 → 0.1.18
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/.docs/organized/code-examples/with-ag-ui.md +146 -152
- package/.docs/organized/code-examples/with-ai-sdk-v5.md +96 -101
- package/.docs/organized/code-examples/with-assistant-transport.md +132 -220
- package/.docs/organized/code-examples/with-cloud.md +124 -131
- package/.docs/organized/code-examples/with-custom-thread-list.md +26 -46
- package/.docs/organized/code-examples/with-external-store.md +146 -151
- package/.docs/organized/code-examples/with-ffmpeg.md +129 -139
- package/.docs/organized/code-examples/with-langgraph.md +231 -225
- package/.docs/organized/code-examples/with-parent-id-grouping.md +146 -151
- package/.docs/organized/code-examples/with-react-hook-form.md +146 -152
- package/.docs/organized/code-examples/{store-example.md → with-store.md} +16 -20
- package/.docs/organized/code-examples/with-tanstack.md +23 -41
- package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +36 -0
- package/.docs/raw/docs/runtimes/custom/local.mdx +31 -8
- package/.docs/raw/docs/ui/Scrollbar.mdx +0 -6
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -1
- package/dist/index.js.map +1 -0
- package/dist/prepare-docs/code-examples.d.ts +2 -0
- package/dist/prepare-docs/code-examples.d.ts.map +1 -0
- package/dist/prepare-docs/code-examples.js +129 -0
- package/dist/prepare-docs/code-examples.js.map +1 -0
- package/dist/prepare-docs/copy-raw.d.ts +2 -0
- package/dist/prepare-docs/copy-raw.d.ts.map +1 -0
- package/dist/prepare-docs/copy-raw.js +50 -0
- package/dist/prepare-docs/copy-raw.js.map +1 -0
- package/dist/prepare-docs/prepare.d.ts +2 -0
- package/dist/prepare-docs/prepare.d.ts.map +1 -0
- package/dist/prepare-docs/prepare.js +18 -195
- package/dist/prepare-docs/prepare.js.map +1 -0
- package/dist/stdio.d.ts +3 -0
- package/dist/stdio.d.ts.map +1 -0
- package/dist/stdio.js +4 -5
- package/dist/stdio.js.map +1 -0
- package/dist/tools/docs.d.ts +23 -0
- package/dist/tools/docs.d.ts.map +1 -0
- package/dist/tools/docs.js +168 -0
- package/dist/tools/docs.js.map +1 -0
- package/dist/tools/examples.d.ts +23 -0
- package/dist/tools/examples.d.ts.map +1 -0
- package/dist/tools/examples.js +95 -0
- package/dist/tools/examples.js.map +1 -0
- package/dist/tools/tests/test-setup.d.ts +4 -0
- package/dist/tools/tests/test-setup.d.ts.map +1 -0
- package/dist/tools/tests/test-setup.js +36 -0
- package/dist/tools/tests/test-setup.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +20 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/mcp-format.d.ts +7 -0
- package/dist/utils/mcp-format.d.ts.map +1 -0
- package/dist/utils/mcp-format.js +11 -0
- package/dist/utils/mcp-format.js.map +1 -0
- package/dist/utils/mdx.d.ts +9 -0
- package/dist/utils/mdx.d.ts.map +1 -0
- package/dist/utils/mdx.js +27 -0
- package/dist/utils/mdx.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +84 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/security.d.ts +2 -0
- package/dist/utils/security.d.ts.map +1 -0
- package/dist/utils/security.js +43 -0
- package/dist/utils/security.js.map +1 -0
- package/package.json +37 -19
- package/src/constants.ts +22 -0
- package/src/index.ts +51 -0
- package/src/prepare-docs/code-examples.ts +158 -0
- package/src/prepare-docs/copy-raw.ts +55 -0
- package/src/prepare-docs/prepare.ts +24 -0
- package/src/stdio.ts +7 -0
- package/src/tools/docs.ts +207 -0
- package/src/tools/examples.ts +107 -0
- package/src/tools/tests/docs.test.ts +122 -0
- package/src/tools/tests/examples.test.ts +94 -0
- package/src/tools/tests/integration.test.ts +46 -0
- package/src/tools/tests/json-parsing.test.ts +23 -0
- package/src/tools/tests/mcp-protocol.test.ts +133 -0
- package/src/tools/tests/path-traversal.test.ts +81 -0
- package/src/tools/tests/test-setup.ts +40 -0
- package/src/utils/logger.ts +20 -0
- package/src/utils/mcp-format.ts +12 -0
- package/src/utils/mdx.ts +39 -0
- package/src/utils/paths.ts +114 -0
- package/src/utils/security.ts +52 -0
- package/src/utils/tests/security.test.ts +119 -0
- package/dist/chunk-M2RKUM66.js +0 -38
- package/dist/chunk-NVNFQ5ZO.js +0 -423
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { server } from "../../index.js";
|
|
3
|
+
import {
|
|
4
|
+
InitializeRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
type InitializeRequest,
|
|
8
|
+
type ListToolsRequest,
|
|
9
|
+
type CallToolRequest,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
|
|
12
|
+
describe("MCP Protocol Integration", () => {
|
|
13
|
+
// These tests verify the MCP protocol layer handles requests correctly
|
|
14
|
+
// and that parameter schemas are properly converted to JSON schemas
|
|
15
|
+
it("should handle Initialize request", async () => {
|
|
16
|
+
const request: InitializeRequest = {
|
|
17
|
+
method: "initialize",
|
|
18
|
+
params: {
|
|
19
|
+
protocolVersion: "2024-11-05",
|
|
20
|
+
capabilities: {
|
|
21
|
+
tools: {},
|
|
22
|
+
},
|
|
23
|
+
clientInfo: {
|
|
24
|
+
name: "test-client",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Parse and validate the request
|
|
31
|
+
const parsed = InitializeRequestSchema.parse(request);
|
|
32
|
+
expect(parsed).toBeDefined();
|
|
33
|
+
|
|
34
|
+
// The server should have an initialize handler set up
|
|
35
|
+
const handlers = (server as any).server._requestHandlers;
|
|
36
|
+
expect(handlers).toBeDefined();
|
|
37
|
+
expect(handlers.get("initialize")).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should handle ListTools request", async () => {
|
|
41
|
+
const request: ListToolsRequest = {
|
|
42
|
+
method: "tools/list",
|
|
43
|
+
params: {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Parse and validate the request
|
|
47
|
+
const parsed = ListToolsRequestSchema.parse(request);
|
|
48
|
+
expect(parsed).toBeDefined();
|
|
49
|
+
|
|
50
|
+
// The server should have a tools/list handler
|
|
51
|
+
const handlers = (server as any).server._requestHandlers;
|
|
52
|
+
expect(handlers.get("tools/list")).toBeDefined();
|
|
53
|
+
|
|
54
|
+
// Call the handler
|
|
55
|
+
const handler = handlers.get("tools/list");
|
|
56
|
+
const result = await handler(parsed, {});
|
|
57
|
+
|
|
58
|
+
expect(result).toBeDefined();
|
|
59
|
+
expect(result.tools).toBeInstanceOf(Array);
|
|
60
|
+
expect(result.tools).toHaveLength(2);
|
|
61
|
+
|
|
62
|
+
// Check the tools have proper JSON schemas
|
|
63
|
+
const docsTool = result.tools.find(
|
|
64
|
+
(t: any) => t.name === "assistantUIDocs",
|
|
65
|
+
);
|
|
66
|
+
expect(docsTool).toBeDefined();
|
|
67
|
+
expect(docsTool.inputSchema).toBeDefined();
|
|
68
|
+
expect(docsTool.inputSchema.type).toBe("object");
|
|
69
|
+
expect(docsTool.inputSchema.properties).toBeDefined();
|
|
70
|
+
|
|
71
|
+
const examplesTool = result.tools.find(
|
|
72
|
+
(t: any) => t.name === "assistantUIExamples",
|
|
73
|
+
);
|
|
74
|
+
expect(examplesTool).toBeDefined();
|
|
75
|
+
expect(examplesTool.inputSchema).toBeDefined();
|
|
76
|
+
expect(examplesTool.inputSchema.type).toBe("object");
|
|
77
|
+
expect(examplesTool.inputSchema.properties).toBeDefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should handle CallTool request for assistantUIDocs", async () => {
|
|
81
|
+
const request: CallToolRequest = {
|
|
82
|
+
method: "tools/call",
|
|
83
|
+
params: {
|
|
84
|
+
name: "assistantUIDocs",
|
|
85
|
+
arguments: {
|
|
86
|
+
paths: ["/"],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Parse and validate the request
|
|
92
|
+
const parsed = CallToolRequestSchema.parse(request);
|
|
93
|
+
expect(parsed).toBeDefined();
|
|
94
|
+
|
|
95
|
+
// The server should have a tools/call handler
|
|
96
|
+
const handlers = (server as any).server._requestHandlers;
|
|
97
|
+
expect(handlers.get("tools/call")).toBeDefined();
|
|
98
|
+
|
|
99
|
+
// Call the handler through the MCP protocol layer
|
|
100
|
+
const handler = handlers.get("tools/call");
|
|
101
|
+
const result = await handler(parsed, {});
|
|
102
|
+
|
|
103
|
+
expect(result).toBeDefined();
|
|
104
|
+
expect(result.content).toBeDefined();
|
|
105
|
+
expect(result.content[0].type).toBe("text");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should handle CallTool request for assistantUIExamples with no arguments", async () => {
|
|
109
|
+
const request: CallToolRequest = {
|
|
110
|
+
method: "tools/call",
|
|
111
|
+
params: {
|
|
112
|
+
name: "assistantUIExamples",
|
|
113
|
+
arguments: {},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Parse and validate the request
|
|
118
|
+
const parsed = CallToolRequestSchema.parse(request);
|
|
119
|
+
expect(parsed).toBeDefined();
|
|
120
|
+
|
|
121
|
+
// The server should have a tools/call handler
|
|
122
|
+
const handlers = (server as any).server._requestHandlers;
|
|
123
|
+
expect(handlers.get("tools/call")).toBeDefined();
|
|
124
|
+
|
|
125
|
+
// Call the handler
|
|
126
|
+
const handler = handlers.get("tools/call");
|
|
127
|
+
const result = await handler(parsed, {});
|
|
128
|
+
|
|
129
|
+
expect(result).toBeDefined();
|
|
130
|
+
expect(result.content).toBeDefined();
|
|
131
|
+
expect(result.content[0].type).toBe("text");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { testContext } from "./test-setup.js";
|
|
3
|
+
|
|
4
|
+
describe("Path Traversal Security", () => {
|
|
5
|
+
describe("assistantUIDocs tool", () => {
|
|
6
|
+
const maliciousPaths = [
|
|
7
|
+
"../../../../etc/passwd",
|
|
8
|
+
"../../../package.json",
|
|
9
|
+
"..\\..\\..\\windows\\system32",
|
|
10
|
+
"/etc/passwd",
|
|
11
|
+
"docs/../../../sensitive-file",
|
|
12
|
+
"./../../private/keys",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
maliciousPaths.forEach((path) => {
|
|
16
|
+
it(`should block path traversal attempt: ${path}`, async () => {
|
|
17
|
+
const result = await testContext.callTool("assistantUIDocs", {
|
|
18
|
+
paths: [path],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(result.error).toBeDefined();
|
|
22
|
+
expect(result.error).toContain("Invalid path");
|
|
23
|
+
expect(result.content).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should handle multiple paths with one malicious", async () => {
|
|
28
|
+
const result = await testContext.callTool("assistantUIDocs", {
|
|
29
|
+
paths: ["getting-started", "../../../../etc/passwd", "guides"],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result.results).toBeDefined();
|
|
33
|
+
expect(result.results).toHaveLength(3);
|
|
34
|
+
|
|
35
|
+
expect(result.results[0].found).toBe(true);
|
|
36
|
+
expect(result.results[1].error).toContain("Invalid path");
|
|
37
|
+
expect(result.results[2].found).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("assistantUIExamples tool", () => {
|
|
42
|
+
const maliciousExamples = [
|
|
43
|
+
"../../../../etc/passwd",
|
|
44
|
+
"../../../src/index",
|
|
45
|
+
"..\\..\\..\\config",
|
|
46
|
+
"/root/.ssh/id_rsa",
|
|
47
|
+
"examples/../../../private",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
maliciousExamples.forEach((example) => {
|
|
51
|
+
it(`should block path traversal attempt: ${example}`, async () => {
|
|
52
|
+
const result = await testContext.callTool("assistantUIExamples", {
|
|
53
|
+
example,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.error).toBeDefined();
|
|
57
|
+
expect(result.error).toContain("Example not found");
|
|
58
|
+
expect(result.content).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("Valid paths should still work", () => {
|
|
64
|
+
it("should allow valid documentation paths", async () => {
|
|
65
|
+
const result = await testContext.callTool("assistantUIDocs", {
|
|
66
|
+
paths: ["getting-started", "api-reference/primitives/Thread"],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.results).toBeDefined();
|
|
70
|
+
expect(result.results).toHaveLength(2);
|
|
71
|
+
expect(result.results.every((r: any) => r.found || r.error)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should allow valid example names", async () => {
|
|
75
|
+
const result = await testContext.callTool("assistantUIExamples", {});
|
|
76
|
+
|
|
77
|
+
expect(result.examples).toBeDefined();
|
|
78
|
+
expect(Array.isArray(result.examples)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { beforeAll } from "vitest";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { PACKAGE_DIR } from "../../constants.js";
|
|
5
|
+
import { docsTools } from "../docs.js";
|
|
6
|
+
import { examplesTools } from "../examples.js";
|
|
7
|
+
|
|
8
|
+
const tools = {
|
|
9
|
+
assistantUIDocs: docsTools,
|
|
10
|
+
assistantUIExamples: examplesTools,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const testContext = {
|
|
14
|
+
callTool: async (name: string, args: any) => {
|
|
15
|
+
const tool = tools[name as keyof typeof tools];
|
|
16
|
+
if (!tool) {
|
|
17
|
+
throw new Error(`Tool ${name} not found`);
|
|
18
|
+
}
|
|
19
|
+
const result = await tool.execute(args);
|
|
20
|
+
|
|
21
|
+
const text = result.content?.[0]?.text;
|
|
22
|
+
if (text === undefined) {
|
|
23
|
+
throw new Error(`Tool ${name} returned no content`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Tool ${name} returned invalid JSON. Output: ${text}\nParse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
const docsPath = join(PACKAGE_DIR, ".docs");
|
|
37
|
+
if (!existsSync(docsPath)) {
|
|
38
|
+
throw new Error("Documentation not prepared. Run: pnpm build");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IS_PREPARE_MODE } from "../constants.js";
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
debug: (message: string, ...args: any[]) => {
|
|
5
|
+
if (process.env["DEBUG"]) {
|
|
6
|
+
console.debug(`[DEBUG] ${message}`, ...args);
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
info: (message: string, ...args: any[]) => {
|
|
10
|
+
if (IS_PREPARE_MODE) {
|
|
11
|
+
console.log(`[INFO] ${message}`, ...args);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
error: (message: string, ...args: any[]) => {
|
|
15
|
+
console.error(`[ERROR] ${message}`, ...args);
|
|
16
|
+
},
|
|
17
|
+
warn: (message: string, ...args: any[]) => {
|
|
18
|
+
console.warn(`[WARN] ${message}`, ...args);
|
|
19
|
+
},
|
|
20
|
+
};
|
package/src/utils/mdx.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import matter from "gray-matter";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
interface MDXContent {
|
|
6
|
+
content: string;
|
|
7
|
+
frontmatter: Record<string, any>;
|
|
8
|
+
excerpt?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function readMDXFile(
|
|
12
|
+
filePath: string,
|
|
13
|
+
): Promise<MDXContent | null> {
|
|
14
|
+
try {
|
|
15
|
+
const fileContent = await readFile(filePath, "utf-8");
|
|
16
|
+
const { content, data } = matter(fileContent);
|
|
17
|
+
|
|
18
|
+
const excerptMatch = content.match(/^(.+?)(?:\n\n|$)/);
|
|
19
|
+
const excerpt =
|
|
20
|
+
excerptMatch?.[1] !== undefined
|
|
21
|
+
? excerptMatch[1].replace(/^#+ /, "")
|
|
22
|
+
: undefined;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
content,
|
|
26
|
+
frontmatter: data,
|
|
27
|
+
...(excerpt !== undefined && { excerpt }),
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logger.error(`Failed to read MDX file: ${filePath}`, error);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatMDXContent(mdxContent: MDXContent): string {
|
|
36
|
+
const { content, frontmatter } = mdxContent;
|
|
37
|
+
|
|
38
|
+
return matter.stringify(content, frontmatter);
|
|
39
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, extname } from "node:path";
|
|
3
|
+
import { DOCS_PATH, MDX_EXTENSION, MD_EXTENSION } from "../constants.js";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
const SIMILARITY_THRESHOLDS = {
|
|
7
|
+
EXACT_MATCH: 1,
|
|
8
|
+
CONTAINS_MATCH: 0.8,
|
|
9
|
+
PARTIAL_MATCH: 0.5,
|
|
10
|
+
MIN_SUGGESTION: 0.3,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export async function listDirContents(dirPath: string): Promise<{
|
|
14
|
+
directories: string[];
|
|
15
|
+
files: string[];
|
|
16
|
+
}> {
|
|
17
|
+
try {
|
|
18
|
+
const items = await readdir(dirPath, { withFileTypes: true });
|
|
19
|
+
|
|
20
|
+
const directories = items
|
|
21
|
+
.filter((item) => item.isDirectory())
|
|
22
|
+
.map((item) => item.name)
|
|
23
|
+
.filter((name) => !name.startsWith("."))
|
|
24
|
+
.sort();
|
|
25
|
+
|
|
26
|
+
const files = items
|
|
27
|
+
.filter(
|
|
28
|
+
(item) =>
|
|
29
|
+
item.isFile() &&
|
|
30
|
+
(extname(item.name) === MDX_EXTENSION ||
|
|
31
|
+
extname(item.name) === MD_EXTENSION),
|
|
32
|
+
)
|
|
33
|
+
.map((item) => item.name)
|
|
34
|
+
.sort();
|
|
35
|
+
|
|
36
|
+
return { directories, files };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.error(`Failed to list directory contents: ${dirPath}`, error);
|
|
39
|
+
return { directories: [], files: [] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getAvailablePaths(): Promise<string[]> {
|
|
44
|
+
const paths: string[] = [];
|
|
45
|
+
|
|
46
|
+
async function scanDirectory(
|
|
47
|
+
dir: string,
|
|
48
|
+
prefix: string = "",
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const { directories, files } = await listDirContents(dir);
|
|
51
|
+
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const name = file.replace(MDX_EXTENSION, "").replace(MD_EXTENSION, "");
|
|
54
|
+
paths.push(prefix ? `${prefix}/${name}` : name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const subdir of directories) {
|
|
58
|
+
const subdirPath = join(dir, subdir);
|
|
59
|
+
const newPrefix = prefix ? `${prefix}/${subdir}` : subdir;
|
|
60
|
+
paths.push(newPrefix);
|
|
61
|
+
await scanDirectory(subdirPath, newPrefix);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await scanDirectory(DOCS_PATH);
|
|
66
|
+
return paths.sort();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function findNearestPaths(
|
|
70
|
+
requestedPath: string,
|
|
71
|
+
availablePaths: string[],
|
|
72
|
+
): string[] {
|
|
73
|
+
const normalizedRequest = requestedPath
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9]/g, "");
|
|
76
|
+
|
|
77
|
+
const scored = availablePaths.map((path) => {
|
|
78
|
+
const normalizedPath = path.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
79
|
+
|
|
80
|
+
if (normalizedPath.includes(normalizedRequest)) {
|
|
81
|
+
return { path, score: SIMILARITY_THRESHOLDS.EXACT_MATCH };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (normalizedRequest.includes(normalizedPath)) {
|
|
85
|
+
return { path, score: SIMILARITY_THRESHOLDS.CONTAINS_MATCH };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const overlap = [...normalizedRequest].filter((char) =>
|
|
89
|
+
normalizedPath.includes(char),
|
|
90
|
+
).length;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
path,
|
|
94
|
+
score:
|
|
95
|
+
(overlap / normalizedRequest.length) *
|
|
96
|
+
SIMILARITY_THRESHOLDS.PARTIAL_MATCH,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return scored
|
|
101
|
+
.filter((item) => item.score > SIMILARITY_THRESHOLDS.MIN_SUGGESTION)
|
|
102
|
+
.sort((a, b) => b.score - a.score)
|
|
103
|
+
.slice(0, 3)
|
|
104
|
+
.map((item) => item.path);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function pathExists(path: string): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
await stat(path);
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function sanitizePath(userPath: string): string {
|
|
4
|
+
if (!userPath || typeof userPath !== "string") {
|
|
5
|
+
throw new Error("Invalid path: Path must be a non-empty string");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Check for traversal patterns before normalization - defense in depth
|
|
9
|
+
if (userPath.includes("../") || userPath.includes("..\\")) {
|
|
10
|
+
throw new Error("Invalid path: Directory traversal attempt detected");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const normalized = path.normalize(userPath);
|
|
14
|
+
|
|
15
|
+
if (path.isAbsolute(normalized)) {
|
|
16
|
+
throw new Error("Invalid path: Absolute paths are not allowed");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const relativePath = path.relative("", normalized);
|
|
20
|
+
|
|
21
|
+
if (relativePath.startsWith("..")) {
|
|
22
|
+
throw new Error("Invalid path: Directory traversal attempt detected");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (relativePath.includes("..")) {
|
|
26
|
+
throw new Error("Invalid path: Path contains invalid traversal sequences");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (relativePath.includes("\0")) {
|
|
30
|
+
throw new Error("Invalid path: Path contains null bytes");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (process.platform !== "win32") {
|
|
34
|
+
if (normalized.includes("\\")) {
|
|
35
|
+
throw new Error("Invalid path: Backslashes not allowed");
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
if (normalized.includes(":") || normalized.startsWith("\\\\")) {
|
|
39
|
+
throw new Error("Invalid path: Path contains invalid Windows characters");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const segments = relativePath.split(path.sep);
|
|
44
|
+
for (const segment of segments) {
|
|
45
|
+
if (segment.startsWith(".") && segment !== ".") {
|
|
46
|
+
throw new Error("Invalid path: Hidden files are not allowed");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Always return Unix-style paths for consistency across platforms
|
|
51
|
+
return relativePath.replace(/\\/g, "/");
|
|
52
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { sanitizePath } from "../security.js";
|
|
3
|
+
|
|
4
|
+
describe("sanitizePath", () => {
|
|
5
|
+
describe("should reject directory traversal attempts", () => {
|
|
6
|
+
const maliciousPaths = [
|
|
7
|
+
"../../../etc/passwd",
|
|
8
|
+
"..\\..\\windows\\system32",
|
|
9
|
+
"/etc/passwd",
|
|
10
|
+
"/absolute/path",
|
|
11
|
+
"docs/../../../etc/passwd",
|
|
12
|
+
"valid/../../../../../../etc/passwd",
|
|
13
|
+
"../",
|
|
14
|
+
"..",
|
|
15
|
+
".../.../",
|
|
16
|
+
"foo/../../bar",
|
|
17
|
+
"./../../etc/passwd",
|
|
18
|
+
"path/with/../../../traversal",
|
|
19
|
+
"path/to/\0/null",
|
|
20
|
+
"path\\to\\..\\..\\file",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
maliciousPaths.push("C:\\Windows\\System32", "\\\\server\\share");
|
|
25
|
+
} else {
|
|
26
|
+
maliciousPaths.push("C:\\Windows\\System32", "\\\\server\\share");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
maliciousPaths.forEach((path) => {
|
|
30
|
+
it(`should reject: ${path}`, () => {
|
|
31
|
+
expect(() => sanitizePath(path)).toThrow();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("should reject invalid inputs", () => {
|
|
37
|
+
it("should reject empty string", () => {
|
|
38
|
+
expect(() => sanitizePath("")).toThrow(
|
|
39
|
+
"Invalid path: Path must be a non-empty string",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should reject null", () => {
|
|
44
|
+
expect(() => sanitizePath(null as any)).toThrow(
|
|
45
|
+
"Invalid path: Path must be a non-empty string",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should reject undefined", () => {
|
|
50
|
+
expect(() => sanitizePath(undefined as any)).toThrow(
|
|
51
|
+
"Invalid path: Path must be a non-empty string",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should reject numbers", () => {
|
|
56
|
+
expect(() => sanitizePath(123 as any)).toThrow(
|
|
57
|
+
"Invalid path: Path must be a non-empty string",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("should reject hidden files", () => {
|
|
63
|
+
const hiddenPaths = [
|
|
64
|
+
".hidden",
|
|
65
|
+
".git/config",
|
|
66
|
+
"docs/.secret",
|
|
67
|
+
"path/to/.env",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
hiddenPaths.forEach((path) => {
|
|
71
|
+
it(`should reject: ${path}`, () => {
|
|
72
|
+
expect(() => sanitizePath(path)).toThrow(
|
|
73
|
+
"Invalid path: Hidden files are not allowed",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("should allow valid paths", () => {
|
|
80
|
+
const validPaths = [
|
|
81
|
+
{ input: "getting-started", expected: "getting-started" },
|
|
82
|
+
{
|
|
83
|
+
input: "api-reference/primitives/Thread",
|
|
84
|
+
expected: "api-reference/primitives/Thread",
|
|
85
|
+
},
|
|
86
|
+
{ input: "guides/tools", expected: "guides/tools" },
|
|
87
|
+
{ input: "docs/index", expected: "docs/index" },
|
|
88
|
+
{ input: "examples/with-ai-sdk", expected: "examples/with-ai-sdk" },
|
|
89
|
+
{ input: "./current-dir-file", expected: "current-dir-file" },
|
|
90
|
+
{
|
|
91
|
+
input: "deeply/nested/path/to/file",
|
|
92
|
+
expected: "deeply/nested/path/to/file",
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
validPaths.forEach(({ input, expected }) => {
|
|
97
|
+
it(`should allow: ${input} => ${expected}`, () => {
|
|
98
|
+
expect(sanitizePath(input)).toBe(expected);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (process.platform === "win32") {
|
|
104
|
+
describe("Windows-specific tests", () => {
|
|
105
|
+
it("should reject Windows drive letters", () => {
|
|
106
|
+
expect(() => sanitizePath("C:")).toThrow(
|
|
107
|
+
"Invalid path: Path contains invalid Windows characters",
|
|
108
|
+
);
|
|
109
|
+
expect(() => sanitizePath("D:\\file")).toThrow();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should reject UNC paths", () => {
|
|
113
|
+
expect(() => sanitizePath("\\\\server\\share")).toThrow(
|
|
114
|
+
"Invalid path: Absolute paths are not allowed",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
package/dist/chunk-M2RKUM66.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'url';
|
|
2
|
-
import { dirname, join } from 'path';
|
|
3
|
-
|
|
4
|
-
// src/constants.ts
|
|
5
|
-
var __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
var ROOT_DIR = join(__dirname$1, "../../../");
|
|
7
|
-
var PACKAGE_DIR = join(__dirname$1, "../");
|
|
8
|
-
var EXAMPLES_PATH = join(ROOT_DIR, "examples");
|
|
9
|
-
var DOCS_BASE = join(PACKAGE_DIR, ".docs");
|
|
10
|
-
var DOCS_PATH = join(DOCS_BASE, "raw/docs");
|
|
11
|
-
join(DOCS_BASE, "raw/blog");
|
|
12
|
-
var CODE_EXAMPLES_PATH = join(DOCS_BASE, "organized/code-examples");
|
|
13
|
-
var MDX_EXTENSION = ".mdx";
|
|
14
|
-
var MD_EXTENSION = ".md";
|
|
15
|
-
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
16
|
-
var IS_PREPARE_MODE = process.env.PREPARE === "true";
|
|
17
|
-
|
|
18
|
-
// src/utils/logger.ts
|
|
19
|
-
var logger = {
|
|
20
|
-
debug: (message, ...args) => {
|
|
21
|
-
if (process.env.DEBUG) {
|
|
22
|
-
console.debug(`[DEBUG] ${message}`, ...args);
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
info: (message, ...args) => {
|
|
26
|
-
if (process.env.PREPARE === "true") {
|
|
27
|
-
console.log(`[INFO] ${message}`, ...args);
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
error: (message, ...args) => {
|
|
31
|
-
console.error(`[ERROR] ${message}`, ...args);
|
|
32
|
-
},
|
|
33
|
-
warn: (message, ...args) => {
|
|
34
|
-
console.warn(`[WARN] ${message}`, ...args);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export { CODE_EXAMPLES_PATH, DOCS_PATH, EXAMPLES_PATH, IS_PREPARE_MODE, MAX_FILE_SIZE, MDX_EXTENSION, MD_EXTENSION, ROOT_DIR, logger };
|