ec-web 0.0.21 → 0.0.23

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/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * 用法:
6
6
  * ec-web [--port 42705] [--home <EVOLCORE_HOME>]
7
7
  *
8
- * 通过 EVOLCORE_HOME 定位 evolcore 数据目录,启动 HTTP+WS 服务,浏览器配对码登录。
8
+ * 通过 EVOLCORE_HOME 定位 evolcore 数据目录,启动 HTTP+WS 服务;配对码和访问 token 由 daemon 管理。
9
9
  * 与 evolcore daemon 通过 IPC socket(live 状态)+ 文件系统(历史数据)旁路通信。
10
10
  */
11
11
  import fs from 'fs';
@@ -106,7 +106,9 @@ for (const list of Object.values(ifaces)) {
106
106
  }
107
107
  }
108
108
  process.stdout.write(`\n${BOLD}${CYAN}EC Web — EvolCore${RST} ${DIM}(home: ${p.root})${RST}\n\n`);
109
- process.stdout.write(` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(5 分钟内有效,配对后 token 缓存 30 天,有访问自动续期)${RST}\n\n`);
109
+ process.stdout.write(handle.pairingCode
110
+ ? ` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(daemon 提供,5 分钟内有效,配对后 token 缓存 30 天,有访问自动续期)${RST}\n\n`
111
+ : ` ${BOLD}配对码:${RST} ${YELLOW}daemon 暂不可用,请先启动 EvolCore${RST}\n\n`);
110
112
  process.stdout.write(` ${BOLD}本机:${RST} http://localhost:${handle.port}\n`);
111
113
  for (const ip of lanIps)
112
114
  process.stdout.write(` ${BOLD}局域网:${RST} http://${ip}:${handle.port}\n`);
@@ -12,7 +12,18 @@ import path from 'path';
12
12
  // Keep this list aligned with evolcore's local-control IPC gate. `evolagent.list`
13
13
  // is used by the Agents view and is read-only from EC Web's perspective, but the
14
14
  // daemon still requires the local control token before returning its registry.
15
- const LOCAL_CONTROL_COMMANDS = new Set(['evolagent.list', 'menu.exec', 'monitor-snapshot']);
15
+ const LOCAL_CONTROL_COMMANDS = new Set([
16
+ 'evolagent.list',
17
+ 'menu.exec',
18
+ 'model.catalog',
19
+ 'monitor-snapshot',
20
+ 'monitor-subscribe',
21
+ 'monitor-unsubscribe',
22
+ 'remote-auth.pair-code',
23
+ 'remote-auth.issue-local',
24
+ 'remote-auth.redeem',
25
+ 'remote-auth.validate',
26
+ ]);
16
27
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
17
28
  let request = cmd;
18
29
  if (LOCAL_CONTROL_COMMANDS.has(cmd.type) && typeof cmd.controlToken !== 'string') {
package/dist/paths.js CHANGED
@@ -25,10 +25,27 @@ export function resolveRoot() {
25
25
  }
26
26
  return _root;
27
27
  }
28
+ function stripTrailingWindowsSeparators(value) {
29
+ const stripped = value.replace(/[\\/]+$/g, '');
30
+ // Keep a drive-root separator. `C:` is a drive-relative path on Windows.
31
+ if (/^[a-zA-Z]:$/.test(stripped) && /^[a-zA-Z]:[\\/]+$/.test(value)) {
32
+ return `${stripped}\\`;
33
+ }
34
+ return stripped;
35
+ }
36
+ /**
37
+ * Canonicalize EVOLCORE_HOME exactly like the main EvolCore package does.
38
+ * The CLI and EC Web may receive the same Windows directory with different
39
+ * casing, slash styles, or a trailing separator; all must select one pipe.
40
+ */
41
+ export function normalizeWindowsSocketRoot(root) {
42
+ return stripTrailingWindowsSeparators(path.win32.resolve(root)).toLowerCase();
43
+ }
28
44
  /** socket 路径派生:与 evolcore resolveInstanceSocketPath 完全一致 */
29
45
  function resolveInstanceSocketPath(root) {
30
46
  if (isWindows) {
31
- const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
47
+ const canonicalRoot = normalizeWindowsSocketRoot(root);
48
+ const hash = crypto.createHash('sha1').update(canonicalRoot).digest('hex').slice(0, 12);
32
49
  return `\\\\.\\pipe\\daemon-${hash}`;
33
50
  }
34
51
  return path.join(root, 'data', 'instance', 'daemon.sock');
package/dist/server.js CHANGED
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * - HTTP: 静态资源 + 配对 API
5
5
  * - WebSocket: 订阅式实时推送(aid / msg / session)
6
- * - 鉴权: 6 位配对码(5 分钟有效)→ token(30 天,有访问自动续期),持久化到磁盘
6
+ * - 鉴权: daemon 通过本地 IPC 提供 6 位配对码(5 分钟有效)和远程访问 token(30 天滑动续期)
7
7
  * - 安全: 绑定 0.0.0.0(支持远程访问),token 校验;远程连接可执行受控管理操作,不能修改 daemon 配置或读取 secret
8
8
  *
9
- * 与 evolcore 的唯一通信:启动时发 ping 检查 protocolVersion(soft 校验)。
9
+ * 与 evolcore daemon 通过 IPC socket(状态、Menu 和远程访问凭据)+ 文件系统(历史数据)旁路通信。
10
10
  */
11
11
  import http from 'http';
12
12
  import fs from 'fs';
@@ -33,13 +33,11 @@ import { ccProjectsDir } from './paths.js';
33
33
  import { relationRolesSource, handleRelationRolesApi, handlePeerRoleApi } from './sources/relation-roles.js';
34
34
  import { roleDefinitionsSource, handleRoleDefinitionsApi } from './sources/role-definitions.js';
35
35
  import { handleModelsApi } from './sources/models.js';
36
- import { detectBaseAgents } from './sources/baseagent-detector.js';
36
+ import { detectBaseAgentsAsync } from './sources/baseagent-detector.js';
37
37
  import { readCodexSessionResult } from './sources/session-codex.js';
38
38
  import { handleBasicSettingsApi } from './sources/basic-settings.js';
39
39
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
40
40
  const STATIC_DIR = path.join(__dirname, 'static');
41
- const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30天(滑动窗口:每次有效访问刷新 lastActive 自动续期)
42
- const PAIRING_TTL_MS = 5 * 60 * 1000; // 5min
43
41
  const DEFAULT_PORT = 42705;
44
42
  const PROTOCOL_VERSION = 1; // 与 evolcore ping response 对齐的软校验版本
45
43
  function daemonOwnerMenuAuth(localDirect, actorAid) {
@@ -61,14 +59,6 @@ function daemonTriggerControlToken() {
61
59
  return undefined;
62
60
  }
63
61
  }
64
- function matchesDaemonControlToken(value) {
65
- const expected = daemonTriggerControlToken();
66
- if (typeof value !== 'string' || !expected)
67
- return false;
68
- const provided = Buffer.from(value);
69
- const target = Buffer.from(expected);
70
- return provided.length === target.length && crypto.timingSafeEqual(provided, target);
71
- }
72
62
  const SOURCES = { agents: aidSource, msg: msgSource, session: sessionSource, cache: cacheSource, system: systemSource, triggers: triggersSource, monitor: monitorSource, logs: logsSource, gateway: gatewaySource, roles: relationRolesSource, roleDefinitions: roleDefinitionsSource };
73
63
  // ECWeb 自身版本:渲染 System 页时随快照下发(不走 daemon IPC,ECWeb 就是这个进程)。
74
64
  function readEcwebVersion() {
@@ -104,66 +94,21 @@ const MIME = {
104
94
  };
105
95
  const HTML_CACHE_CONTROL = 'no-cache';
106
96
  const STATIC_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
107
- function tokenStorePath() {
108
- const dir = resolvePaths().instanceDir;
109
- const current = path.join(dir, 'ecweb-tokens.json');
110
- const legacy = path.join(dir, 'watch-web-tokens.json');
111
- if (!fs.existsSync(current) && fs.existsSync(legacy)) {
112
- try {
113
- fs.renameSync(legacy, current);
114
- }
115
- catch { }
116
- }
117
- return current;
118
- }
119
- function loadTokens() {
120
- try {
121
- const store = JSON.parse(fs.readFileSync(tokenStorePath(), 'utf-8'));
122
- if (Array.isArray(store.tokens))
123
- return store;
124
- }
125
- catch { }
126
- return { tokens: [] };
127
- }
128
- function saveTokens(store) {
129
- try {
130
- fs.mkdirSync(resolvePaths().instanceDir, { recursive: true });
131
- const tmp = tokenStorePath() + '.tmp';
132
- fs.writeFileSync(tmp, JSON.stringify(store, null, 2));
133
- fs.renameSync(tmp, tokenStorePath());
134
- }
135
- catch { }
136
- }
137
- function pruneExpired(store, now) {
138
- const before = store.tokens.length;
139
- store.tokens = store.tokens.filter(t => now - t.lastActive < TOKEN_TTL_MS);
140
- return store.tokens.length !== before;
97
+ async function remoteAuth(type, payload = {}) {
98
+ const response = await ipcQuery(resolvePaths().socket, { type, ...payload }, 2_000);
99
+ return response && typeof response === 'object' ? response : null;
141
100
  }
142
- function validAuthorizedBy(value) {
143
- if (typeof value !== 'string')
144
- return undefined;
145
- const normalized = value.trim();
146
- if (!normalized || normalized.length > 512 || /[\r\n]/.test(normalized))
147
- return undefined;
148
- return normalized;
149
- }
150
- function getValidTokenRecord(token, now) {
101
+ async function validateRemoteToken(token) {
151
102
  if (!token)
152
103
  return null;
153
- const store = loadTokens();
154
- const changed = pruneExpired(store, now);
155
- const rec = store.tokens.find(t => t.token === token);
156
- if (rec) {
157
- rec.lastActive = now;
158
- saveTokens(store);
159
- return rec;
160
- }
161
- if (changed)
162
- saveTokens(store);
163
- return null;
104
+ const result = await remoteAuth('remote-auth.validate', { token });
105
+ if (!result?.ok || result.valid !== true)
106
+ return null;
107
+ return result.authorizedBy ? { authorizedBy: result.authorizedBy } : {};
164
108
  }
165
- function validateAndRenew(token, now) {
166
- return getValidTokenRecord(token, now) !== null;
109
+ async function issueLocalRemoteToken(label) {
110
+ const result = await remoteAuth('remote-auth.issue-local', { label });
111
+ return result?.ok && typeof result.token === 'string' ? result.token : null;
167
112
  }
168
113
  // ── Static ──
169
114
  // 经 AUN Service Proxy 转发时,proxy-server 注入 x-forwarded-prefix 头,
@@ -469,11 +414,8 @@ function isLocalDirect(req) {
469
414
  const providerAid = req.headers['x-aun-provider-aid'];
470
415
  return !providerAid || (Array.isArray(providerAid) ? providerAid.length === 0 : !providerAid.trim());
471
416
  }
472
- function genPairingCode() {
473
- return String(crypto.randomInt(0, 1000000)).padStart(6, '0');
474
- }
475
417
  // ── Pair handler ──
476
- function handlePair(req, res, pairingCode, pairingExpiry, authorizedBy, log) {
418
+ function handlePair(req, res, log) {
477
419
  let body = '';
478
420
  req.on('data', (chunk) => { body += chunk; if (body.length > 4096)
479
421
  req.destroy(); });
@@ -484,47 +426,34 @@ function handlePair(req, res, pairingCode, pairingExpiry, authorizedBy, log) {
484
426
  code = String(JSON.parse(body).code || '');
485
427
  }
486
428
  catch { }
487
- if (Date.now() > pairingExpiry) {
488
- res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: '配对码已过期,请重启 watch' }));
489
- log(`✗ 配对失败(码已过期) from ${ip}`);
490
- return;
491
- }
492
- if (code !== pairingCode) {
493
- res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: '配对码错误' }));
494
- log(`✗ 配对失败(码错误: ${code}) from ${ip}`);
495
- return;
496
- }
497
- const now = Date.now();
498
- const token = crypto.randomBytes(32).toString('hex');
499
- const store = loadTokens();
500
- pruneExpired(store, now);
501
- store.tokens.push({
502
- token,
503
- createdAt: now,
504
- lastActive: now,
429
+ void remoteAuth('remote-auth.redeem', {
430
+ code,
505
431
  label: ip,
506
- ...(authorizedBy ? { authorizedBy } : {}),
432
+ }).then(result => {
433
+ if (!result?.ok || typeof result.token !== 'string') {
434
+ const status = result?.code === 'PAIRING_EXPIRED' ? 403 : 403;
435
+ res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: result?.error ?? '配对失败' }));
436
+ log(`✗ 配对失败(${result?.code ?? 'daemon-unavailable'}) from ${ip}`);
437
+ return;
438
+ }
439
+ res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: true, token: result.token }));
440
+ log(`✓ 配对成功 from ${ip}(daemon token 缓存 30 天,有访问自动续期)`);
441
+ }).catch(error => {
442
+ res.writeHead(503, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: 'daemon unavailable' }));
443
+ log(`✗ 配对失败(daemon unavailable: ${error instanceof Error ? error.message : String(error)}) from ${ip}`);
507
444
  });
508
- saveTokens(store);
509
- res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: true, token }));
510
- log(`✓ 配对成功 from ${ip}(token 缓存 30 天,有访问自动续期${authorizedBy ? `,批准者 ${authorizedBy}` : ''})`);
511
445
  });
512
446
  }
513
447
  /**
514
448
  * 本地直连免配对:自动发 token。远程(隧道或真远程)需配对码。
515
449
  * 本地直连判定:socket 来源是回环 AND 无 x-aun-provider-aid 头(非隧道)。
516
450
  */
517
- function issueLocalDirectToken(req, log) {
451
+ async function issueLocalDirectToken(req, log) {
518
452
  if (!isLocalDirect(req))
519
453
  return null;
520
- const now = Date.now();
521
- const token = crypto.randomBytes(32).toString('hex');
522
- const store = loadTokens();
523
- pruneExpired(store, now);
524
- const ip = clientIp(req);
525
- store.tokens.push({ token, createdAt: now, lastActive: now, label: `local-direct:${ip}` });
526
- saveTokens(store);
527
- log(`✓ 本地直连自动授权 from ${ip}`);
454
+ const token = await issueLocalRemoteToken(`local-direct:${clientIp(req)}`);
455
+ if (token)
456
+ log(`✓ 本地直连自动授权 from ${clientIp(req)}`);
528
457
  return token;
529
458
  }
530
459
  // ── WebSocket connection ──
@@ -555,7 +484,30 @@ function handleConnection(ws, req, log, tokenRecord) {
555
484
  send({ type: 'error', message: `unknown view: ${view}` });
556
485
  return;
557
486
  }
558
- const sourceParams = { ...params, __menuAuth: daemonOwnerMenuAuth(localDirect, actorAid) };
487
+ const sourceParams = {
488
+ ...params,
489
+ __menuAuth: daemonOwnerMenuAuth(localDirect, actorAid),
490
+ __ecwebRequestId: crypto.randomUUID(),
491
+ __ecwebEndpoint: `WS snapshot:${view}`,
492
+ };
493
+ let deactivate = null;
494
+ if (source.activate) {
495
+ try {
496
+ deactivate = await source.activate(sourceParams);
497
+ }
498
+ catch (e) {
499
+ if (generation === subscriptionGeneration)
500
+ send({ type: 'error', message: `subscription failed: ${e?.message || e}` });
501
+ return;
502
+ }
503
+ if (generation !== subscriptionGeneration) {
504
+ deactivate();
505
+ return;
506
+ }
507
+ // Install cleanup before awaiting the initial snapshot, so a view switch
508
+ // or socket close releases the monitor lease immediately.
509
+ unsubscribe = deactivate;
510
+ }
559
511
  // System 视图:把 ECWeb 自身版本合并进快照(前端版本卡用)
560
512
  const decorate = (data) => (view === 'system' && data ? { ...data, ecwebVersion: ECWEB_VERSION } : data);
561
513
  let initialSnapshot;
@@ -570,16 +522,30 @@ function handleConnection(ws, req, log, tokenRecord) {
570
522
  return;
571
523
  send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
572
524
  }
573
- const nextUnsubscribe = source.subscribe(sourceParams, (data) => {
574
- if (generation === subscriptionGeneration && currentView === view) {
575
- send({ type: 'delta', view, data: decorate(data) });
525
+ let nextUnsubscribe;
526
+ try {
527
+ nextUnsubscribe = source.subscribe(sourceParams, (data) => {
528
+ if (generation === subscriptionGeneration && currentView === view) {
529
+ send({ type: 'delta', view, data: decorate(data) });
530
+ }
531
+ });
532
+ }
533
+ catch (e) {
534
+ deactivate?.();
535
+ if (generation === subscriptionGeneration) {
536
+ unsubscribe = null;
537
+ send({ type: 'error', message: `subscription failed: ${e?.message || e}` });
576
538
  }
577
- });
539
+ return;
540
+ }
578
541
  if (generation !== subscriptionGeneration) {
579
542
  nextUnsubscribe();
580
543
  return;
581
544
  }
582
- unsubscribe = nextUnsubscribe;
545
+ unsubscribe = () => {
546
+ nextUnsubscribe();
547
+ deactivate?.();
548
+ };
583
549
  };
584
550
  ws.on('message', async (raw) => {
585
551
  let msg;
@@ -771,26 +737,20 @@ export async function startWatchWebServer(opts = {}) {
771
737
  if (pingResp && pingResp.protocolVersion !== undefined && pingResp.protocolVersion < PROTOCOL_VERSION) {
772
738
  log(`⚠️ evolcore protocolVersion=${pingResp.protocolVersion},watch 期望 >=${PROTOCOL_VERSION},部分功能可能异常`);
773
739
  }
774
- let pairingCode = genPairingCode();
775
- let pairingExpiry = Date.now() + PAIRING_TTL_MS;
776
- let pairingAuthorizedBy;
777
- function freshPairing() {
778
- if (Date.now() > pairingExpiry) {
779
- pairingCode = genPairingCode();
780
- pairingExpiry = Date.now() + PAIRING_TTL_MS;
781
- pairingAuthorizedBy = undefined;
782
- log(`↺ 配对码已刷新:${pairingCode}(5 分钟有效)`);
783
- }
784
- return { code: pairingCode, expiresAt: pairingExpiry };
785
- }
786
- const server = http.createServer((req, res) => {
740
+ const server = http.createServer(async (req, res) => {
787
741
  const pathname = requestPath(req.url || '');
788
742
  const localDirect = isLocalDirect(req);
789
743
  if (req.method === 'GET' && (req.url || '') === '/api/available-baseagents') {
790
744
  // Base agent 可用性检测 API
791
- const available = detectBaseAgents();
792
- res.writeHead(200, { 'Content-Type': 'application/json' });
793
- res.end(JSON.stringify(available));
745
+ try {
746
+ const available = await detectBaseAgentsAsync();
747
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
748
+ res.end(JSON.stringify(available));
749
+ }
750
+ catch (error) {
751
+ res.writeHead(500, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
752
+ res.end(JSON.stringify({ success: false, error: error?.message || String(error) }));
753
+ }
794
754
  }
795
755
  else if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
796
756
  // 取码 API:仅真本地直连可用(socket 回环 + 无 x-aun-provider-aid)。
@@ -801,28 +761,24 @@ export async function startWatchWebServer(opts = {}) {
801
761
  res.end(JSON.stringify({ error: 'forbidden' }));
802
762
  return;
803
763
  }
804
- const { code, expiresAt } = freshPairing();
805
- // daemon 通过 localhost 取码时带上 AUN 已认证的 owner AID。只接受
806
- // 第一次绑定,避免同一配对码在不同 owner 间被重新归属。
807
- const ownerHeader = req.headers['x-evolcore-pair-authorized-by'];
808
- const controlHeader = req.headers['x-evolcore-control-token'];
809
- const requestedOwner = validAuthorizedBy(Array.isArray(ownerHeader) ? ownerHeader[0] : ownerHeader);
810
- const suppliedControlToken = Array.isArray(controlHeader) ? controlHeader[0] : controlHeader;
811
- if (!pairingAuthorizedBy && requestedOwner && matchesDaemonControlToken(suppliedControlToken)) {
812
- pairingAuthorizedBy = requestedOwner;
764
+ const pair = await remoteAuth('remote-auth.pair-code');
765
+ if (!pair?.ok || typeof pair.code !== 'string' || typeof pair.expiresAt !== 'number') {
766
+ res.writeHead(503, { 'Content-Type': 'application/json' });
767
+ res.end(JSON.stringify({ error: 'daemon unavailable' }));
768
+ return;
813
769
  }
814
770
  res.writeHead(200, { 'Content-Type': 'application/json' });
815
- res.end(JSON.stringify({ code, expiresAt }));
771
+ res.end(JSON.stringify({ code: pair.code, expiresAt: pair.expiresAt }));
816
772
  }
817
773
  else if (req.method === 'POST' && (req.url || '').startsWith('/api/pair')) {
818
- // 配对 API:远程(隧道/真远程)需要配对码;本地直连自动发 token 跳过配对。
819
- const autoToken = issueLocalDirectToken(req, log);
774
+ // 配对 API:远程(隧道/真远程)需要 daemon 配对码;本地直连自动发 daemon token。
775
+ const autoToken = await issueLocalDirectToken(req, log);
820
776
  if (autoToken) {
821
777
  res.writeHead(200, { 'Content-Type': 'application/json' });
822
778
  res.end(JSON.stringify({ ok: true, token: autoToken }));
823
779
  return;
824
780
  }
825
- handlePair(req, res, pairingCode, pairingExpiry, pairingAuthorizedBy, log);
781
+ handlePair(req, res, log);
826
782
  }
827
783
  else if (pathname === '/api/basic-settings' || pathname === '/api/basic-settings/secret') {
828
784
  const authHeader = req.headers.authorization || '';
@@ -830,11 +786,11 @@ export async function startWatchWebServer(opts = {}) {
830
786
  const { query } = parseUrl(req.url || '');
831
787
  let token = bearerToken || query.token || '';
832
788
  if (!token) {
833
- const autoToken = issueLocalDirectToken(req, log);
789
+ const autoToken = await issueLocalDirectToken(req, log);
834
790
  if (autoToken)
835
791
  token = autoToken;
836
792
  }
837
- if (!token || !validateAndRenew(token, Date.now())) {
793
+ if (!token || !(await validateRemoteToken(token))) {
838
794
  res.writeHead(401, { 'Content-Type': 'application/json' });
839
795
  res.end(JSON.stringify({ success: false, error: 'unauthorized' }));
840
796
  return;
@@ -858,11 +814,11 @@ export async function startWatchWebServer(opts = {}) {
858
814
  const { query } = parseUrl(req.url || '');
859
815
  let token = bearerToken || query.token || '';
860
816
  if (!token) {
861
- const autoToken = issueLocalDirectToken(req, log);
817
+ const autoToken = await issueLocalDirectToken(req, log);
862
818
  if (autoToken)
863
819
  token = autoToken;
864
820
  }
865
- if (!token || !validateAndRenew(token, Date.now())) {
821
+ if (!token || !(await validateRemoteToken(token))) {
866
822
  res.writeHead(401, { 'Content-Type': 'application/json' });
867
823
  res.end(JSON.stringify({ error: 'unauthorized' }));
868
824
  return;
@@ -875,11 +831,11 @@ export async function startWatchWebServer(opts = {}) {
875
831
  const { query } = parseUrl(req.url || '');
876
832
  let token = bearerToken || query.token || '';
877
833
  if (!token) {
878
- const autoToken = issueLocalDirectToken(req, log);
834
+ const autoToken = await issueLocalDirectToken(req, log);
879
835
  if (autoToken)
880
836
  token = autoToken;
881
837
  }
882
- if (!token || !validateAndRenew(token, Date.now())) {
838
+ if (!token || !(await validateRemoteToken(token))) {
883
839
  res.writeHead(401, { 'Content-Type': 'application/json' });
884
840
  res.end(JSON.stringify({ error: 'unauthorized' }));
885
841
  return;
@@ -893,11 +849,11 @@ export async function startWatchWebServer(opts = {}) {
893
849
  const { query } = parseUrl(req.url || '');
894
850
  let token = bearerToken || query.token || '';
895
851
  if (!token) {
896
- const autoToken = issueLocalDirectToken(req, log);
852
+ const autoToken = await issueLocalDirectToken(req, log);
897
853
  if (autoToken)
898
854
  token = autoToken;
899
855
  }
900
- const tokenRecord = token ? getValidTokenRecord(token, Date.now()) : null;
856
+ const tokenRecord = token ? await validateRemoteToken(token) : null;
901
857
  if (!token || !tokenRecord) {
902
858
  res.writeHead(401, { 'Content-Type': 'application/json' });
903
859
  res.end(JSON.stringify({ error: 'unauthorized' }));
@@ -912,11 +868,11 @@ export async function startWatchWebServer(opts = {}) {
912
868
  const { query } = parseUrl(req.url || '');
913
869
  let token = bearerToken || query.token || '';
914
870
  if (!token) {
915
- const autoToken = issueLocalDirectToken(req, log);
871
+ const autoToken = await issueLocalDirectToken(req, log);
916
872
  if (autoToken)
917
873
  token = autoToken;
918
874
  }
919
- const tokenRecord = token ? getValidTokenRecord(token, Date.now()) : null;
875
+ const tokenRecord = token ? await validateRemoteToken(token) : null;
920
876
  if (!token || !tokenRecord) {
921
877
  res.writeHead(401, { 'Content-Type': 'application/json' });
922
878
  res.end(JSON.stringify({ error: 'unauthorized' }));
@@ -930,11 +886,11 @@ export async function startWatchWebServer(opts = {}) {
930
886
  const { query } = parseUrl(req.url || '');
931
887
  let token = bearerToken || query.token || '';
932
888
  if (!token) {
933
- const autoToken = issueLocalDirectToken(req, log);
889
+ const autoToken = await issueLocalDirectToken(req, log);
934
890
  if (autoToken)
935
891
  token = autoToken;
936
892
  }
937
- const tokenRecord = token ? getValidTokenRecord(token, Date.now()) : null;
893
+ const tokenRecord = token ? await validateRemoteToken(token) : null;
938
894
  if (!token || !tokenRecord) {
939
895
  res.writeHead(401, { 'Content-Type': 'application/json' });
940
896
  res.end(JSON.stringify({ error: 'unauthorized' }));
@@ -947,11 +903,11 @@ export async function startWatchWebServer(opts = {}) {
947
903
  }
948
904
  });
949
905
  const wss = new WebSocketServer({ noServer: true });
950
- server.on('upgrade', (req, socket, head) => {
906
+ server.on('upgrade', async (req, socket, head) => {
951
907
  const { query } = parseUrl(req.url || '');
952
908
  // 本地直连免 token;远程需 token。
953
909
  const localDirect = isLocalDirect(req);
954
- const tokenRecord = localDirect ? null : getValidTokenRecord(query.token || '', Date.now());
910
+ const tokenRecord = localDirect ? null : await validateRemoteToken(query.token || '');
955
911
  const authed = localDirect || tokenRecord !== null;
956
912
  wss.handleUpgrade(req, socket, head, (ws) => {
957
913
  if (!authed) {
@@ -963,11 +919,12 @@ export async function startWatchWebServer(opts = {}) {
963
919
  });
964
920
  });
965
921
  const { port, displaced } = await bindPort(server, opts.port ?? DEFAULT_PORT, log);
922
+ const initialPair = await remoteAuth('remote-auth.pair-code');
966
923
  return {
967
924
  url: `http://0.0.0.0:${port}`,
968
925
  port,
969
926
  displaced,
970
- pairingCode,
927
+ pairingCode: initialPair?.ok && typeof initialPair.code === 'string' ? initialPair.code : '',
971
928
  close() {
972
929
  return new Promise((resolve) => {
973
930
  // 强制断开所有 WS 客户端(graceful close 握手可能永不完成)