dshost-plugin 0.1.14 → 0.1.15

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/core.js CHANGED
@@ -375,6 +375,63 @@ export function startAgent(opts) {
375
375
 
376
376
  const svcList = normalizeServices(services, log);
377
377
  const svcMap = new Map(svcList.map((s) => [s.id, s]));
378
+ // 单业务配置管理(门户托管):宿主显式开启 remoteServices 才接受 relay 下发的
379
+ // 'services:config';下发项与本机声明走同一 normalizeServices 白名单,且只允许
380
+ // auth=open、不接受任何凭据字段;与本机声明同 id 的下发项一律拒绝(本机为准)。
381
+ const remoteServices = opts.remoteServices === true || process.env.DSH_REMOTE_SERVICES === '1';
382
+ let managedList = [];
383
+ let lastApplied = null;
384
+ function servicesFrame() {
385
+ const pub = ({ id, name, desc, icon, auth, entry, spa, category }) => ({ id, name, desc, icon, auth, entry, spa, category });
386
+ return {
387
+ type: 'services',
388
+ servicesOnly: process.env.DSH_SERVICES_ONLY === '1' || undefined,
389
+ capabilities: remoteServices && process.env.DSH_SERVICES_ONLY !== '1' ? { remoteConfig: 1 } : undefined,
390
+ services: [
391
+ ...svcList.map((s) => ({ ...pub(s), origin: 'local' })),
392
+ ...managedList.map((s) => ({ ...pub(s), origin: 'managed' })),
393
+ ],
394
+ applied: lastApplied || undefined,
395
+ };
396
+ }
397
+ function applyManagedConfig(raw) {
398
+ const results = [];
399
+ const accepted = [];
400
+ const localIds = new Set(svcList.map((s) => s.id));
401
+ for (const item of Array.isArray(raw) ? raw.slice(0, 32) : []) {
402
+ const id = String(item?.id || '');
403
+ if (localIds.has(id)) { results.push({ id, ok: false, error: '与本机声明的服务同名,以本机为准' }); continue; }
404
+ const warn = [];
405
+ const [svc] = normalizeServices([{
406
+ id, name: item?.name, desc: item?.desc, icon: item?.icon, upstream: item?.upstream,
407
+ entry: item?.entry, spa: item?.spa === true, auth: 'open',
408
+ }], { warn: (m) => warn.push(m) });
409
+ if (!svc) { results.push({ id, ok: false, error: (warn[0] || 'invalid').replace(/^\[agent\] /, '') }); continue; }
410
+ svc.category = typeof item?.category === 'string' ? item.category.slice(0, 16) : '';
411
+ accepted.push(svc);
412
+ results.push({ id, ok: true });
413
+ }
414
+ for (const s of managedList) svcMap.delete(s.id);
415
+ managedList = accepted;
416
+ for (const s of managedList) svcMap.set(s.id, s);
417
+ lastApplied = results;
418
+ log.log?.(`[agent] managed services applied: ${results.map((r) => `${r.id}:${r.ok ? 'ok' : 'rejected'}`).join(', ') || '(none)'}`);
419
+ }
420
+ function probeUpstream(ws, msg) {
421
+ const t0 = Date.now();
422
+ const reply = (r) => send(ws, { type: 'services:probe-result', reqId: msg.reqId, ms: Date.now() - t0, ...r });
423
+ const up = parseUpstream(msg.upstream);
424
+ if (!up) { reply({ ok: false, error: '上游地址格式错误' }); return; }
425
+ if (!upstreamAllowed(up.host)) { reply({ ok: false, error: `${up.host} 不在实例白名单内(回环 / 10.144.144.0/24 / AGENT_UPSTREAM_ALLOW)` }); return; }
426
+ const entry = typeof msg.entry === 'string' && msg.entry.startsWith('/') ? msg.entry : '/';
427
+ const req = http.request({ host: up.host, port: up.port, path: entry, method: 'GET', timeout: 5000, headers: { host: `${up.host}:${up.port}` } }, (res) => {
428
+ res.resume();
429
+ reply({ ok: res.statusCode < 500, status: res.statusCode });
430
+ });
431
+ req.on('timeout', () => { req.destroy(new Error('连接超时(5s)')); });
432
+ req.on('error', (e) => reply({ ok: false, error: e.code === 'ECONNREFUSED' ? '连接被拒绝(端口没有服务在监听)' : e.message }));
433
+ req.end();
434
+ }
378
435
  let relayUsername = null; // set on 'registered'; subject of SSO trust headers
379
436
  if (svcList.length) {
380
437
  log.log?.(`[agent] plugin services: ${svcList.map((s) => `${s.id}->${s.host}:${s.port}${s.auth === 'sso' ? ' (sso)' : ''}`).join(', ')}`);
@@ -487,17 +544,26 @@ export function startAgent(opts) {
487
544
  send(ws, { type: 'system-info', info: collectSystemInfo(dshVersion) });
488
545
  // 插件工作区:上报本实例声明的服务清单(relay 存实例维度,供 /p 路由校验)
489
546
  // DSH_SERVICES_ONLY=1:独立 services agent,relay 不注册主实例(服务旁挂到同机 dsh 实例)
490
- send(ws, {
491
- type: 'services',
492
- servicesOnly: process.env.DSH_SERVICES_ONLY === '1' || undefined,
493
- services: svcList.map(({ id, name, desc, icon, auth, entry, spa }) => ({ id, name, desc, icon, auth, entry, spa })),
494
- });
547
+ send(ws, servicesFrame());
495
548
  startHeartbeat(ws);
496
549
  return;
497
550
  }
498
551
 
499
552
  if (msg.type === 'heartbeat') return;
500
553
 
554
+ // 门户托管业务下发(仅宿主开启 remoteServices 时接受)
555
+ if (msg.type === 'services:config') {
556
+ if (!remoteServices) { log.warn?.('[agent] services:config ignored (remoteServices disabled)'); return; }
557
+ applyManagedConfig(msg.services);
558
+ send(ws, servicesFrame());
559
+ return;
560
+ }
561
+ if (msg.type === 'services:probe') {
562
+ if (!remoteServices) { send(ws, { type: 'services:probe-result', reqId: msg.reqId, ok: false, error: 'remoteServices disabled' }); return; }
563
+ probeUpstream(ws, msg);
564
+ return;
565
+ }
566
+
501
567
  if (msg.type === 'http:request') {
502
568
  // [Artifact Viewer] 是 dsh UI 注入脚本的专属旁路;插件流不经过它。
503
569
  if (!msg.target && serveArtifactIfRequested(ws, msg)) return;
package/lib/index.js CHANGED
@@ -20,6 +20,8 @@ export function apply(ctx, config) {
20
20
  const dshPort = parseInt(config.dshPort || process.env.DSH_PORT || String(DSH_PORT), 10);
21
21
  // 插件工作区服务清单(cordis.patch.yml 的 config.services 数组)
22
22
  const services = Array.isArray(config.services) ? config.services : [];
23
+ // 单业务配置管理:允许 dshost.me 门户下发托管业务(显式开启;上游白名单仍在本机强制)
24
+ const remoteServices = config.remoteServices === true;
23
25
 
24
26
  console.log('[dsh-remote] Plugin loaded, token:', token ? '***' : 'none');
25
27
  if (services.length) console.log('[dsh-remote] Plugin services:', services.map((s) => s && s.id).filter(Boolean).join(', '));
@@ -41,6 +43,7 @@ export function apply(ctx, config) {
41
43
  dshHost,
42
44
  dshPort,
43
45
  services,
46
+ remoteServices,
44
47
  });
45
48
 
46
49
  // dsh >= 0.1.2 web auth: the web UI is gated behind a per-process one-time
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dshost-plugin",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Official remote cloud relay plugin for DSHost (dshost.me): securely access your dsh Web UI from anywhere",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",