@mintlify/cli 4.0.900 → 4.0.902
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/__test__/init.test.ts +82 -0
- package/__test__/openApiCheck.test.ts +17 -0
- package/bin/helpers.js +8 -2
- package/bin/init.js +13 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/helpers.tsx +8 -2
- package/src/init.tsx +15 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { init } from '../src/init.js';
|
|
2
|
+
|
|
3
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
4
|
+
select: vi.fn(),
|
|
5
|
+
input: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('@mintlify/previewing', () => ({
|
|
9
|
+
addLogs: vi.fn(),
|
|
10
|
+
addLog: vi.fn(),
|
|
11
|
+
SpinnerLog: vi.fn(),
|
|
12
|
+
removeLastLog: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@mintlify/validation', () => ({
|
|
16
|
+
docsConfigSchema: {
|
|
17
|
+
options: [{ shape: { theme: { _def: { value: 'quill' } } } }],
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('fs-extra', () => ({
|
|
22
|
+
default: {
|
|
23
|
+
readdir: vi.fn().mockResolvedValue([]),
|
|
24
|
+
ensureDir: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
copy: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
remove: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
readJson: vi.fn().mockResolvedValue({ theme: 'quill', name: 'Test' }),
|
|
29
|
+
writeJson: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('adm-zip', () => ({
|
|
34
|
+
default: vi.fn().mockImplementation(() => ({
|
|
35
|
+
extractAllTo: vi.fn(),
|
|
36
|
+
})),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
40
|
+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('init', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('path traversal prevention', () => {
|
|
49
|
+
it('rejects path traversal with ../', async () => {
|
|
50
|
+
await expect(init('../outside', false, 'quill', 'Test')).rejects.toThrow(
|
|
51
|
+
'Access denied: path "../outside" is outside the current directory'
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('rejects deep path traversal', async () => {
|
|
56
|
+
await expect(init('../../../etc', false, 'quill', 'Test')).rejects.toThrow(
|
|
57
|
+
'Access denied: path "../../../etc" is outside the current directory'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects absolute paths outside cwd', async () => {
|
|
62
|
+
await expect(init('/etc/test', false, 'quill', 'Test')).rejects.toThrow(
|
|
63
|
+
'Access denied: path "/etc/test" is outside the current directory'
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('allows current directory (.)', async () => {
|
|
68
|
+
// Should not throw for current directory
|
|
69
|
+
await expect(init('.', false, 'quill', 'Test')).resolves.not.toThrow();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('allows subdirectory paths', async () => {
|
|
73
|
+
// Should not throw for subdirectory
|
|
74
|
+
await expect(init('docs', false, 'quill', 'Test')).resolves.not.toThrow();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('allows nested subdirectory paths', async () => {
|
|
78
|
+
// Should not throw for nested subdirectory
|
|
79
|
+
await expect(init('docs/api', false, 'quill', 'Test')).resolves.not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -125,3 +125,20 @@ describe('openApiCheck', () => {
|
|
|
125
125
|
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
describe('readLocalOpenApiFile', () => {
|
|
130
|
+
it('rejects path traversal attempts', async () => {
|
|
131
|
+
const { readLocalOpenApiFile } =
|
|
132
|
+
await vi.importActual<typeof import('../src/helpers.js')>('../src/helpers.js');
|
|
133
|
+
|
|
134
|
+
await expect(readLocalOpenApiFile('../etc/passwd')).rejects.toThrow(
|
|
135
|
+
'Access denied: invalid path'
|
|
136
|
+
);
|
|
137
|
+
await expect(readLocalOpenApiFile('/etc/passwd')).rejects.toThrow(
|
|
138
|
+
'Access denied: invalid path'
|
|
139
|
+
);
|
|
140
|
+
await expect(readLocalOpenApiFile('../../secret.yaml')).rejects.toThrow(
|
|
141
|
+
'Access denied: invalid path'
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/bin/helpers.js
CHANGED
|
@@ -143,8 +143,14 @@ export const suppressConsoleWarnings = () => {
|
|
|
143
143
|
};
|
|
144
144
|
};
|
|
145
145
|
export const readLocalOpenApiFile = (filename) => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
-
const
|
|
147
|
-
const
|
|
146
|
+
const baseDir = process.cwd();
|
|
147
|
+
// const pathname = path.resolve(process.cwd(), filename);
|
|
148
|
+
const resolvedPath = path.resolve(baseDir, filename);
|
|
149
|
+
const relative = path.relative(baseDir, resolvedPath);
|
|
150
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
151
|
+
throw new Error('Access denied: invalid path');
|
|
152
|
+
}
|
|
153
|
+
const file = yield fs.readFile(resolvedPath, 'utf-8');
|
|
148
154
|
const document = yaml.load(file);
|
|
149
155
|
return document;
|
|
150
156
|
});
|
package/bin/init.js
CHANGED
|
@@ -14,6 +14,15 @@ import { docsConfigSchema } from '@mintlify/validation';
|
|
|
14
14
|
import AdmZip from 'adm-zip';
|
|
15
15
|
import fse from 'fs-extra';
|
|
16
16
|
import { Box, Text } from 'ink';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
const validatePathWithinCwd = (inputPath) => {
|
|
19
|
+
const baseDir = process.cwd();
|
|
20
|
+
const resolvedPath = path.resolve(baseDir, inputPath);
|
|
21
|
+
const relative = path.relative(baseDir, resolvedPath);
|
|
22
|
+
if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
|
|
23
|
+
throw new Error(`Access denied: path "${inputPath}" is outside the current directory`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
17
26
|
const sendOnboardingMessage = (installDir) => {
|
|
18
27
|
addLogs(_jsx(Text, { bold: true, children: "Documentation Setup!" }), _jsx(Text, { children: "To see your docs run" }), _jsxs(Box, { children: [_jsx(Text, { color: "blue", children: "cd" }), _jsxs(Text, { children: [" ", installDir] })] }), _jsx(Text, { color: "blue", children: "mint dev" }));
|
|
19
28
|
};
|
|
@@ -22,6 +31,8 @@ const sendUsageMessageForAI = (directory, contentsOccupied, themes) => {
|
|
|
22
31
|
};
|
|
23
32
|
export function init(installDir, force, theme, name) {
|
|
24
33
|
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
// Validate path is within current working directory to prevent path traversal
|
|
35
|
+
validatePathWithinCwd(installDir);
|
|
25
36
|
const isInteractive = process.stdin.isTTY;
|
|
26
37
|
const isClaudeCode = process.env.CLAUDECODE === '1';
|
|
27
38
|
const isAI = !isInteractive || isClaudeCode;
|
|
@@ -62,6 +73,8 @@ export function init(installDir, force, theme, name) {
|
|
|
62
73
|
throw new Error('Subdirectory name cannot be empty');
|
|
63
74
|
}
|
|
64
75
|
installDir = installDir === '.' ? subdir : `${installDir}/${subdir}`;
|
|
76
|
+
// Re-validate after subdirectory is appended
|
|
77
|
+
validatePathWithinCwd(installDir);
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
if (!isAI && (!selectedTheme || !projectName)) {
|