@langify-org/dsh-code-server 0.2.0 → 0.2.1
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.ja.md +3 -2
- package/README.md +3 -2
- package/README.zh.md +3 -2
- package/dist/client.cjs +0 -6
- package/dist/index.mjs +23 -11
- package/extension/dsh-code-bridge.vsix +0 -0
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -44,7 +44,7 @@ DSH の Web 版や Desktop 版では **Plugins** ページから追加できま
|
|
|
44
44
|
- ブラウザーでは、DSH が自分のオリジンの `/code-server` でエディターを配信します。すべてのリクエストと WebSocket は、先に DSH の認証とオリジンの検査を通ります。そのためエディターには、DSH に届く場所(Tailscale 経由など)からしか届きません。
|
|
45
45
|
- DSH Desktop は DSH を `dsh-app://app` で表示します。code-server はここでは動きません。サーバーの宛先を自分のページのアドレスから作り、このスキームでは webview に必要な Service Worker も使えないためです。また Desktop は、自分以外のオリジンから Host への WebSocket を遮断します。そこで Desktop では、プラグイン自身が `127.0.0.1:<desktopPort>` でエディターを配信します。DSH とは別の、ふつうの http のオリジンです。待ち受けは、Desktop が初めて Code ページを開いたときに始めます。エディターのアドレスのパスには、プラグインの起動ごとに作るランダムなトークンが入ります。code-server はすべてをページからの相対パスで指すので、どのリクエストにもトークンが付きます。トークンを受け取れるのは、DSH が認証したページだけです。さらに、別のホスト名を名乗るリクエストは拒否し、DNS rebinding を防ぎます。
|
|
46
46
|
- 起動のたびに、同梱の VS Code 拡張 **DSH Code Bridge**(`langify-org.dsh-code-bridge`)を code-server に入れ、古いものを置き換えます。
|
|
47
|
-
- Code ページごとに専用の接続(binding)を作ります。ページは binding をエディターの URL の断片(サーバーには送られない部分)に入れ、そのエディター画面の拡張がそれを読み取って、別の非公開 socket
|
|
47
|
+
- Code ページごとに専用の接続(binding)を作ります。ページは binding をエディターの URL の断片(サーバーには送られない部分)に入れ、そのエディター画面の拡張がそれを読み取って、別の非公開 socket でプラグインに接続します。同じフォルダーを複数のページで開いていても、ファイルはその画面にだけ開きます。エディターの画面が再読み込みしたとき(フォルダーを信頼したときなど)は、その画面の拡張が同じ binding につなぎ直します。ページはそれを最大1分待ちます。
|
|
48
48
|
|
|
49
49
|
## セキュリティ
|
|
50
50
|
|
|
@@ -59,7 +59,8 @@ code-server とそのターミナル・拡張は、あなたの権限で、DSH
|
|
|
59
59
|
3. サインインしていないと `/code-server/` と `POST /api/langify-code-server/create` が 401 を返し、別オリジンからの WebSocket の upgrade は 403 を返す。
|
|
60
60
|
4. セッションの作業フォルダーが変わると、エディターが開き直すかを尋ねる。未保存のファイルがあれば先に保存を求め、そのあとページが新しいフォルダーでエディターを開き直す。
|
|
61
61
|
5. DSH Desktop では、エディターが `http://127.0.0.1:41820/…` から読み込まれ、テーマとファイルアイコンが出て、Markdown のプレビューが開く。トークンが無いとポートは 404 を返す。
|
|
62
|
-
6.
|
|
62
|
+
6. エディターで **Developer: Reload Window** を実行したあとも、DSH で選んだファイルが**再接続**なしでその画面に開く。
|
|
63
|
+
7. Code タブを閉じると binding が閉じる。DSH を止めると code-server も止まり、socket が消える。
|
|
63
64
|
|
|
64
65
|
## 既知の制約
|
|
65
66
|
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ With the defaults, the editor shares settings and extensions with your own code-
|
|
|
44
44
|
- In a browser, DSH serves the editor under `/code-server` on its own origin. Every request and WebSocket passes DSH's authentication and origin check first, so the editor is reachable wherever DSH is, for example over Tailscale, and nowhere else.
|
|
45
45
|
- DSH Desktop shows DSH at `dsh-app://app`, where code-server cannot run: it builds its server's addresses from its own page's, and that scheme allows no service workers, which webviews need. Desktop also refuses WebSockets to its Host from any other origin. So in Desktop the plugin serves the editor itself, on `127.0.0.1:<desktopPort>`, a plain http origin apart from DSH's. It starts listening when Desktop first opens the Code page. The editor's address carries in its path a random token, made anew whenever the plugin starts; code-server addresses everything relative to its page, so every request carries it. Only pages DSH has authenticated receive the token. The port also refuses requests that name another host, which stops DNS rebinding.
|
|
46
46
|
- Before each start, the plugin installs its bundled VS Code extension, **DSH Code Bridge** (`langify-org.dsh-code-bridge`), into code-server, replacing any older copy.
|
|
47
|
-
- Each Code page gets its own connection ("binding"). The page puts the binding in the editor's URL fragment, which never reaches a server; the extension in that editor window reads it back and connects to the plugin over another private socket. Files open in that window only, even when several pages show the same folder.
|
|
47
|
+
- Each Code page gets its own connection ("binding"). The page puts the binding in the editor's URL fragment, which never reaches a server; the extension in that editor window reads it back and connects to the plugin over another private socket. Files open in that window only, even when several pages show the same folder. When the window reloads, for example after you trust its folder, its extension attaches to the binding again; the page waits up to a minute for that.
|
|
48
48
|
|
|
49
49
|
## Security
|
|
50
50
|
|
|
@@ -59,7 +59,8 @@ These need a browser and a real code-server, so they are not part of `pnpm check
|
|
|
59
59
|
3. Without signing in, `/code-server/` and `POST /api/langify-code-server/create` answer 401, and a WebSocket upgrade from another origin answers 403.
|
|
60
60
|
4. After the session's working folder changes, the editor offers to reopen; with an unsaved file it asks to save first, then the page reloads the editor on the new folder.
|
|
61
61
|
5. In DSH Desktop, the editor loads from `http://127.0.0.1:41820/…` with your theme and file icons, and a Markdown preview opens. The port answers 404 without the token.
|
|
62
|
-
6.
|
|
62
|
+
6. After **Developer: Reload Window** in the editor, a file chosen in DSH still opens there, without **Reconnect**.
|
|
63
|
+
7. Closing the Code tab closes its binding; stopping DSH stops code-server and removes its sockets.
|
|
63
64
|
|
|
64
65
|
## Known limitations
|
|
65
66
|
|
package/README.zh.md
CHANGED
|
@@ -44,7 +44,7 @@ dsh plugin --profile <profile> add @langify-org/dsh-code-server
|
|
|
44
44
|
- 在浏览器中,DSH 在自己的源下通过 `/code-server` 提供编辑器。每个请求和 WebSocket 都会先经过 DSH 的认证和来源检查,因此只有能访问 DSH 的地方(例如通过 Tailscale)才能访问编辑器。
|
|
45
45
|
- DSH Desktop 在 `dsh-app://app` 显示 DSH,code-server 无法在那里运行:它根据自身页面的地址构造服务器地址,而且该协议不支持 webview 所需的 Service Worker。Desktop 还会拒绝来自其他源、发往其 Host 的 WebSocket。因此在 Desktop 中,插件自己在 `127.0.0.1:<desktopPort>` 提供编辑器,这是一个独立于 DSH 的普通 http 源。Desktop 第一次打开 Code 页面时才开始监听。编辑器地址的路径中带有一个随机令牌,插件每次启动时重新生成;code-server 的所有地址都相对于其页面,因此每个请求都会带上它。只有经过 DSH 认证的页面才能拿到该令牌。该端口还会拒绝声明为其他主机名的请求,以防 DNS rebinding。
|
|
46
46
|
- 每次启动前,插件都会把内置的 VS Code 扩展 **DSH Code Bridge**(`langify-org.dsh-code-bridge`)安装到 code-server 中,替换旧版本。
|
|
47
|
-
- 每个 Code 页面都有自己的连接(binding)。页面把 binding 放在编辑器 URL
|
|
47
|
+
- 每个 Code 页面都有自己的连接(binding)。页面把 binding 放在编辑器 URL 的片段中(这一部分不会发送到服务器),该编辑器窗口中的扩展读取它,并通过另一个私有套接字连接到插件。即使多个页面打开了同一个文件夹,文件也只会在对应的窗口中打开。编辑器窗口重新加载时(例如在你信任其文件夹之后),该窗口中的扩展会重新连接到同一个 binding;页面最多等待一分钟。
|
|
48
48
|
|
|
49
49
|
## 安全
|
|
50
50
|
|
|
@@ -59,7 +59,8 @@ code-server 及其终端和扩展以你的身份运行,不受 DSH 沙箱和审
|
|
|
59
59
|
3. 未登录时,`/code-server/` 和 `POST /api/langify-code-server/create` 返回 401,来自其他源的 WebSocket upgrade 返回 403。
|
|
60
60
|
4. 会话的工作文件夹变化后,编辑器会询问是否重新打开;如有未保存的文件,会先要求保存,然后页面在新文件夹中重新打开编辑器。
|
|
61
61
|
5. 在 DSH Desktop 中,编辑器从 `http://127.0.0.1:41820/…` 加载,显示你的主题和文件图标,Markdown 预览可以打开;没有令牌时该端口返回 404。
|
|
62
|
-
6.
|
|
62
|
+
6. 在编辑器中执行 **Developer: Reload Window** 之后,在 DSH 中选择的文件仍会在该窗口打开,无需**重新连接**。
|
|
63
|
+
7. 关闭 Code 标签页会关闭其 binding;停止 DSH 会停止 code-server 并删除其套接字。
|
|
63
64
|
|
|
64
65
|
## 已知限制
|
|
65
66
|
|
package/dist/client.cjs
CHANGED
|
@@ -38,7 +38,6 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
"error.desktop_port_in_use": "The editor's port for DSH Desktop is taken: {message}. Choose another one with the plugin's desktopPort setting.",
|
|
39
39
|
"error.unknown_binding": "This editor connection is gone. Reconnect.",
|
|
40
40
|
"error.not_ready": "The editor is still starting.",
|
|
41
|
-
"error.binding_disconnected": "The editor window disconnected. Reconnect.",
|
|
42
41
|
"error.binding_expired": "The editor connection expired. Reconnect.",
|
|
43
42
|
"error.binding_closed": "The editor connection was closed. Reconnect.",
|
|
44
43
|
"error.internal_error": "The plugin failed: {message}",
|
|
@@ -83,7 +82,6 @@ window.__ModuleLoader__.load({
|
|
|
83
82
|
"error.desktop_port_in_use": "供 DSH Desktop 使用的编辑器端口已被占用:{message}。请在插件设置 desktopPort 中改用其他端口。",
|
|
84
83
|
"error.unknown_binding": "与此编辑器的连接已不存在。请重新连接。",
|
|
85
84
|
"error.not_ready": "编辑器仍在启动中。",
|
|
86
|
-
"error.binding_disconnected": "编辑器窗口已断开。请重新连接。",
|
|
87
85
|
"error.binding_expired": "与编辑器的连接已过期。请重新连接。",
|
|
88
86
|
"error.binding_closed": "与编辑器的连接已关闭。请重新连接。",
|
|
89
87
|
"error.internal_error": "插件出错:{message}",
|
|
@@ -128,7 +126,6 @@ window.__ModuleLoader__.load({
|
|
|
128
126
|
"error.desktop_port_in_use": "DSH Desktop 用のエディターのポートが使われています: {message}。プラグインの設定 desktopPort で別の番号にしてください。",
|
|
129
127
|
"error.unknown_binding": "このエディターとの接続はもうありません。再接続してください。",
|
|
130
128
|
"error.not_ready": "エディターはまだ起動中です。",
|
|
131
|
-
"error.binding_disconnected": "エディターの画面が切断されました。再接続してください。",
|
|
132
129
|
"error.binding_expired": "エディターとの接続が期限切れになりました。再接続してください。",
|
|
133
130
|
"error.binding_closed": "エディターとの接続が閉じられました。再接続してください。",
|
|
134
131
|
"error.internal_error": "プラグインでエラーが起きました: {message}",
|
|
@@ -185,7 +182,6 @@ window.__ModuleLoader__.load({
|
|
|
185
182
|
state: zod.z.enum([
|
|
186
183
|
"pending",
|
|
187
184
|
"ready",
|
|
188
|
-
"disconnected",
|
|
189
185
|
"expired",
|
|
190
186
|
"closed"
|
|
191
187
|
]),
|
|
@@ -201,7 +197,6 @@ window.__ModuleLoader__.load({
|
|
|
201
197
|
// src/client/session-bus.ts
|
|
202
198
|
/** After these the page must reconnect; files are never sent to another window. */
|
|
203
199
|
const FINAL_PHASES = /* @__PURE__ */ new Set([
|
|
204
|
-
"disconnected",
|
|
205
200
|
"expired",
|
|
206
201
|
"closed",
|
|
207
202
|
"error"
|
|
@@ -329,7 +324,6 @@ window.__ModuleLoader__.load({
|
|
|
329
324
|
/** After these, the binding is gone and the page must reconnect. */
|
|
330
325
|
const FINAL_ERRORS = /* @__PURE__ */ new Set([
|
|
331
326
|
"unknown_binding",
|
|
332
|
-
"binding_disconnected",
|
|
333
327
|
"binding_expired",
|
|
334
328
|
"binding_closed"
|
|
335
329
|
]);
|
package/dist/index.mjs
CHANGED
|
@@ -46,7 +46,6 @@ const HOST_ERROR_CODES = [
|
|
|
46
46
|
"desktop_port_in_use",
|
|
47
47
|
"unknown_binding",
|
|
48
48
|
"not_ready",
|
|
49
|
-
"binding_disconnected",
|
|
50
49
|
"binding_expired",
|
|
51
50
|
"binding_closed",
|
|
52
51
|
"internal_error"
|
|
@@ -82,7 +81,6 @@ z.strictObject({
|
|
|
82
81
|
state: z.enum([
|
|
83
82
|
"pending",
|
|
84
83
|
"ready",
|
|
85
|
-
"disconnected",
|
|
86
84
|
"expired",
|
|
87
85
|
"closed"
|
|
88
86
|
]),
|
|
@@ -196,7 +194,7 @@ var CodeServerError = class extends Error {
|
|
|
196
194
|
}
|
|
197
195
|
};
|
|
198
196
|
// src/host/bridge.ts
|
|
199
|
-
/**
|
|
197
|
+
/** A binding without a window expires this long after it was created or lost its window. */
|
|
200
198
|
const CONNECT_TIMEOUT_MS = 6e4;
|
|
201
199
|
/** A binding expires this long after the page last watched it. */
|
|
202
200
|
const LEASE_MS = 12e4;
|
|
@@ -206,7 +204,6 @@ const OPEN_TIMEOUT_MS = 15e3;
|
|
|
206
204
|
const WATCH_MS = 2e4;
|
|
207
205
|
const HELLO_TIMEOUT_MS = 5e3;
|
|
208
206
|
const finalError = {
|
|
209
|
-
disconnected: "binding_disconnected",
|
|
210
207
|
expired: "binding_expired",
|
|
211
208
|
closed: "binding_closed"
|
|
212
209
|
};
|
|
@@ -252,6 +249,7 @@ var Bridge = class Bridge {
|
|
|
252
249
|
reconnect: false,
|
|
253
250
|
announcedCwd: cwd,
|
|
254
251
|
leaseUntil: now + LEASE_MS,
|
|
252
|
+
peer: void 0,
|
|
255
253
|
opens: /* @__PURE__ */ new Map(),
|
|
256
254
|
watchers: /* @__PURE__ */ new Set()
|
|
257
255
|
};
|
|
@@ -375,16 +373,29 @@ var Bridge = class Bridge {
|
|
|
375
373
|
const deadline = binding.state === "pending" ? Math.min(binding.connectBy, binding.leaseUntil) : binding.leaseUntil;
|
|
376
374
|
binding.timer = setTimeout(() => this.#finish(binding, "expired"), deadline - Date.now());
|
|
377
375
|
}
|
|
378
|
-
/**
|
|
379
|
-
#
|
|
380
|
-
if (binding.state !== "pending" && binding.state !== "ready") return;
|
|
381
|
-
binding.state = state;
|
|
376
|
+
/** Let go of the window: its unanswered requests have an unknown outcome. */
|
|
377
|
+
#detach(binding, reason) {
|
|
382
378
|
binding.peer?.destroy();
|
|
379
|
+
binding.peer = void 0;
|
|
383
380
|
for (const open of binding.opens.values()) {
|
|
384
381
|
clearTimeout(open.timer);
|
|
385
|
-
open.reject(new CodeServerError("outcome_unknown",
|
|
382
|
+
open.reject(new CodeServerError("outcome_unknown", `${reason} before the editor answered`));
|
|
386
383
|
}
|
|
387
384
|
binding.opens.clear();
|
|
385
|
+
}
|
|
386
|
+
/** Wait for the window again, as for a new binding. */
|
|
387
|
+
#lose(binding) {
|
|
388
|
+
this.#detach(binding, "The editor window went away");
|
|
389
|
+
binding.state = "pending";
|
|
390
|
+
binding.connectBy = Date.now() + CONNECT_TIMEOUT_MS;
|
|
391
|
+
this.#schedule(binding);
|
|
392
|
+
this.#changed(binding);
|
|
393
|
+
}
|
|
394
|
+
/** Enter a final state once; the record stays for a lease so watches can read it. */
|
|
395
|
+
#finish(binding, state) {
|
|
396
|
+
if (binding.state !== "pending" && binding.state !== "ready") return;
|
|
397
|
+
binding.state = state;
|
|
398
|
+
this.#detach(binding, `The binding was ${state}`);
|
|
388
399
|
clearTimeout(binding.timer);
|
|
389
400
|
binding.timer = setTimeout(() => this.#bindings.delete(binding.id), LEASE_MS);
|
|
390
401
|
this.#changed(binding);
|
|
@@ -406,7 +417,7 @@ var Bridge = class Bridge {
|
|
|
406
417
|
socket.on("close", () => {
|
|
407
418
|
this.#sockets.delete(socket);
|
|
408
419
|
clearTimeout(helloTimer);
|
|
409
|
-
if (binding?.peer === socket) this.#
|
|
420
|
+
if (binding?.peer === socket) this.#lose(binding);
|
|
410
421
|
});
|
|
411
422
|
readFrames(socket, (value) => {
|
|
412
423
|
if (refused) return;
|
|
@@ -415,8 +426,9 @@ var Bridge = class Bridge {
|
|
|
415
426
|
const hello = helloSchema.safeParse(value);
|
|
416
427
|
if (!hello.success) return reject("invalid_hello", "Expected a hello frame");
|
|
417
428
|
const candidate = this.#bindings.get(hello.data.bindingId);
|
|
418
|
-
if (candidate?.state !== "pending") return reject("binding_unavailable", "The binding
|
|
429
|
+
if (candidate?.state !== "pending" && candidate?.state !== "ready") return reject("binding_unavailable", "The binding has ended");
|
|
419
430
|
if (candidate.cwd !== hello.data.cwd) return reject("cwd_mismatch", "The window opened another folder");
|
|
431
|
+
this.#detach(candidate, "Another window took over");
|
|
420
432
|
binding = candidate;
|
|
421
433
|
binding.peer = socket;
|
|
422
434
|
binding.state = "ready";
|
|
Binary file
|
package/package.json
CHANGED