dsh-over-ssh 0.2.1 → 0.2.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/lib/fs.js CHANGED
@@ -630,6 +630,20 @@ async function readBody(req, maxBytes) {
630
630
  }
631
631
  return Buffer.concat(chunks).toString("utf8");
632
632
  }
633
+ async function readJsonObject(req, maxBytes) {
634
+ const text = (await readBody(req, maxBytes)).trim();
635
+ if (text === "") return {};
636
+ let parsed;
637
+ try {
638
+ parsed = JSON.parse(text);
639
+ } catch {
640
+ throw new SshDevError("ROOT_INVALID", "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON");
641
+ }
642
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
643
+ throw new SshDevError("ROOT_INVALID", "\u8BF7\u6C42\u4F53\u9876\u5C42\u5FC5\u987B\u662F\u5BF9\u8C61");
644
+ }
645
+ return parsed;
646
+ }
633
647
  function toBytes(chunk) {
634
648
  if (typeof chunk === "string") return Buffer.from(chunk, "utf8");
635
649
  if (chunk instanceof Uint8Array) return chunk;
@@ -650,6 +664,38 @@ function asRecord(value) {
650
664
  function messageOf(error) {
651
665
  return error instanceof Error ? error.message : String(error);
652
666
  }
667
+ function reasonOf(error) {
668
+ const reason = asRecord(asRecord(error)?.["details"])?.["reason"];
669
+ return typeof reason === "string" && reason !== "" ? reason : void 0;
670
+ }
671
+ var STATUS_BY_CODE = {
672
+ // 输入问题:调用方改一下请求就能成功。
673
+ ROOT_INVALID: 400,
674
+ CONNECTION_INVALID: 400,
675
+ CONNECTION_NOT_FOUND: 400,
676
+ // 路径不在任何已注册的根内 / 根没有注册:客户端换一个路径或先加根就能成功,不是服务器故障。
677
+ // 它此前漏在表外,于是**所有**经 HTTP 暴露的根外访问都报 500(审计 2026-11-06 第 4 轮 P1)。
678
+ ROOT_OUTSIDE: 400,
679
+ // 目标不存在 / 状态冲突 / 本组合不支持:客户端可以据此换一条路。
680
+ TRASH_NOT_FOUND: 404,
681
+ TRANSFER_CONFLICT: 409,
682
+ SESSION_ACTIVE: 409,
683
+ REMOTE_UNSUPPORTED: 501,
684
+ // 调用方主动取消(客户端断开/关掉面板)。499 是 nginx 的 "client closed request" 约定:它不是一个错误,
685
+ // 只是说明"没人等这个回答了";映射成 500 会让日志里出现一堆假故障。
686
+ CANCELLED: 499
687
+ };
688
+ function codeOf(error) {
689
+ const code = asRecord(error)?.["code"];
690
+ return typeof code === "string" && code !== "" ? code : void 0;
691
+ }
692
+ function statusForCode(code) {
693
+ if (code === void 0) return 500;
694
+ return STATUS_BY_CODE[code] ?? 500;
695
+ }
696
+ function statusForError(error) {
697
+ return statusForCode(codeOf(error));
698
+ }
653
699
 
654
700
  // src/host/infra/transport/git-status-route.ts
655
701
  var GIT_STATUS_API_PATH2 = GIT_STATUS_API_PATH;
@@ -668,9 +714,9 @@ function toPublicAnswer(snapshot) {
668
714
  function createGitStatusApi(deps) {
669
715
  const handle = async (req, res) => {
670
716
  const method = (req.method ?? "GET").toUpperCase();
671
- if (method !== "POST" && method !== "GET" && method !== "HEAD") {
672
- res.setHeader("allow", "GET, HEAD, POST");
673
- send(req, res, 405, { ok: false, error: `\u4E0D\u652F\u6301\u7684\u65B9\u6CD5\uFF1A${method}` });
717
+ if (method !== "POST") {
718
+ res.setHeader("allow", "POST");
719
+ send(req, res, 405, { ok: false, error: "git-status \u53EA\u63A5\u53D7 POST" });
674
720
  return;
675
721
  }
676
722
  const verdict = trustVerdict(req);
@@ -678,13 +724,8 @@ function createGitStatusApi(deps) {
678
724
  send(req, res, 403, { ok: false, error: trustMessage(verdict) });
679
725
  return;
680
726
  }
681
- if (method !== "POST") {
682
- res.setHeader("allow", "POST");
683
- send(req, res, 405, { ok: false, error: "git-status \u53EA\u63A5\u53D7 POST" });
684
- return;
685
- }
686
727
  try {
687
- const body = asRecord(JSON.parse(await readBody(req, ROOTS_API_MAX_BODY_BYTES)));
728
+ const body = await readJsonObject(req, ROOTS_API_MAX_BODY_BYTES);
688
729
  const connectionId = body?.["connectionId"];
689
730
  const path = body?.["path"];
690
731
  if (typeof connectionId !== "string" || connectionId === "") {
@@ -708,8 +749,14 @@ function createGitStatusApi(deps) {
708
749
  }
709
750
  send(req, res, 200, toPublicAnswer(snapshot));
710
751
  } catch (error) {
711
- const invalidBody = error instanceof SyntaxError;
712
- send(req, res, invalidBody ? 400 : 500, { ok: false, error: messageOf(error) });
752
+ const code = codeOf(error);
753
+ const reason = reasonOf(error);
754
+ send(req, res, statusForError(error), {
755
+ ok: false,
756
+ error: messageOf(error),
757
+ ...code === void 0 ? {} : { code },
758
+ ...reason === void 0 ? {} : { reason }
759
+ });
713
760
  }
714
761
  };
715
762
  return { handle };
@@ -780,6 +827,16 @@ function replaceLiteral(source, needle, replacement, all) {
780
827
 
781
828
  // src/host/integration/session-scope.ts
782
829
  import { AsyncLocalStorage } from "node:async_hooks";
830
+
831
+ // src/host/integration/session-fork-pending.ts
832
+ var pending = /* @__PURE__ */ new Map();
833
+ function pendingForkSources(sessionId) {
834
+ if (sessionId === void 0 || sessionId === "") return [];
835
+ const source = pending.get(sessionId);
836
+ return source === void 0 ? [] : [source];
837
+ }
838
+
839
+ // src/host/integration/session-scope.ts
783
840
  var STORE_KEY = /* @__PURE__ */ Symbol.for("dsh-over-ssh/session-scope");
784
841
  function store() {
785
842
  const holder = globalThis;
@@ -796,6 +853,15 @@ var MAX_FORK_DEPTH = 32;
796
853
  var ancestorCache = /* @__PURE__ */ new Map();
797
854
  var ANCESTOR_CACHE_MAX = 256;
798
855
  function readSessionAncestors(ctx, sessionId) {
856
+ const pending2 = pendingForkSources(sessionId);
857
+ if (pending2.length > 0) {
858
+ const inherited = /* @__PURE__ */ new Set();
859
+ for (const source of pending2) {
860
+ inherited.add(source);
861
+ for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
862
+ }
863
+ return [...inherited];
864
+ }
799
865
  const cached = ancestorCache.get(sessionId);
800
866
  if (cached !== void 0) {
801
867
  ancestorCache.delete(sessionId);
@@ -857,7 +923,7 @@ function readSessionCwd(ctx, sessionId) {
857
923
  }
858
924
  return found.cwd;
859
925
  }
860
- function readSessionCwds(ctx) {
926
+ function readSessionWorkspaces(ctx) {
861
927
  let list;
862
928
  try {
863
929
  const sessions = ctx.get("sessions");
@@ -866,12 +932,14 @@ function readSessionCwds(ctx) {
866
932
  return [];
867
933
  }
868
934
  if (!Array.isArray(list)) return [];
869
- const cwds = [];
935
+ const pairs = [];
870
936
  for (const session of list) {
937
+ const id = session?.header?.id;
871
938
  const cwd = session?.header?.cwd;
872
- if (typeof cwd === "string" && cwd !== "") cwds.push(cwd);
939
+ if (typeof id !== "string" || id === "" || typeof cwd !== "string" || cwd === "") continue;
940
+ pairs.push({ sessionId: id, cwd });
873
941
  }
874
- return cwds;
942
+ return pairs;
875
943
  }
876
944
  var sessionCwdCache = /* @__PURE__ */ new Map();
877
945
  var SESSION_CWD_CACHE_MAX = 256;
@@ -940,7 +1008,7 @@ var RoutedFsProvider = class extends FileSystem {
940
1008
  // it"). Every method here may run with the Proxy as `this`, so the state it touches cannot be `#private`.
941
1009
  rangeReader;
942
1010
  rangeChunkBytes;
943
- /** Reads the live sessions' working directories; see {@link RoutedFsProviderOptions.sessionWorkspaceCwds}. */
1011
+ /** Reads the live sessions' working directories (with ids when available); see {@link RoutedFsProviderOptions.sessionWorkspaceCwds}. */
944
1012
  sessionWorkspaceCwds;
945
1013
  /** Reads one session's fork lineage; see {@link RoutedFsProviderOptions.sessionAncestors}. */
946
1014
  sessionAncestors;
@@ -955,7 +1023,7 @@ var RoutedFsProvider = class extends FileSystem {
955
1023
  constructor(ctx, options = {}) {
956
1024
  super(ctx);
957
1025
  this.rangeReader = options.readBytesRange;
958
- this.sessionWorkspaceCwds = options.sessionWorkspaceCwds ?? (() => readSessionCwds(ctx));
1026
+ this.sessionWorkspaceCwds = options.sessionWorkspaceCwds ?? (() => readSessionWorkspaces(ctx));
959
1027
  this.sessionAncestors = options.sessionAncestors ?? ((sessionId) => readSessionAncestors(ctx, sessionId));
960
1028
  this.sessionSync = { lastSignature: "" };
961
1029
  startHostErrorDetection();
@@ -1482,7 +1550,8 @@ async function* pagedText(reader, chunkBytes, routed, firstWindow, signal) {
1482
1550
  }
1483
1551
  function sessionSyncSignature(cwds, registry) {
1484
1552
  const roots = registry.sessionWorkspaceRecords.map((record) => record.key).join("\0");
1485
- return `${cwds.join("\0")}${roots}`;
1553
+ const items = cwds.map((item) => typeof item === "string" ? item : `${item.sessionId}${item.cwd}`).join("\0");
1554
+ return `${items}${roots}`;
1486
1555
  }
1487
1556
  function assertText(displayPath, bytes) {
1488
1557
  if (bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) {