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/README.md +12 -3
- package/agents/agent-manifest.json +4 -4
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +111 -40
- package/lib/file-references.js +17 -0
- package/lib/fs.js +87 -18
- package/lib/index.js +550 -145
- package/lib/terminal-wrapper.js +4 -3
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +11 -2
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +8 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +9 -3
- package/lib/types/src/host/integration/session/session-header.d.ts +200 -0
- package/lib/types/src/wiring/helpers.d.ts +1 -1
- package/package.json +1 -1
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"
|
|
672
|
-
res.setHeader("allow", "
|
|
673
|
-
send(req, res, 405, { ok: false, error:
|
|
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 =
|
|
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
|
|
712
|
-
|
|
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
|
|
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
|
|
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
|
|
939
|
+
if (typeof id !== "string" || id === "" || typeof cwd !== "string" || cwd === "") continue;
|
|
940
|
+
pairs.push({ sessionId: id, cwd });
|
|
873
941
|
}
|
|
874
|
-
return
|
|
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 ?? (() =>
|
|
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
|
-
|
|
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)) {
|