dsh-code-server-app 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/lib/index.js CHANGED
@@ -99,6 +99,9 @@ export const Config = z.object({
99
99
  * (同源、无额外端口、复用 DSH 的 Host/Origin + cookie 防护;需要 DSH 提供 webServer 服务,
100
100
  * 缺失或注册失败时自动回退 loopback)。 */
101
101
  serve: z.union([z.const('loopback'), z.const('dsh')]).default('loopback'),
102
+ /** keepResident=true(默认)客户端在宿主 running 后把 IDE 预先加载到"停放区",
103
+ * 切标签/收起侧栏不再重载;false 则只在打开面板时才加载(省内存)。 */
104
+ keepResident: z.boolean().default(true),
102
105
  });
103
106
 
104
107
  const DEFAULT_CONFIG = {
@@ -384,6 +387,7 @@ export async function apply(ctx, config) {
384
387
  let reserveComposer = true;
385
388
  let windowedOpen = false;
386
389
  let serveSetting = 'loopback';
390
+ let keepResident = true; // 客户端常驻预热(0.2.2)
387
391
  if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
388
392
  try {
389
393
  const scope = settingsSvc.register(SETTINGS_NS, Config);
@@ -393,6 +397,7 @@ export async function apply(ctx, config) {
393
397
  reserveComposer = resolved && typeof resolved.reserveComposer === 'boolean' ? resolved.reserveComposer : true;
394
398
  windowedOpen = resolved && typeof resolved.windowedOpen === 'boolean' ? resolved.windowedOpen : false;
395
399
  serveSetting = resolved && resolved.serve === 'dsh' ? 'dsh' : 'loopback';
400
+ keepResident = resolved && typeof resolved.keepResident === 'boolean' ? resolved.keepResident : true;
396
401
  }
397
402
  scope.watch((next) => {
398
403
  if (next != null && typeof next.reserveComposer === 'boolean') {
@@ -409,6 +414,10 @@ export async function apply(ctx, config) {
409
414
  console.log(`[code-server] serve updated: ${serveSetting}(下次启动生效)`);
410
415
  }
411
416
  }
417
+ if (next != null && typeof next.keepResident === 'boolean') {
418
+ keepResident = next.keepResident;
419
+ console.log(`[code-server] keepResident updated: ${keepResident}`);
420
+ }
412
421
  });
413
422
  } catch (error) {
414
423
  console.error(`[code-server] settings unavailable; using defaults (reserveComposer=true, windowedOpen=false): ${error.message}`);
@@ -502,6 +511,7 @@ export async function apply(ctx, config) {
502
511
  adopted: state.adopted,
503
512
  reserveComposer,
504
513
  windowedOpen,
514
+ keepResident,
505
515
  env: state.env,
506
516
  setup: {
507
517
  running: state.setup.running,
package/lib/launcher.mjs CHANGED
@@ -207,6 +207,56 @@ function handleProxyUpgrade(req, socket, head, target) {
207
207
  socket.on('close', () => upstream.destroy());
208
208
  }
209
209
 
210
+ // ---------------------------------------------------------------- 跨源防护(等价 code-server 的 ensureOrigin)
211
+
212
+ /** 取请求 Host:优先 `Forwarded: host=`、其次 `X-Forwarded-Host`(取第一个)、最后 `Host`。
213
+ * trim + 小写,与 code-server 的 getHost() 同语义(反向代理未透传 Host 时返回 undefined)。 */
214
+ function getHostHeader(req) {
215
+ const first = (name) => {
216
+ const value = req.headers[name];
217
+ return Array.isArray(value) ? value[0] : value;
218
+ };
219
+ const forwarded = first('forwarded');
220
+ if (forwarded !== undefined && forwarded !== '') {
221
+ for (const part of forwarded.split(/[;,]/)) {
222
+ const eq = part.indexOf('=');
223
+ if (eq < 0) continue;
224
+ const key = part.slice(0, eq).trim().toLowerCase();
225
+ const value = part.slice(eq + 1).trim();
226
+ if (key === 'host' && value !== '') return value.toLowerCase();
227
+ }
228
+ }
229
+ const xHost = first('x-forwarded-host');
230
+ if (xHost !== undefined && xHost !== '') {
231
+ const head = xHost.split(',')[0];
232
+ if (head !== undefined && head.trim() !== '') return head.trim().toLowerCase();
233
+ }
234
+ const host = first('host');
235
+ return host !== undefined && host !== '' ? host.trim().toLowerCase() : undefined;
236
+ }
237
+
238
+ /** code-server `authenticateOrigin()` 的等价物:带 Origin 的请求(浏览器)其 host 必须等于 Host;
239
+ * 缺 Origin(非浏览器,如本地工具/测试)放行。code-server 另外支持 `--trusted-origins` /
240
+ * `--proxy-domain` 通配,本插件没有这两个概念,故不实现。
241
+ * 为什么必须有:VS Code 的 handleUpgrade 不校验来源,而 IDE 是 auth=none —— 缺了这道检查,
242
+ * 本机任意浏览器页面都能开 ws://127.0.0.1:<port>/stable-<commit> 直接驱动 IDE。
243
+ * (dsh 模式下请求来自 DSH 自己的路由并已过 requestRejection,Origin/Host 天然一致。) */
244
+ function originAllowed(req) {
245
+ const raw = req.headers.origin;
246
+ const originRaw = Array.isArray(raw) ? raw[0] : raw;
247
+ if (originRaw === undefined || originRaw === '') return true;
248
+ let origin;
249
+ try {
250
+ origin = new URL(originRaw).host.trim().toLowerCase();
251
+ } catch {
252
+ return false;
253
+ }
254
+ if (origin === '') return false;
255
+ const host = getHostHeader(req);
256
+ if (host === undefined) return false;
257
+ return host === origin;
258
+ }
259
+
210
260
  // ---------------------------------------------------------------- HTTP / WS 分发
211
261
 
212
262
  const server = createHttpServer((req, res) => {
@@ -243,6 +293,12 @@ server.on('upgrade', (req, socket, head) => {
243
293
  const url = req.url ?? '/';
244
294
  recentUpgrades.push(url);
245
295
  if (recentUpgrades.length > 10) recentUpgrades.shift();
296
+ // 跨源防护(与 code-server 一致:只卡 upgrade,HTTP 侧 VS Code 仅接受 GET 且状态变更都走 WS)
297
+ if (!originAllowed(req)) {
298
+ log(`拒绝跨源 WebSocket:origin=${req.headers.origin} host=${req.headers.host ?? '-'} url=${url}`);
299
+ socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
300
+ return;
301
+ }
246
302
  if (!args.disableProxy) {
247
303
  const target = proxyTarget(url);
248
304
  if (target !== null) { handleProxyUpgrade(req, socket, head, target); return; }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code-server-app",
3
- "version": "0.2.0",
4
- "description": "VS Code (from a code-server release) inside DSH Web and Desktop: right-sidebar tab with a floating-ball fallback, driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
3
+ "version": "0.2.2",
4
+ "description": "VS Code (from a code-server release) inside DSH Web and Desktop: right-sidebar tab with a floating-ball fallback, driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). The IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs no longer reloads the workbench. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
5
5
  "homepage": "https://github.com/jinsiyu/dsh-code-server-app",
6
6
  "repository": {
7
7
  "type": "git",
@@ -3,7 +3,7 @@
3
3
  "vscodeVersion": "1.136.1",
4
4
  "productPath": "stable-8d5f383f301ca20681f5b6606b8207d9dc87bdd8",
5
5
  "layout": "vscode-only",
6
- "preparedAt": "2026-09-10T08:30:15.204Z",
6
+ "preparedAt": "2026-09-10T13:43:43.880Z",
7
7
  "source": "registry",
8
8
  "node": "v24.13.1",
9
9
  "platform": "win32",