@tiphareth/dsh-hardssh 0.1.2-alpha
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 +50 -0
- package/cordis.patch.yml +26 -0
- package/lib/client.js +17794 -0
- package/lib/environment-BL1jddfB.js +449 -0
- package/lib/fs.js +478 -0
- package/lib/index.js +6758 -0
- package/lib/subprocess.js +600 -0
- package/lib/switch-fs-CAJpFY9C.js +193 -0
- package/lib/switch-fs-RrZtG2gv.js +210 -0
- package/lib/types/backend.d.ts +108 -0
- package/lib/types/base/capability.d.ts +107 -0
- package/lib/types/base/index.d.ts +18 -0
- package/lib/types/base/ledger-router.d.ts +48 -0
- package/lib/types/base/ledger.d.ts +82 -0
- package/lib/types/base/model.d.ts +108 -0
- package/lib/types/base/namespace.d.ts +57 -0
- package/lib/types/base/plugin.d.ts +102 -0
- package/lib/types/base/registry.d.ts +60 -0
- package/lib/types/base/router.d.ts +40 -0
- package/lib/types/client/api.d.ts +85 -0
- package/lib/types/client/directory-flow.d.ts +82 -0
- package/lib/types/client/icons.d.ts +19 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +104 -0
- package/lib/types/client/manager-button.d.ts +32 -0
- package/lib/types/client/migrate.d.ts +20 -0
- package/lib/types/client/ssh/api.d.ts +89 -0
- package/lib/types/client/ssh/apply.d.ts +23 -0
- package/lib/types/client/ssh/locales.d.ts +156 -0
- package/lib/types/client/ssh/mount.d.ts +13 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/HostFingerprintDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/HostFormDialog.d.ts +13 -0
- package/lib/types/client/ssh/panel/HostsTab.d.ts +10 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/SshPanel.d.ts +14 -0
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +12 -0
- package/lib/types/client/ssh/panel/TransferTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/controller.d.ts +23 -0
- package/lib/types/client/ssh/panel/helpers.d.ts +15 -0
- package/lib/types/client/ssh/panel/xterm.css.d.ts +3 -0
- package/lib/types/client/ssh/sidebar-entry.d.ts +25 -0
- package/lib/types/client/state.d.ts +32 -0
- package/lib/types/client/text.d.ts +11 -0
- package/lib/types/client/workspace-badges.d.ts +38 -0
- package/lib/types/client/workspace-gate.d.ts +15 -0
- package/lib/types/client-http.d.ts +15 -0
- package/lib/types/core.d.ts +43 -0
- package/lib/types/fs.d.ts +36 -0
- package/lib/types/host-http.d.ts +25 -0
- package/lib/types/index.d.ts +53 -0
- package/lib/types/ledger.d.ts +132 -0
- package/lib/types/protocol.d.ts +101 -0
- package/lib/types/providers/index.d.ts +19 -0
- package/lib/types/providers/local/provider.d.ts +58 -0
- package/lib/types/providers/ssh/provider.d.ts +112 -0
- package/lib/types/remote/environment.d.ts +29 -0
- package/lib/types/remote/output.d.ts +38 -0
- package/lib/types/remote/remote-fs.d.ts +69 -0
- package/lib/types/remote/remote-process.d.ts +41 -0
- package/lib/types/remote/remote-subprocess.d.ts +31 -0
- package/lib/types/remote/remote-terminal.d.ts +41 -0
- package/lib/types/remote-runner.d.ts +83 -0
- package/lib/types/remote-search.d.ts +50 -0
- package/lib/types/routes.d.ts +24 -0
- package/lib/types/runtime/workspace-core.d.ts +59 -0
- package/lib/types/seam-state.d.ts +69 -0
- package/lib/types/shell.d.ts +8 -0
- package/lib/types/ssh/connection/lease.d.ts +16 -0
- package/lib/types/ssh/connection/pool.d.ts +57 -0
- package/lib/types/ssh/engine.d.ts +434 -0
- package/lib/types/ssh/exec/output.d.ts +31 -0
- package/lib/types/ssh/known-hosts.d.ts +89 -0
- package/lib/types/ssh/plugin.d.ts +57 -0
- package/lib/types/ssh/protocol.d.ts +236 -0
- package/lib/types/ssh/routes.d.ts +44 -0
- package/lib/types/ssh/store.d.ts +107 -0
- package/lib/types/ssh/tools.d.ts +35 -0
- package/lib/types/ssh/transfer/progress.d.ts +14 -0
- package/lib/types/ssh/vault.d.ts +110 -0
- package/lib/types/subprocess.d.ts +32 -0
- package/lib/types/switch/switch-fs.d.ts +88 -0
- package/lib/types/switch/switch-subprocess.d.ts +34 -0
- package/lib/types/tools.d.ts +12 -0
- package/package.json +142 -0
- package/src/backend.ts +624 -0
- package/src/base/capability.ts +86 -0
- package/src/base/index.ts +18 -0
- package/src/base/ledger-router.ts +128 -0
- package/src/base/ledger.ts +299 -0
- package/src/base/model.ts +118 -0
- package/src/base/namespace.ts +102 -0
- package/src/base/plugin.ts +170 -0
- package/src/base/registry.ts +109 -0
- package/src/base/router.ts +43 -0
- package/src/client/api.ts +196 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/directory-flow.tsx +482 -0
- package/src/client/icons.tsx +50 -0
- package/src/client/index.ts +210 -0
- package/src/client/locales.ts +105 -0
- package/src/client/manager-button.tsx +269 -0
- package/src/client/migrate.ts +109 -0
- package/src/client/ssh/api.ts +411 -0
- package/src/client/ssh/apply.ts +50 -0
- package/src/client/ssh/locales.ts +322 -0
- package/src/client/ssh/mount.tsx +83 -0
- package/src/client/ssh/panel/ClusterTab.tsx +123 -0
- package/src/client/ssh/panel/HostFingerprintDialog.tsx +82 -0
- package/src/client/ssh/panel/HostFormDialog.tsx +228 -0
- package/src/client/ssh/panel/HostsTab.tsx +236 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +80 -0
- package/src/client/ssh/panel/SshPanel.tsx +77 -0
- package/src/client/ssh/panel/TerminalTab.tsx +176 -0
- package/src/client/ssh/panel/TransferTab.tsx +232 -0
- package/src/client/ssh/panel/TunnelsTab.tsx +177 -0
- package/src/client/ssh/panel/controller.ts +48 -0
- package/src/client/ssh/panel/helpers.ts +26 -0
- package/src/client/ssh/panel/panel.module.css +1006 -0
- package/src/client/ssh/panel/xterm.css.ts +2 -0
- package/src/client/ssh/sidebar-entry.ts +123 -0
- package/src/client/state.ts +99 -0
- package/src/client/text.ts +22 -0
- package/src/client/workspace-badges.ts +99 -0
- package/src/client/workspace-gate.ts +232 -0
- package/src/client/workspace.module.css +283 -0
- package/src/client-http.ts +45 -0
- package/src/core.ts +47 -0
- package/src/fs.ts +85 -0
- package/src/host-http.ts +74 -0
- package/src/index.ts +244 -0
- package/src/ledger.ts +416 -0
- package/src/protocol.ts +114 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/local/provider.ts +179 -0
- package/src/providers/ssh/provider.ts +274 -0
- package/src/remote/environment.ts +123 -0
- package/src/remote/output.ts +142 -0
- package/src/remote/remote-fs.ts +532 -0
- package/src/remote/remote-process.ts +203 -0
- package/src/remote/remote-subprocess.ts +159 -0
- package/src/remote/remote-terminal.ts +141 -0
- package/src/remote-runner.ts +201 -0
- package/src/remote-search.ts +163 -0
- package/src/routes.ts +395 -0
- package/src/runtime/workspace-core.ts +154 -0
- package/src/seam-state.ts +185 -0
- package/src/shell.ts +10 -0
- package/src/ssh/connection/lease.ts +17 -0
- package/src/ssh/connection/pool.ts +275 -0
- package/src/ssh/engine.ts +1761 -0
- package/src/ssh/exec/output.ts +70 -0
- package/src/ssh/known-hosts.ts +214 -0
- package/src/ssh/plugin.ts +184 -0
- package/src/ssh/protocol.ts +227 -0
- package/src/ssh/routes.ts +892 -0
- package/src/ssh/store.ts +544 -0
- package/src/ssh/tools.ts +402 -0
- package/src/ssh/transfer/progress.ts +75 -0
- package/src/ssh/vault.ts +477 -0
- package/src/subprocess.ts +71 -0
- package/src/switch/switch-fs.ts +253 -0
- package/src/switch/switch-subprocess.ts +59 -0
- package/src/tools.ts +221 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { FileSystem } from "@deepseek-ai/dsh-fs";
|
|
2
|
+
//#region src/switch/switch-fs.ts
|
|
3
|
+
/**
|
|
4
|
+
* The `ctx.fs` switching facade: routes every filesystem call to the backend
|
|
5
|
+
* of the SESSION's workspace — a local session operates on the local sandbox,
|
|
6
|
+
* an SSH-bound workspace on the remote host the workspace is bound to.
|
|
7
|
+
*
|
|
8
|
+
* Routing anchor: the cwd the tool call was made under (`resolve`'s
|
|
9
|
+
* `opts.cwd`, the write/edit `sandboxPolicy.workspaceRoot`). The resolver
|
|
10
|
+
* ("which SSH workspace owns this cwd?") lives in the deps; a hit routes to
|
|
11
|
+
* that workspace's remote backend, a miss to the local backend. Later
|
|
12
|
+
* operations (stat/read/write) receive only the resolved `FsTarget`, so the
|
|
13
|
+
* backend identity is encoded into the target key — `ssh:<recordId>:<remoteKey>`
|
|
14
|
+
* for remote targets, bare local keys for local ones — and decoded on every
|
|
15
|
+
* dispatch. This keeps two SSH workspaces on DIFFERENT hosts (or different
|
|
16
|
+
* directories on the SAME host) from colliding, and never misroutes a local
|
|
17
|
+
* target.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-hardssh/switch-fs
|
|
20
|
+
*/
|
|
21
|
+
/** Key namespace: 'ssh:<recordId>:' for remote workspaces. */
|
|
22
|
+
const REMOTE_PREFIX = "ssh:";
|
|
23
|
+
/** The routing filesystem facade. */
|
|
24
|
+
var SwitchFileSystem = class extends FileSystem {
|
|
25
|
+
deps;
|
|
26
|
+
local;
|
|
27
|
+
constructor(ctx, deps) {
|
|
28
|
+
super(ctx);
|
|
29
|
+
this.deps = deps;
|
|
30
|
+
this.local = deps.local;
|
|
31
|
+
}
|
|
32
|
+
/** Sandbox default from the local backend (remote worlds are unconfined —
|
|
33
|
+
* the docs/doc of the tool layer reads this once at mount). */
|
|
34
|
+
get sandboxMode() {
|
|
35
|
+
return this.local.sandboxMode;
|
|
36
|
+
}
|
|
37
|
+
/** Encode a raw key into the world's namespace. */
|
|
38
|
+
encode(rawKey, namespace) {
|
|
39
|
+
return namespace === "" ? rawKey : `${namespace}${rawKey}`;
|
|
40
|
+
}
|
|
41
|
+
/** Decode a namespaced key back to (rawKey, world). */
|
|
42
|
+
decode(key) {
|
|
43
|
+
if (key.startsWith("ssh:")) {
|
|
44
|
+
const end = key.indexOf(":", 4);
|
|
45
|
+
if (end > 0) {
|
|
46
|
+
const namespace = key.slice(0, end + 1);
|
|
47
|
+
const world = this.deps.worldForNamespace(namespace);
|
|
48
|
+
if (world !== void 0) return {
|
|
49
|
+
rawKey: key.slice(namespace.length),
|
|
50
|
+
world
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
rawKey: key,
|
|
57
|
+
world: {
|
|
58
|
+
backend: this.local,
|
|
59
|
+
namespace: ""
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async resolve(path, opts) {
|
|
64
|
+
const world = this.deps.worldFor(opts?.cwd);
|
|
65
|
+
const effectivePath = world.anchorPath !== void 0 && world.remoteRoot !== void 0 ? this.translateAnchorPath(world.anchorPath, world.remoteRoot, path) : path;
|
|
66
|
+
const raw = await world.backend.resolve(effectivePath, opts);
|
|
67
|
+
return {
|
|
68
|
+
targetKey: this.encode(String(raw.targetKey), world.namespace),
|
|
69
|
+
displayPath: raw.displayPath
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Rewrite `<anchor>/<rest>` to `<remoteRoot>/<rest>` when `path` is the
|
|
73
|
+
* anchor or under it; otherwise return `path` unchanged (remote absolute
|
|
74
|
+
* paths, relative paths, and foreign local paths all pass through). */
|
|
75
|
+
translateAnchorPath(anchor, remoteRoot, path) {
|
|
76
|
+
const norm = (value) => value.replace(/[\\/]+$/, "");
|
|
77
|
+
const a = norm(anchor);
|
|
78
|
+
const isWin = a.includes("\\");
|
|
79
|
+
const normPath = (value) => {
|
|
80
|
+
let out = norm(value);
|
|
81
|
+
if (isWin) out = out.replace(/\//g, "\\");
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
84
|
+
const p = normPath(path);
|
|
85
|
+
const aa = isWin ? a.toLowerCase() : a;
|
|
86
|
+
const pp = isWin ? p.toLowerCase() : p;
|
|
87
|
+
if (pp === aa) return remoteRoot;
|
|
88
|
+
if (pp.startsWith(`${aa}\\`) || pp.startsWith(`${aa}/`)) {
|
|
89
|
+
const tail = p.slice(a.length).replace(/^[\\/]+/, "").replace(/\\/g, "/");
|
|
90
|
+
return tail === "" ? remoteRoot : `${remoteRoot.replace(/\/+$/, "")}/${tail}`;
|
|
91
|
+
}
|
|
92
|
+
return path;
|
|
93
|
+
}
|
|
94
|
+
processPath(target) {
|
|
95
|
+
const key = String(target.targetKey);
|
|
96
|
+
const decoded = this.decode(key);
|
|
97
|
+
return decoded?.world.backend.processPath({
|
|
98
|
+
targetKey: decoded.rawKey,
|
|
99
|
+
displayPath: target.displayPath
|
|
100
|
+
}) ?? key;
|
|
101
|
+
}
|
|
102
|
+
fileUrl(target) {
|
|
103
|
+
const decoded = this.decode(String(target.targetKey));
|
|
104
|
+
if (decoded === void 0) return this.local.fileUrl(target);
|
|
105
|
+
return decoded.world.backend.fileUrl({
|
|
106
|
+
targetKey: decoded.rawKey,
|
|
107
|
+
displayPath: target.displayPath
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
contains(parent, child) {
|
|
111
|
+
const p = this.decode(String(parent.targetKey));
|
|
112
|
+
const c = this.decode(String(child.targetKey));
|
|
113
|
+
if (p === void 0 || c === void 0 || p.world.backend !== c.world.backend) return false;
|
|
114
|
+
return p.world.backend.contains({
|
|
115
|
+
targetKey: p.rawKey,
|
|
116
|
+
displayPath: parent.displayPath
|
|
117
|
+
}, {
|
|
118
|
+
targetKey: c.rawKey,
|
|
119
|
+
displayPath: child.displayPath
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async stat(target, signal) {
|
|
123
|
+
const decoded = this.decode(String(target.targetKey));
|
|
124
|
+
if (decoded === void 0) return void 0;
|
|
125
|
+
return decoded.world.backend.stat({
|
|
126
|
+
targetKey: decoded.rawKey,
|
|
127
|
+
displayPath: target.displayPath
|
|
128
|
+
}, signal);
|
|
129
|
+
}
|
|
130
|
+
async lstat(path, opts, signal) {
|
|
131
|
+
const world = this.deps.worldFor(opts?.cwd);
|
|
132
|
+
const effectivePath = world.anchorPath !== void 0 && world.remoteRoot !== void 0 ? this.translateAnchorPath(world.anchorPath, world.remoteRoot, path) : path;
|
|
133
|
+
return world.backend.lstat(effectivePath, opts, signal);
|
|
134
|
+
}
|
|
135
|
+
async readText(target, signal) {
|
|
136
|
+
const decoded = this.decode(String(target.targetKey));
|
|
137
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
138
|
+
return decoded.world.backend.readText({
|
|
139
|
+
targetKey: decoded.rawKey,
|
|
140
|
+
displayPath: target.displayPath
|
|
141
|
+
}, signal);
|
|
142
|
+
}
|
|
143
|
+
async streamText(target, signal) {
|
|
144
|
+
const decoded = this.decode(String(target.targetKey));
|
|
145
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
146
|
+
return decoded.world.backend.streamText({
|
|
147
|
+
targetKey: decoded.rawKey,
|
|
148
|
+
displayPath: target.displayPath
|
|
149
|
+
}, signal);
|
|
150
|
+
}
|
|
151
|
+
async readBytes(target, signal, maxBytes) {
|
|
152
|
+
const decoded = this.decode(String(target.targetKey));
|
|
153
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
154
|
+
return decoded.world.backend.readBytes({
|
|
155
|
+
targetKey: decoded.rawKey,
|
|
156
|
+
displayPath: target.displayPath
|
|
157
|
+
}, signal, maxBytes);
|
|
158
|
+
}
|
|
159
|
+
async listDir(target, signal) {
|
|
160
|
+
const decoded = this.decode(String(target.targetKey));
|
|
161
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
162
|
+
const entries = await decoded.world.backend.listDir({
|
|
163
|
+
targetKey: decoded.rawKey,
|
|
164
|
+
displayPath: target.displayPath
|
|
165
|
+
}, signal);
|
|
166
|
+
if (decoded.world.namespace === "") return entries;
|
|
167
|
+
return entries.map((entry) => ({
|
|
168
|
+
...entry,
|
|
169
|
+
target: {
|
|
170
|
+
targetKey: this.encode(String(entry.target.targetKey), decoded.world.namespace),
|
|
171
|
+
displayPath: entry.target.displayPath
|
|
172
|
+
}
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
async writeText(target, content, expected, signal, sandboxPolicy) {
|
|
176
|
+
const decoded = this.decode(String(target.targetKey));
|
|
177
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
178
|
+
return decoded.world.backend.writeText({
|
|
179
|
+
targetKey: decoded.rawKey,
|
|
180
|
+
displayPath: target.displayPath
|
|
181
|
+
}, content, expected, signal, sandboxPolicy);
|
|
182
|
+
}
|
|
183
|
+
async editText(target, edit, expected, signal, sandboxPolicy) {
|
|
184
|
+
const decoded = this.decode(String(target.targetKey));
|
|
185
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
186
|
+
return decoded.world.backend.editText({
|
|
187
|
+
targetKey: decoded.rawKey,
|
|
188
|
+
displayPath: target.displayPath
|
|
189
|
+
}, edit, expected, signal, sandboxPolicy);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
//#endregion
|
|
193
|
+
export { SwitchFileSystem as n, REMOTE_PREFIX as t };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { FileSystem } from "@deepseek-ai/dsh-fs";
|
|
2
|
+
//#region src/switch/switch-fs.ts
|
|
3
|
+
/**
|
|
4
|
+
* The `ctx.fs` switching facade: routes every filesystem call to the backend
|
|
5
|
+
* of the SESSION's workspace — a local session operates on the local sandbox,
|
|
6
|
+
* an SSH-bound workspace on the remote host the workspace is bound to.
|
|
7
|
+
*
|
|
8
|
+
* Routing anchor: the cwd the tool call was made under (`resolve`'s
|
|
9
|
+
* `opts.cwd`, the write/edit `sandboxPolicy.workspaceRoot`). The resolver
|
|
10
|
+
* ("which SSH workspace owns this cwd?") lives in the deps; a hit routes to
|
|
11
|
+
* that workspace's remote backend, a miss to the local backend. Later
|
|
12
|
+
* operations (stat/read/write) receive only the resolved `FsTarget`, so the
|
|
13
|
+
* backend identity is encoded into the target key — `ssh:<recordId>:<remoteKey>`
|
|
14
|
+
* for remote targets, bare local keys for local ones — and decoded on every
|
|
15
|
+
* dispatch. This keeps two SSH workspaces on DIFFERENT hosts (or different
|
|
16
|
+
* directories on the SAME host) from colliding, and never misroutes a local
|
|
17
|
+
* target.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-hardssh/switch-fs
|
|
20
|
+
*/
|
|
21
|
+
/** Key namespace: 'ssh:<recordId>:' (legacy) or 'wfs://<id>/' (current)
|
|
22
|
+
* for remote/workspace-bound targets. Both decode; new keys emit wfs://. */
|
|
23
|
+
const REMOTE_PREFIX = "ssh:";
|
|
24
|
+
/** Strict WFS key prefix (includes the double slash + separator). */
|
|
25
|
+
const WFS_NAMESPACE_MARKER = `wfs://`;
|
|
26
|
+
/** The routing filesystem facade. */
|
|
27
|
+
var SwitchFileSystem = class extends FileSystem {
|
|
28
|
+
deps;
|
|
29
|
+
local;
|
|
30
|
+
constructor(ctx, deps) {
|
|
31
|
+
super(ctx);
|
|
32
|
+
this.deps = deps;
|
|
33
|
+
this.local = deps.local;
|
|
34
|
+
}
|
|
35
|
+
/** Sandbox default from the local backend (remote worlds are unconfined —
|
|
36
|
+
* the docs/doc of the tool layer reads this once at mount). */
|
|
37
|
+
get sandboxMode() {
|
|
38
|
+
return this.local.sandboxMode;
|
|
39
|
+
}
|
|
40
|
+
/** Encode a raw key into the world's namespace. */
|
|
41
|
+
encode(rawKey, namespace) {
|
|
42
|
+
return namespace === "" ? rawKey : `${namespace}${rawKey}`;
|
|
43
|
+
}
|
|
44
|
+
/** Decode a namespaced key back to (rawKey, world). Supports both the
|
|
45
|
+
* current `wfs://<id>/…` form and the legacy `ssh:<recordId>:…` form. */
|
|
46
|
+
decode(key) {
|
|
47
|
+
if (key.startsWith(WFS_NAMESPACE_MARKER)) {
|
|
48
|
+
const rest = key.slice(WFS_NAMESPACE_MARKER.length);
|
|
49
|
+
const end = rest.indexOf("/");
|
|
50
|
+
if (end > 0) {
|
|
51
|
+
const namespace = `wfs://${rest.slice(0, end + 1)}`;
|
|
52
|
+
const world = this.deps.worldForNamespace(namespace);
|
|
53
|
+
if (world !== void 0) return {
|
|
54
|
+
rawKey: rest.slice(end + 1),
|
|
55
|
+
world
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.startsWith("ssh:")) {
|
|
61
|
+
const end = key.indexOf(":", 4);
|
|
62
|
+
if (end > 0) {
|
|
63
|
+
const namespace = key.slice(0, end + 1);
|
|
64
|
+
const world = this.deps.worldForNamespace(namespace);
|
|
65
|
+
if (world !== void 0) return {
|
|
66
|
+
rawKey: key.slice(namespace.length),
|
|
67
|
+
world
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
rawKey: key,
|
|
74
|
+
world: {
|
|
75
|
+
backend: this.local,
|
|
76
|
+
namespace: ""
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async resolve(path, opts) {
|
|
81
|
+
const world = this.deps.worldFor(opts?.cwd);
|
|
82
|
+
const effectivePath = world.anchorPath !== void 0 && world.remoteRoot !== void 0 ? this.translateAnchorPath(world.anchorPath, world.remoteRoot, path) : path;
|
|
83
|
+
const raw = await world.backend.resolve(effectivePath, opts);
|
|
84
|
+
return {
|
|
85
|
+
targetKey: this.encode(String(raw.targetKey), world.namespace),
|
|
86
|
+
displayPath: raw.displayPath
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Rewrite `<anchor>/<rest>` to `<remoteRoot>/<rest>` when `path` is the
|
|
90
|
+
* anchor or under it; otherwise return `path` unchanged (remote absolute
|
|
91
|
+
* paths, relative paths, and foreign local paths all pass through). */
|
|
92
|
+
translateAnchorPath(anchor, remoteRoot, path) {
|
|
93
|
+
const norm = (value) => value.replace(/[\\/]+$/, "");
|
|
94
|
+
const a = norm(anchor);
|
|
95
|
+
const isWin = a.includes("\\");
|
|
96
|
+
const normPath = (value) => {
|
|
97
|
+
let out = norm(value);
|
|
98
|
+
if (isWin) out = out.replace(/\//g, "\\");
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
const p = normPath(path);
|
|
102
|
+
const aa = isWin ? a.toLowerCase() : a;
|
|
103
|
+
const pp = isWin ? p.toLowerCase() : p;
|
|
104
|
+
if (pp === aa) return remoteRoot;
|
|
105
|
+
if (pp.startsWith(`${aa}\\`) || pp.startsWith(`${aa}/`)) {
|
|
106
|
+
const tail = p.slice(a.length).replace(/^[\\/]+/, "").replace(/\\/g, "/");
|
|
107
|
+
return tail === "" ? remoteRoot : `${remoteRoot.replace(/\/+$/, "")}/${tail}`;
|
|
108
|
+
}
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
processPath(target) {
|
|
112
|
+
const key = String(target.targetKey);
|
|
113
|
+
const decoded = this.decode(key);
|
|
114
|
+
return decoded?.world.backend.processPath({
|
|
115
|
+
targetKey: decoded.rawKey,
|
|
116
|
+
displayPath: target.displayPath
|
|
117
|
+
}) ?? key;
|
|
118
|
+
}
|
|
119
|
+
fileUrl(target) {
|
|
120
|
+
const decoded = this.decode(String(target.targetKey));
|
|
121
|
+
if (decoded === void 0) return this.local.fileUrl(target);
|
|
122
|
+
return decoded.world.backend.fileUrl({
|
|
123
|
+
targetKey: decoded.rawKey,
|
|
124
|
+
displayPath: target.displayPath
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
contains(parent, child) {
|
|
128
|
+
const p = this.decode(String(parent.targetKey));
|
|
129
|
+
const c = this.decode(String(child.targetKey));
|
|
130
|
+
if (p === void 0 || c === void 0 || p.world.backend !== c.world.backend) return false;
|
|
131
|
+
return p.world.backend.contains({
|
|
132
|
+
targetKey: p.rawKey,
|
|
133
|
+
displayPath: parent.displayPath
|
|
134
|
+
}, {
|
|
135
|
+
targetKey: c.rawKey,
|
|
136
|
+
displayPath: child.displayPath
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async stat(target, signal) {
|
|
140
|
+
const decoded = this.decode(String(target.targetKey));
|
|
141
|
+
if (decoded === void 0) return void 0;
|
|
142
|
+
return decoded.world.backend.stat({
|
|
143
|
+
targetKey: decoded.rawKey,
|
|
144
|
+
displayPath: target.displayPath
|
|
145
|
+
}, signal);
|
|
146
|
+
}
|
|
147
|
+
async lstat(path, opts, signal) {
|
|
148
|
+
const world = this.deps.worldFor(opts?.cwd);
|
|
149
|
+
const effectivePath = world.anchorPath !== void 0 && world.remoteRoot !== void 0 ? this.translateAnchorPath(world.anchorPath, world.remoteRoot, path) : path;
|
|
150
|
+
return world.backend.lstat(effectivePath, opts, signal);
|
|
151
|
+
}
|
|
152
|
+
async readText(target, signal) {
|
|
153
|
+
const decoded = this.decode(String(target.targetKey));
|
|
154
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
155
|
+
return decoded.world.backend.readText({
|
|
156
|
+
targetKey: decoded.rawKey,
|
|
157
|
+
displayPath: target.displayPath
|
|
158
|
+
}, signal);
|
|
159
|
+
}
|
|
160
|
+
async streamText(target, signal) {
|
|
161
|
+
const decoded = this.decode(String(target.targetKey));
|
|
162
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
163
|
+
return decoded.world.backend.streamText({
|
|
164
|
+
targetKey: decoded.rawKey,
|
|
165
|
+
displayPath: target.displayPath
|
|
166
|
+
}, signal);
|
|
167
|
+
}
|
|
168
|
+
async readBytes(target, signal, maxBytes) {
|
|
169
|
+
const decoded = this.decode(String(target.targetKey));
|
|
170
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
171
|
+
return decoded.world.backend.readBytes({
|
|
172
|
+
targetKey: decoded.rawKey,
|
|
173
|
+
displayPath: target.displayPath
|
|
174
|
+
}, signal, maxBytes);
|
|
175
|
+
}
|
|
176
|
+
async listDir(target, signal) {
|
|
177
|
+
const decoded = this.decode(String(target.targetKey));
|
|
178
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
179
|
+
const entries = await decoded.world.backend.listDir({
|
|
180
|
+
targetKey: decoded.rawKey,
|
|
181
|
+
displayPath: target.displayPath
|
|
182
|
+
}, signal);
|
|
183
|
+
if (decoded.world.namespace === "") return entries;
|
|
184
|
+
return entries.map((entry) => ({
|
|
185
|
+
...entry,
|
|
186
|
+
target: {
|
|
187
|
+
targetKey: this.encode(String(entry.target.targetKey), decoded.world.namespace),
|
|
188
|
+
displayPath: entry.target.displayPath
|
|
189
|
+
}
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
async writeText(target, content, expected, signal, sandboxPolicy) {
|
|
193
|
+
const decoded = this.decode(String(target.targetKey));
|
|
194
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
195
|
+
return decoded.world.backend.writeText({
|
|
196
|
+
targetKey: decoded.rawKey,
|
|
197
|
+
displayPath: target.displayPath
|
|
198
|
+
}, content, expected, signal, sandboxPolicy);
|
|
199
|
+
}
|
|
200
|
+
async editText(target, edit, expected, signal, sandboxPolicy) {
|
|
201
|
+
const decoded = this.decode(String(target.targetKey));
|
|
202
|
+
if (decoded === void 0) throw new Error("fs-ssh: cannot route target");
|
|
203
|
+
return decoded.world.backend.editText({
|
|
204
|
+
targetKey: decoded.rawKey,
|
|
205
|
+
displayPath: target.displayPath
|
|
206
|
+
}, edit, expected, signal, sandboxPolicy);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
210
|
+
export { SwitchFileSystem as n, WFS_NAMESPACE_MARKER as r, REMOTE_PREFIX as t };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gated filesystem backends and the production workspace file service.
|
|
3
|
+
*
|
|
4
|
+
* UI workspace routes resolve an exact ledger anchor and then create a
|
|
5
|
+
* request-scoped RemoteBackend. There is no implicit local fallback and no
|
|
6
|
+
* dependency on the legacy global SSH mode state.
|
|
7
|
+
*/
|
|
8
|
+
import type { SshEngine } from './ssh/engine.ts';
|
|
9
|
+
import { shellQuote } from './shell.ts';
|
|
10
|
+
import { RemoteSearchService } from './remote-search.ts';
|
|
11
|
+
import type { SshWorkspaceLedger } from './ledger.ts';
|
|
12
|
+
import type { DirListing, FileRead, FileWriteResult, SearchView, SshWorkspaceRecord, WorkspaceEntry } from './protocol.ts';
|
|
13
|
+
export declare const SEARCH_HIT_CAP = 200;
|
|
14
|
+
export declare const SEARCH_SCAN_CAP = 20000;
|
|
15
|
+
export declare const SEARCH_MAX_DEPTH = 4;
|
|
16
|
+
export declare const REMOTE_SEARCH_TIMEOUT_MS = 20000;
|
|
17
|
+
export type BackendErrorCode = 'binary' | 'too-large' | 'conflict' | 'outside-root' | 'not-remote' | 'root-mismatch' | 'not-found' | 'forbidden' | 'invalid' | 'io';
|
|
18
|
+
export declare class BackendError extends Error {
|
|
19
|
+
readonly code: BackendErrorCode;
|
|
20
|
+
constructor(code: BackendErrorCode, message: string, options?: {
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export interface WorkspaceFileContext {
|
|
25
|
+
workspaceId: string;
|
|
26
|
+
requestedRoot: string;
|
|
27
|
+
anchorPath: string;
|
|
28
|
+
alias: string;
|
|
29
|
+
remoteRoot: string;
|
|
30
|
+
}
|
|
31
|
+
export interface WorkspaceFileService {
|
|
32
|
+
resolveContext(root: string): Promise<WorkspaceFileContext>;
|
|
33
|
+
list(root: string, rel: string): Promise<DirListing>;
|
|
34
|
+
read(root: string, rel: string): Promise<FileRead>;
|
|
35
|
+
write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
|
|
36
|
+
search(root: string, query: string): Promise<SearchView>;
|
|
37
|
+
}
|
|
38
|
+
export interface WorkspaceBackend {
|
|
39
|
+
assertRoot(root: string): void;
|
|
40
|
+
list(root: string, rel: string): Promise<DirListing>;
|
|
41
|
+
read(root: string, rel: string): Promise<FileRead>;
|
|
42
|
+
write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
|
|
43
|
+
search(root: string, query: string): Promise<SearchView>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a workspace-relative path.
|
|
47
|
+
*
|
|
48
|
+
* Backslashes are rejected instead of treated as separators: the same input
|
|
49
|
+
* would mean a separator on Windows and a filename character on POSIX.
|
|
50
|
+
* Absolute paths, drive-qualified paths, NUL and ".." are rejected.
|
|
51
|
+
*/
|
|
52
|
+
export declare function normalizeRel(raw: string): string;
|
|
53
|
+
/** POSIX prefix gate. Handles "/" without producing an empty root. */
|
|
54
|
+
export declare function isInside(root: string, abs: string): boolean;
|
|
55
|
+
/** Resolve a normalized relative path below a POSIX root. */
|
|
56
|
+
export declare function relToAbs(root: string, rel: string): string;
|
|
57
|
+
export { shellQuote };
|
|
58
|
+
/** Stable dir-first, case-insensitive ordering shared by every backend. */
|
|
59
|
+
export declare function sortWorkspaceEntries(entries: WorkspaceEntry[]): WorkspaceEntry[];
|
|
60
|
+
export type WorkspacePathFlavor = 'local' | 'remote';
|
|
61
|
+
/** Return abs relative to root, rejecting prefix-collision/parent traversal. */
|
|
62
|
+
export declare function relativeWorkspacePath(root: string, abs: string, flavor: WorkspacePathFlavor): string;
|
|
63
|
+
/** Normalize filesystem/SFTP errors to stable route-visible codes. */
|
|
64
|
+
export declare function toBackendIoError(error: unknown, path: string): BackendError;
|
|
65
|
+
/** Map a BackendError to its stable HTTP status (routes use this). */
|
|
66
|
+
export declare function backendErrorStatus(error: BackendError, ioStatus: 500 | 502): number;
|
|
67
|
+
/**
|
|
68
|
+
* Local implementation retained for explicit local-only consumers. It is
|
|
69
|
+
* deliberately NOT selected by LedgerWorkspaceFileService.
|
|
70
|
+
*/
|
|
71
|
+
export declare class LocalBackend implements WorkspaceBackend {
|
|
72
|
+
assertRoot(root: string): void;
|
|
73
|
+
list(root: string, rel: string): Promise<DirListing>;
|
|
74
|
+
read(root: string, rel: string): Promise<FileRead>;
|
|
75
|
+
write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
|
|
76
|
+
search(root: string, query: string): Promise<SearchView>;
|
|
77
|
+
private resolvePath;
|
|
78
|
+
/** Resolve the nearest existing ancestor; root and every existing target
|
|
79
|
+
* ancestor must resolve below the real workspace root. */
|
|
80
|
+
private assertRealpathWalk;
|
|
81
|
+
}
|
|
82
|
+
/** A request-scoped remote backend bound to one immutable ledger record. */
|
|
83
|
+
export declare class RemoteBackend implements WorkspaceBackend {
|
|
84
|
+
private readonly engine;
|
|
85
|
+
private readonly record;
|
|
86
|
+
private readonly searchService;
|
|
87
|
+
private readonly remoteRoot;
|
|
88
|
+
constructor(engine: SshEngine, record: SshWorkspaceRecord, searchService: RemoteSearchService);
|
|
89
|
+
assertRoot(root: string): void;
|
|
90
|
+
list(root: string, rel: string): Promise<DirListing>;
|
|
91
|
+
read(root: string, rel: string): Promise<FileRead>;
|
|
92
|
+
write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
|
|
93
|
+
search(root: string, query: string): Promise<SearchView>;
|
|
94
|
+
}
|
|
95
|
+
/** Production UI service: exact ledger anchor -> request-scoped backend. */
|
|
96
|
+
export declare class LedgerWorkspaceFileService implements WorkspaceFileService {
|
|
97
|
+
private readonly ledger;
|
|
98
|
+
private readonly engine;
|
|
99
|
+
private readonly searchService;
|
|
100
|
+
constructor(ledger: SshWorkspaceLedger, engine: SshEngine, searchService: RemoteSearchService);
|
|
101
|
+
resolveContext(root: string): Promise<WorkspaceFileContext>;
|
|
102
|
+
list(root: string, rel: string): Promise<DirListing>;
|
|
103
|
+
read(root: string, rel: string): Promise<FileRead>;
|
|
104
|
+
write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
|
|
105
|
+
search(root: string, query: string): Promise<SearchView>;
|
|
106
|
+
private resolveBackend;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=backend.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider capability interfaces — the resource-access contracts a workspace
|
|
3
|
+
* provider may offer. Providers implement only what they support; consumers
|
|
4
|
+
* look up capabilities through `WorkspaceConnection.get(...)` and degrade
|
|
5
|
+
* gracefully when undefined.
|
|
6
|
+
*
|
|
7
|
+
* All paths in these interfaces are workspace-relative POSIX-like paths
|
|
8
|
+
* resolved against the record's `location.root` by the provider. The provider
|
|
9
|
+
* is responsible for root confinement (and any extra semantics, e.g. SFTP
|
|
10
|
+
* mode bits), so a consumer can treat every workspace uniformly.
|
|
11
|
+
*
|
|
12
|
+
* @module @tiphareth/dsh-hardssh/base/capability
|
|
13
|
+
*/
|
|
14
|
+
/** Uniform stat shape (both backends normalize to this). */
|
|
15
|
+
export interface WorkspaceStat {
|
|
16
|
+
type: 'dir' | 'file' | 'other';
|
|
17
|
+
size: number;
|
|
18
|
+
mtimeMs: number;
|
|
19
|
+
mode?: number;
|
|
20
|
+
}
|
|
21
|
+
/** One directory entry. */
|
|
22
|
+
export interface WorkspaceDirEntry {
|
|
23
|
+
name: string;
|
|
24
|
+
type: 'dir' | 'file' | 'other';
|
|
25
|
+
size: number;
|
|
26
|
+
mtimeMs: number;
|
|
27
|
+
}
|
|
28
|
+
/** File-system capability. */
|
|
29
|
+
export interface WorkspaceFileSystem {
|
|
30
|
+
stat(path: string, signal?: AbortSignal): Promise<WorkspaceStat | undefined>;
|
|
31
|
+
list(path: string, signal?: AbortSignal): Promise<WorkspaceDirEntry[]>;
|
|
32
|
+
readFile(path: string, signal?: AbortSignal): Promise<Uint8Array>;
|
|
33
|
+
writeFile(path: string, data: Uint8Array, signal?: AbortSignal): Promise<void>;
|
|
34
|
+
mkdir(path: string, options?: {
|
|
35
|
+
recursive?: boolean;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
rm(path: string, options?: {
|
|
39
|
+
recursive?: boolean;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
rename(from: string, to: string, signal?: AbortSignal): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/** One process result. */
|
|
45
|
+
export interface WorkspaceProcessResult {
|
|
46
|
+
stdout: string;
|
|
47
|
+
stderr: string;
|
|
48
|
+
exitCode: number;
|
|
49
|
+
timedOut?: boolean;
|
|
50
|
+
durationMs?: number;
|
|
51
|
+
}
|
|
52
|
+
/** Subprocess capability. */
|
|
53
|
+
export interface WorkspaceProcessRuntime {
|
|
54
|
+
exec(command: string, options?: {
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
cwd?: string;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}): Promise<WorkspaceProcessResult>;
|
|
59
|
+
}
|
|
60
|
+
/** Interactive terminal capability. */
|
|
61
|
+
export interface WorkspaceTerminalService {
|
|
62
|
+
openTerminal(cols: number, rows: number): Promise<WorkspaceTerminalHandle>;
|
|
63
|
+
}
|
|
64
|
+
/** Minimal PTY handle (mirrors the ssh provider's duck-typed handle). */
|
|
65
|
+
export interface WorkspaceTerminalHandle {
|
|
66
|
+
shell: string;
|
|
67
|
+
write(data: string): void;
|
|
68
|
+
resize(cols: number, rows: number): void;
|
|
69
|
+
kill(): void;
|
|
70
|
+
onData(handler: (data: string) => void): {
|
|
71
|
+
dispose(): void;
|
|
72
|
+
};
|
|
73
|
+
onExit(handler: (event: {
|
|
74
|
+
exitCode: number;
|
|
75
|
+
}) => void): {
|
|
76
|
+
dispose(): void;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** One filename-search hit. */
|
|
80
|
+
export interface WorkspaceSearchHit {
|
|
81
|
+
/** Absolute path on the provider. */
|
|
82
|
+
path: string;
|
|
83
|
+
/** Path relative to the search root. */
|
|
84
|
+
rel: string;
|
|
85
|
+
isDir: boolean;
|
|
86
|
+
}
|
|
87
|
+
/** Filename / content search capability. */
|
|
88
|
+
export interface WorkspaceSearchService {
|
|
89
|
+
/** glob-style filename search. */
|
|
90
|
+
glob(pattern: string, options?: {
|
|
91
|
+
root?: string;
|
|
92
|
+
maxDepth?: number;
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
hits: WorkspaceSearchHit[];
|
|
96
|
+
truncated: boolean;
|
|
97
|
+
}>;
|
|
98
|
+
/** fixed-string content search (skips .git / node_modules). */
|
|
99
|
+
grep(fixedPhrase: string, options?: {
|
|
100
|
+
root?: string;
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
}): Promise<{
|
|
103
|
+
hits: WorkspaceSearchHit[];
|
|
104
|
+
truncated: boolean;
|
|
105
|
+
}>;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tiphareth/dsh-hardssh/base — the generic workspace base.
|
|
3
|
+
*
|
|
4
|
+
* Provider-agnostic core: workspace model, provider/connection/capability
|
|
5
|
+
* contracts, registry, ledger, namespace codec, router, and the plugin
|
|
6
|
+
* registration API. No SSH, no Cordis, no React.
|
|
7
|
+
*
|
|
8
|
+
* @module @tiphareth/dsh-hardssh/base
|
|
9
|
+
*/
|
|
10
|
+
export * from './model.ts';
|
|
11
|
+
export * from './capability.ts';
|
|
12
|
+
export * from './registry.ts';
|
|
13
|
+
export * from './ledger.ts';
|
|
14
|
+
export * from './namespace.ts';
|
|
15
|
+
export * from './router.ts';
|
|
16
|
+
export * from './ledger-router.ts';
|
|
17
|
+
export * from './plugin.ts';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger-backed workspace router — the concrete `WorkspaceRouter` that ties
|
|
3
|
+
* the ledger's anchor index, the namespace codec, and the provider registry
|
|
4
|
+
* together. Any provider can be routed through this router with zero SSH
|
|
5
|
+
* knowledge: a session cwd inside a workspace's anchor resolves to that
|
|
6
|
+
* workspace's provider connection; an explicit `wfs://…` (or legacy `ssh:…`)
|
|
7
|
+
* target key resolves to the owning connection.
|
|
8
|
+
*
|
|
9
|
+
* The router opens connections lazily and caches them per workspace id;
|
|
10
|
+
* removed workspaces drop their connections so stale keys fail closed.
|
|
11
|
+
*
|
|
12
|
+
* @module @tiphareth/dsh-hardssh/base/ledger-router
|
|
13
|
+
*/
|
|
14
|
+
import type { WorkspaceConnection, WorkspaceRecord } from './model.ts';
|
|
15
|
+
import { type WorkspaceNamespaceCodec, type WorkspaceRoute } from './namespace.ts';
|
|
16
|
+
import type { WorkspaceRegistry } from './registry.ts';
|
|
17
|
+
import { type WorkspaceLedger } from './ledger.ts';
|
|
18
|
+
import type { WorkspaceResolution, WorkspaceRouter } from './router.ts';
|
|
19
|
+
/** Open-or-reuse strategy for workspace connections. */
|
|
20
|
+
export interface LedgerRouterOptions {
|
|
21
|
+
codec?: WorkspaceNamespaceCodec;
|
|
22
|
+
/** Called with the record; resolves to a connection (undefined = cannot open). */
|
|
23
|
+
open(record: WorkspaceRecord, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
|
|
24
|
+
/** Called when a workspace is removed (drop cache / pool leases). */
|
|
25
|
+
onRemoved?(workspaceId: string): void;
|
|
26
|
+
}
|
|
27
|
+
/** Ledger + registry based router. */
|
|
28
|
+
export declare class LedgerWorkspaceRouter implements WorkspaceRouter {
|
|
29
|
+
private readonly ledger;
|
|
30
|
+
private readonly registry;
|
|
31
|
+
private readonly options;
|
|
32
|
+
readonly codec: WorkspaceNamespaceCodec;
|
|
33
|
+
private readonly connections;
|
|
34
|
+
private readonly anchors;
|
|
35
|
+
constructor(ledger: WorkspaceLedger, registry: WorkspaceRegistry, options: LedgerRouterOptions);
|
|
36
|
+
private reindex;
|
|
37
|
+
private openFor;
|
|
38
|
+
fromNamespace(key: string): WorkspaceResolution | undefined;
|
|
39
|
+
fromAnchor(cwd: string | undefined): WorkspaceConnection | undefined;
|
|
40
|
+
resolveRoute(route: WorkspaceRoute): WorkspaceResolution | undefined;
|
|
41
|
+
/** Async open used by the adapter at mount time (pre-warm active records). */
|
|
42
|
+
ensureOpen(record: WorkspaceRecord, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
|
|
43
|
+
/** All currently cached connections (for lifecycle/teardown). */
|
|
44
|
+
connectionsSnapshot(): WorkspaceConnection[];
|
|
45
|
+
/** Close every cached connection (plugin teardown). */
|
|
46
|
+
closeAll(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ledger-router.d.ts.map
|