dsh-over-ssh 0.2.0 → 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.
@@ -154,11 +154,28 @@ function normalizeLocalPath2(path, options = {}) {
154
154
  return normalizeLocalPath(path, { caseInsensitive: options.caseInsensitive ?? process.platform === "win32" });
155
155
  }
156
156
 
157
+ // src/host/integration/session-fork-pending.ts
158
+ var pending = /* @__PURE__ */ new Map();
159
+ function pendingForkSources(sessionId) {
160
+ if (sessionId === void 0 || sessionId === "") return [];
161
+ const source = pending.get(sessionId);
162
+ return source === void 0 ? [] : [source];
163
+ }
164
+
157
165
  // src/host/integration/session-scope.ts
158
166
  var MAX_FORK_DEPTH = 32;
159
167
  var ancestorCache = /* @__PURE__ */ new Map();
160
168
  var ANCESTOR_CACHE_MAX = 256;
161
169
  function readSessionAncestors(ctx, sessionId) {
170
+ const pending2 = pendingForkSources(sessionId);
171
+ if (pending2.length > 0) {
172
+ const inherited = /* @__PURE__ */ new Set();
173
+ for (const source of pending2) {
174
+ inherited.add(source);
175
+ for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
176
+ }
177
+ return [...inherited];
178
+ }
162
179
  const cached = ancestorCache.get(sessionId);
163
180
  if (cached !== void 0) {
164
181
  ancestorCache.delete(sessionId);
package/lib/fs.js CHANGED
@@ -559,6 +559,7 @@ var USER_ROOTS_FILE_NAME = "user-roots.json";
559
559
  var USER_ROOTS_LOCK_FILE_NAME = `${USER_ROOTS_FILE_NAME}.lock`;
560
560
 
561
561
  // src/shared/routes.ts
562
+ var GIT_STATUS_API_PATH = "/api/over-ssh/git-status";
562
563
  function asRouteHost(value) {
563
564
  if (typeof value !== "object" || value === null) return void 0;
564
565
  const register = value.register;
@@ -629,6 +630,20 @@ async function readBody(req, maxBytes) {
629
630
  }
630
631
  return Buffer.concat(chunks).toString("utf8");
631
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
+ }
632
647
  function toBytes(chunk) {
633
648
  if (typeof chunk === "string") return Buffer.from(chunk, "utf8");
634
649
  if (chunk instanceof Uint8Array) return chunk;
@@ -649,9 +664,41 @@ function asRecord(value) {
649
664
  function messageOf(error) {
650
665
  return error instanceof Error ? error.message : String(error);
651
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
+ }
652
699
 
653
700
  // src/host/infra/transport/git-status-route.ts
654
- var GIT_STATUS_API_PATH = "/api/over-ssh/git-status";
701
+ var GIT_STATUS_API_PATH2 = GIT_STATUS_API_PATH;
655
702
  function toPublicAnswer(snapshot) {
656
703
  return {
657
704
  ok: true,
@@ -667,9 +714,9 @@ function toPublicAnswer(snapshot) {
667
714
  function createGitStatusApi(deps) {
668
715
  const handle = async (req, res) => {
669
716
  const method = (req.method ?? "GET").toUpperCase();
670
- if (method !== "POST" && method !== "GET" && method !== "HEAD") {
671
- res.setHeader("allow", "GET, HEAD, POST");
672
- 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" });
673
720
  return;
674
721
  }
675
722
  const verdict = trustVerdict(req);
@@ -677,13 +724,8 @@ function createGitStatusApi(deps) {
677
724
  send(req, res, 403, { ok: false, error: trustMessage(verdict) });
678
725
  return;
679
726
  }
680
- if (method !== "POST") {
681
- res.setHeader("allow", "POST");
682
- send(req, res, 405, { ok: false, error: "git-status \u53EA\u63A5\u53D7 POST" });
683
- return;
684
- }
685
727
  try {
686
- const body = asRecord(JSON.parse(await readBody(req, ROOTS_API_MAX_BODY_BYTES)));
728
+ const body = await readJsonObject(req, ROOTS_API_MAX_BODY_BYTES);
687
729
  const connectionId = body?.["connectionId"];
688
730
  const path = body?.["path"];
689
731
  if (typeof connectionId !== "string" || connectionId === "") {
@@ -707,8 +749,14 @@ function createGitStatusApi(deps) {
707
749
  }
708
750
  send(req, res, 200, toPublicAnswer(snapshot));
709
751
  } catch (error) {
710
- const invalidBody = error instanceof SyntaxError;
711
- 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
+ });
712
760
  }
713
761
  };
714
762
  return { handle };
@@ -720,7 +768,7 @@ function registerGitStatusRoute(provider, deps) {
720
768
  const host = asRouteHost(scoped.get("webServer"));
721
769
  if (host === void 0) return void 0;
722
770
  try {
723
- const dispose = host.register({ kind: "exact", path: GIT_STATUS_API_PATH, handler: api.handle });
771
+ const dispose = host.register({ kind: "exact", path: GIT_STATUS_API_PATH2, handler: api.handle });
724
772
  return () => dispose();
725
773
  } catch {
726
774
  return void 0;
@@ -779,6 +827,16 @@ function replaceLiteral(source, needle, replacement, all) {
779
827
 
780
828
  // src/host/integration/session-scope.ts
781
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
782
840
  var STORE_KEY = /* @__PURE__ */ Symbol.for("dsh-over-ssh/session-scope");
783
841
  function store() {
784
842
  const holder = globalThis;
@@ -795,6 +853,15 @@ var MAX_FORK_DEPTH = 32;
795
853
  var ancestorCache = /* @__PURE__ */ new Map();
796
854
  var ANCESTOR_CACHE_MAX = 256;
797
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
+ }
798
865
  const cached = ancestorCache.get(sessionId);
799
866
  if (cached !== void 0) {
800
867
  ancestorCache.delete(sessionId);
@@ -856,7 +923,7 @@ function readSessionCwd(ctx, sessionId) {
856
923
  }
857
924
  return found.cwd;
858
925
  }
859
- function readSessionCwds(ctx) {
926
+ function readSessionWorkspaces(ctx) {
860
927
  let list;
861
928
  try {
862
929
  const sessions = ctx.get("sessions");
@@ -865,12 +932,14 @@ function readSessionCwds(ctx) {
865
932
  return [];
866
933
  }
867
934
  if (!Array.isArray(list)) return [];
868
- const cwds = [];
935
+ const pairs = [];
869
936
  for (const session of list) {
937
+ const id = session?.header?.id;
870
938
  const cwd = session?.header?.cwd;
871
- 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 });
872
941
  }
873
- return cwds;
942
+ return pairs;
874
943
  }
875
944
  var sessionCwdCache = /* @__PURE__ */ new Map();
876
945
  var SESSION_CWD_CACHE_MAX = 256;
@@ -939,7 +1008,7 @@ var RoutedFsProvider = class extends FileSystem {
939
1008
  // it"). Every method here may run with the Proxy as `this`, so the state it touches cannot be `#private`.
940
1009
  rangeReader;
941
1010
  rangeChunkBytes;
942
- /** 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}. */
943
1012
  sessionWorkspaceCwds;
944
1013
  /** Reads one session's fork lineage; see {@link RoutedFsProviderOptions.sessionAncestors}. */
945
1014
  sessionAncestors;
@@ -954,7 +1023,7 @@ var RoutedFsProvider = class extends FileSystem {
954
1023
  constructor(ctx, options = {}) {
955
1024
  super(ctx);
956
1025
  this.rangeReader = options.readBytesRange;
957
- this.sessionWorkspaceCwds = options.sessionWorkspaceCwds ?? (() => readSessionCwds(ctx));
1026
+ this.sessionWorkspaceCwds = options.sessionWorkspaceCwds ?? (() => readSessionWorkspaces(ctx));
958
1027
  this.sessionAncestors = options.sessionAncestors ?? ((sessionId) => readSessionAncestors(ctx, sessionId));
959
1028
  this.sessionSync = { lastSignature: "" };
960
1029
  startHostErrorDetection();
@@ -1481,7 +1550,8 @@ async function* pagedText(reader, chunkBytes, routed, firstWindow, signal) {
1481
1550
  }
1482
1551
  function sessionSyncSignature(cwds, registry) {
1483
1552
  const roots = registry.sessionWorkspaceRecords.map((record) => record.key).join("\0");
1484
- 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}`;
1485
1555
  }
1486
1556
  function assertText(displayPath, bytes) {
1487
1557
  if (bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) {