zen-gitsync 2.11.39 → 2.12.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.
Files changed (27) hide show
  1. package/README.md +695 -695
  2. package/package.json +1 -1
  3. package/src/ui/public/assets/EditorView-CHBjgiZc.css +1 -0
  4. package/src/ui/public/assets/EditorView-bnJmBq-i.js +21 -0
  5. package/src/ui/public/assets/SourceMapView-DhQX0K7t.css +1 -0
  6. package/src/ui/public/assets/SourceMapView-Rz5SD0A0.js +3 -0
  7. package/src/ui/public/assets/index-Bo3tntQh.js +73 -0
  8. package/src/ui/public/assets/{index-DXO3Lvqi.css → index-bOs5P8fz.css} +1 -1
  9. package/src/ui/public/assets/{ts.worker-Dth06zuC.js → ts.worker-METxwbDZ.js} +1 -16
  10. package/src/ui/public/assets/{vendor-B1T2uxYO.js → vendor-DITsiaGj.js} +294 -287
  11. package/src/ui/public/assets/vendor-q83wvJns.css +1 -0
  12. package/src/ui/public/index.html +4 -4
  13. package/src/ui/server/.claude/codediff.txt +6 -0
  14. package/src/ui/server/routes/fs.js +33 -45
  15. package/src/ui/server/routes/instances.js +24 -24
  16. package/src/ui/server/utils/instanceRegistry.js +256 -256
  17. package/src/ui/server/utils/pathGuard.js +141 -0
  18. package/src/ui/server/utils/pathGuard.test.js +124 -0
  19. package/src/ui/server/utils/randomStartPort.js +37 -37
  20. package/src/utils/index.js +1044 -1044
  21. package/src/ui/public/assets/devopicons-QN4QXivI.woff2 +0 -0
  22. package/src/ui/public/assets/file-icons-C0jOugUK.woff2 +0 -0
  23. package/src/ui/public/assets/fontawesome-B-jkhYfk.woff2 +0 -0
  24. package/src/ui/public/assets/index-BvVl-092.js +0 -95
  25. package/src/ui/public/assets/mfixx-CpAhKOZz.woff2 +0 -0
  26. package/src/ui/public/assets/octicons-CaZ_fok2.woff2 +0 -0
  27. package/src/ui/public/assets/vendor-hOO_r_AU.css +0 -1
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import open from 'open';
6
6
  import os from 'os';
7
7
  import { spawn, exec } from 'child_process';
8
+ import { ensureWithinCwd } from '../utils/pathGuard.js';
8
9
 
9
10
  export function registerFsRoutes({
10
11
  app,
@@ -17,6 +18,12 @@ export function registerFsRoutes({
17
18
  setProjectRoomId,
18
19
  setIsGitRepo
19
20
  }) {
21
+ // ── 解析并校验 user 输入路径在当前项目 cwd 内(防 ../ 父目录逃逸、startsWith 假阳性、Windows 大小写)──
22
+ const safePathInProject = async (userPath) => {
23
+ const cwd = getCurrentProjectPath() || process.cwd()
24
+ return ensureWithinCwd(userPath, cwd)
25
+ }
26
+
20
27
  // 新增获取当前工作目录接口
21
28
  app.get('/api/current_directory', async (req, res) => {
22
29
  try {
@@ -497,12 +504,9 @@ export function registerFsRoutes({
497
504
  try {
498
505
  const filePath = req.query.path;
499
506
  if (!filePath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
500
- // 安全:只允许读取当前工作目录内的文件
501
- const cwd = getCurrentProjectPath() || process.cwd();
502
- const resolved = path.resolve(filePath);
503
- if (!resolved.startsWith(path.resolve(cwd))) {
504
- return res.status(403).json({ success: false, error: '禁止访问工作目录以外的文件' });
505
- }
507
+ const safe = await safePathInProject(filePath);
508
+ if (!safe) return res.status(403).json({ success: false, error: '禁止访问工作目录以外的文件' });
509
+ const resolved = safe.safePath;
506
510
  const stat = await fs.stat(resolved);
507
511
  if (!stat.isFile()) return res.status(400).json({ success: false, error: '目标不是文件' });
508
512
  // 超过 2MB 不读取
@@ -521,11 +525,9 @@ export function registerFsRoutes({
521
525
  try {
522
526
  const filePath = req.query.path;
523
527
  if (!filePath) return res.status(400).end();
524
- const cwd = getCurrentProjectPath() || process.cwd();
525
- const resolved = path.resolve(filePath);
526
- if (!resolved.startsWith(path.resolve(cwd))) {
527
- return res.status(403).end();
528
- }
528
+ const safe = await safePathInProject(filePath);
529
+ if (!safe) return res.status(403).end();
530
+ const resolved = safe.safePath;
529
531
  const stat = await fs.stat(resolved);
530
532
  if (!stat.isFile()) return res.status(400).end();
531
533
  if (stat.size > 20 * 1024 * 1024) return res.status(413).end();
@@ -552,12 +554,9 @@ export function registerFsRoutes({
552
554
  if (!filePath || content === undefined) {
553
555
  return res.status(400).json({ success: false, error: '缺少 path 或 content 参数' });
554
556
  }
555
- const cwd = getCurrentProjectPath() || process.cwd();
556
- const resolved = path.resolve(filePath);
557
- if (!resolved.startsWith(path.resolve(cwd))) {
558
- return res.status(403).json({ success: false, error: '禁止写入工作目录以外的文件' });
559
- }
560
- await fs.writeFile(resolved, content, 'utf-8');
557
+ const safe = await safePathInProject(filePath);
558
+ if (!safe) return res.status(403).json({ success: false, error: '禁止写入工作目录以外的文件' });
559
+ await fs.writeFile(safe.safePath, content, 'utf-8');
561
560
  res.json({ success: true });
562
561
  } catch (error) {
563
562
  res.status(500).json({ success: false, error: error.message });
@@ -569,11 +568,9 @@ export function registerFsRoutes({
569
568
  try {
570
569
  const { path: filePath } = req.body;
571
570
  if (!filePath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
572
- const cwd = getCurrentProjectPath() || process.cwd();
573
- const resolved = path.resolve(filePath);
574
- if (!resolved.startsWith(path.resolve(cwd))) {
575
- return res.status(403).json({ success: false, error: '禁止在工作目录以外创建文件' });
576
- }
571
+ const safe = await safePathInProject(filePath);
572
+ if (!safe) return res.status(403).json({ success: false, error: '禁止在工作目录以外创建文件' });
573
+ const resolved = safe.safePath;
577
574
  // 如果已存在则拒绝
578
575
  try {
579
576
  await fs.access(resolved);
@@ -592,11 +589,9 @@ export function registerFsRoutes({
592
589
  try {
593
590
  const { path: dirPath } = req.body;
594
591
  if (!dirPath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
595
- const cwd = getCurrentProjectPath() || process.cwd();
596
- const resolved = path.resolve(dirPath);
597
- if (!resolved.startsWith(path.resolve(cwd))) {
598
- return res.status(403).json({ success: false, error: '禁止在工作目录以外创建目录' });
599
- }
592
+ const safe = await safePathInProject(dirPath);
593
+ if (!safe) return res.status(403).json({ success: false, error: '禁止在工作目录以外创建目录' });
594
+ const resolved = safe.safePath;
600
595
  try {
601
596
  await fs.access(resolved);
602
597
  return res.status(409).json({ success: false, error: '目录已存在' });
@@ -613,11 +608,9 @@ export function registerFsRoutes({
613
608
  try {
614
609
  const filePath = req.query.path;
615
610
  if (!filePath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
616
- const cwd = getCurrentProjectPath() || process.cwd();
617
- const resolved = path.resolve(filePath);
618
- if (!resolved.startsWith(path.resolve(cwd))) {
619
- return res.status(403).json({ success: false, error: '禁止删除工作目录以外的内容' });
620
- }
611
+ const safe = await safePathInProject(filePath);
612
+ if (!safe) return res.status(403).json({ success: false, error: '禁止删除工作目录以外的内容' });
613
+ const resolved = safe.safePath;
621
614
  const stat = await fs.stat(resolved);
622
615
  if (stat.isDirectory()) {
623
616
  await fs.rm(resolved, { recursive: true, force: true });
@@ -635,17 +628,14 @@ export function registerFsRoutes({
635
628
  try {
636
629
  const { oldPath, newPath } = req.body;
637
630
  if (!oldPath || !newPath) return res.status(400).json({ success: false, error: '缺少参数' });
638
- const cwd = getCurrentProjectPath() || process.cwd();
639
- const resolvedOld = path.resolve(oldPath);
640
- const resolvedNew = path.resolve(newPath);
641
- if (!resolvedOld.startsWith(path.resolve(cwd)) || !resolvedNew.startsWith(path.resolve(cwd))) {
642
- return res.status(403).json({ success: false, error: '禁止操作工作目录以外的内容' });
643
- }
631
+ const safeOld = await safePathInProject(oldPath)
632
+ const safeNew = await safePathInProject(newPath)
633
+ if (!safeOld || !safeNew) return res.status(403).json({ success: false, error: '禁止操作工作目录以外的内容' });
644
634
  try {
645
- await fs.access(resolvedNew);
635
+ await fs.access(safeNew.safePath);
646
636
  return res.status(409).json({ success: false, error: '目标名称已存在' });
647
637
  } catch { /* 不存在,继续 */ }
648
- await fs.rename(resolvedOld, resolvedNew);
638
+ await fs.rename(safeOld.safePath, safeNew.safePath);
649
639
  res.json({ success: true });
650
640
  } catch (error) {
651
641
  res.status(500).json({ success: false, error: error.message });
@@ -661,11 +651,9 @@ export function registerFsRoutes({
661
651
  const targetPath = req.body?.path;
662
652
  if (!targetPath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
663
653
 
664
- const cwd = getCurrentProjectPath() || process.cwd();
665
- const resolved = path.resolve(targetPath);
666
- if (!resolved.startsWith(path.resolve(cwd))) {
667
- return res.status(403).json({ success: false, error: '禁止访问工作目录以外的内容' });
668
- }
654
+ const safe = await safePathInProject(targetPath);
655
+ if (!safe) return res.status(403).json({ success: false, error: '禁止访问工作目录以外的内容' });
656
+ const resolved = safe.safePath;
669
657
 
670
658
  try {
671
659
  await fs.access(resolved);
@@ -1,24 +1,24 @@
1
- // 实例注册表 API 路由
2
- // 当前只暴露只读列表;停止/启停他人实例属于 out-of-scope(v1 只做跳转导航)
3
-
4
- export function registerInstancesRoutes({ app, registry, getCurrentInstanceId }) {
5
- // 获取所有活跃实例(自动 prune 失效条目)
6
- app.get('/api/instances', async (req, res) => {
7
- try {
8
- const instances = await registry.list({ pruneStale: true });
9
- const currentInstanceId = typeof getCurrentInstanceId === 'function'
10
- ? getCurrentInstanceId()
11
- : null;
12
- res.json({
13
- success: true,
14
- instances,
15
- currentInstanceId
16
- });
17
- } catch (error) {
18
- res.status(500).json({
19
- success: false,
20
- error: error?.message || String(error)
21
- });
22
- }
23
- });
24
- }
1
+ // 实例注册表 API 路由
2
+ // 当前只暴露只读列表;停止/启停他人实例属于 out-of-scope(v1 只做跳转导航)
3
+
4
+ export function registerInstancesRoutes({ app, registry, getCurrentInstanceId }) {
5
+ // 获取所有活跃实例(自动 prune 失效条目)
6
+ app.get('/api/instances', async (req, res) => {
7
+ try {
8
+ const instances = await registry.list({ pruneStale: true });
9
+ const currentInstanceId = typeof getCurrentInstanceId === 'function'
10
+ ? getCurrentInstanceId()
11
+ : null;
12
+ res.json({
13
+ success: true,
14
+ instances,
15
+ currentInstanceId
16
+ });
17
+ } catch (error) {
18
+ res.status(500).json({
19
+ success: false,
20
+ error: error?.message || String(error)
21
+ });
22
+ }
23
+ });
24
+ }