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.
- package/README.md +13 -4
- package/agents/agent-manifest.json +6 -6
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +613 -257
- package/lib/file-references.js +17 -0
- package/lib/fs.js +90 -20
- package/lib/index.js +1385 -463
- 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/domain/session/session-admin.d.ts +5 -0
- 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 +11 -2
- package/package.json +1 -1
package/lib/file-references.js
CHANGED
|
@@ -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
|
|
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"
|
|
671
|
-
res.setHeader("allow", "
|
|
672
|
-
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" });
|
|
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 =
|
|
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
|
|
711
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
939
|
+
if (typeof id !== "string" || id === "" || typeof cwd !== "string" || cwd === "") continue;
|
|
940
|
+
pairs.push({ sessionId: id, cwd });
|
|
872
941
|
}
|
|
873
|
-
return
|
|
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 ?? (() =>
|
|
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
|
-
|
|
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)) {
|