@walkhi/code-relax 0.1.0-beta.3 → 0.1.0-beta.5

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 CHANGED
@@ -80,4 +80,4 @@ npm uninstall -g @walkhi/code-relax
80
80
 
81
81
  ## 发布边界
82
82
 
83
- registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.2`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。待发布的 `0.1.0-beta.3` 将 WMI 后的第二层 PowerShell broker 改为 Node worker,并加入 `%LOCALAPPDATA%\CodexRemote\install.log` 持久安装日志;本机启动交接和 npm 打包已验证,发布后仍需在原故障电脑复核。详细规则见 [npm 分发](../docs/npm-distribution.md)。
83
+ registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.5`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。该版让 WMI 所有的 Node broker 在目标进程期间保留隐藏控制台,避免 Codex 调用工具时反复弹出控制台窗口;同时沿用官方中继自助登记和持久安装日志,通信协议仍为 7。详细规则见 [npm 分发](../docs/npm-distribution.md)。
@@ -17,7 +17,6 @@ try {
17
17
  const descriptors = (spec.logs || []).map(file => fs.openSync(file, 'a'));
18
18
  try {
19
19
  const child = spawn(spec.executable, spec.args || [], {
20
- detached: true,
21
20
  windowsHide: true,
22
21
  env: spec.environment,
23
22
  stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'],
@@ -26,7 +25,8 @@ try {
26
25
  child.once('spawn', resolve);
27
26
  child.once('error', reject);
28
27
  });
29
- child.unref();
28
+ // Keep this WMI-owned broker alive with the target so Codex tools inherit
29
+ // its hidden console instead of creating a visible console per command.
30
30
  result.pid = child.pid;
31
31
  diagnostic(`target started (PID ${child.pid})`);
32
32
  } finally {
@@ -1,9 +1,9 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { execFile } from 'node:child_process';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
- import { startBackground, processAlive } from '../background-process.mjs';
6
- import { promisify } from 'node:util';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { startBackground, processAlive } from '../background-process.mjs';
6
+ import { promisify } from 'node:util';
7
7
  import { randomBytes, randomUUID } from 'node:crypto';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { hostname } from 'node:os';
@@ -16,45 +16,49 @@ import { connectBridgeRelay } from './connector.mjs';
16
16
  import { validProbeIceServers } from './p2p-probe.mjs';
17
17
  import { relayAddress, validSecret } from './wire.mjs';
18
18
  import { saveLanAuthorizations } from './lan-authorizations.mjs';
19
- import { lanNetworks } from '../platform/windows/network-info.mjs';
20
-
21
- const exec = promisify(execFile);
22
- const script = fileURLToPath(import.meta.url);
23
- const recordPath = root => path.join(root, 'self-relay-session.json');
24
-
25
- async function loadConfig(stateRoot) {
26
- const config = readRecord(path.join(stateRoot, 'self-relay-config.json')) || {};
19
+ import { lanNetworks } from '../platform/windows/network-info.mjs';
20
+
21
+ const exec = promisify(execFile);
22
+ const script = fileURLToPath(import.meta.url);
23
+ const recordPath = root => path.join(root, 'self-relay-session.json');
24
+ const officialConfig = { url: 'wss://api.uulife.site/relay' };
25
+
26
+ async function loadConfig(stateRoot) {
27
+ const configPath = path.join(stateRoot, 'self-relay-config.json');
28
+ const stored = readRecord(configPath) || {};
29
+ const config = { ...officialConfig, ...stored };
30
+ if (!stored.url) writeRecord(configPath, config);
27
31
  if (config.p2pIceServers !== undefined && !validProbeIceServers(config.p2pIceServers)) throw new Error('p2pIceServers 只接受最多 4 个 stun:主机:端口地址');
28
32
  if (config.p2pWindowsRouteHelper !== undefined && typeof config.p2pWindowsRouteHelper !== 'boolean') throw new Error('p2pWindowsRouteHelper 必须是布尔值');
29
33
  if (config.p2pWindowsRouteHelper && !/^[A-Za-z0-9_-]{32,128}$/.test(config.p2pWindowsRouteHelperToken || '')) throw new Error('p2pWindowsRouteHelperToken 无效');
30
34
  const url = process.env.RELAY_URL || config.url;
31
- if (!url) throw new Error('请先配置 self-relay-config.json 的 url 和 hostKeyFile(或 sshHost、sshKeyPath),详见自建中继文档。');
35
+ if (!url) throw new Error('请先配置 self-relay-config.json 的 url 和 hostKeyFile(或 sshHost、sshKeyPath),详见自建中继文档。');
32
36
  relayAddress(url);
33
37
  const resolved = relayAddress(url);
34
38
  const pairingUrl = config.pairingUrl || `${resolved.startsWith('wss:') ? 'https:' : 'http:'}//${new URL(resolved).host}/pair`;
35
39
  const pairing = new URL(pairingUrl);
36
40
  if (!['https:', 'http:'].includes(pairing.protocol) || pairing.pathname !== '/pair' || pairing.search || pairing.hash || pairing.username || pairing.password ||
37
41
  (pairing.protocol === 'http:' && pairing.hostname !== '127.0.0.1')) throw new Error('self-relay-config.json 的 pairingUrl 必须是 HTTPS /pair 地址。');
38
- return { ...config, url: resolved, pairingUrl: pairing.href };
39
- }
40
- async function registrationKey(config) {
41
- const hostKeyFile = process.env.RELAY_HOST_KEY_FILE || config.hostKeyFile;
42
- let hostKey;
43
- if (hostKeyFile) hostKey = fs.readFileSync(hostKeyFile, 'utf8').trim();
44
- else {
45
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(config.sshHost || '') ||
46
- !/^\/[a-zA-Z0-9/_.-]+$/.test(config.sshKeyPath || '')) throw new Error('中继注册凭据配置不完整或无效。');
47
- try {
48
- const result = await exec('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', config.sshHost,
49
- `sudo -n cat ${config.sshKeyPath}`], { encoding: 'utf8', timeout: 15000, windowsHide: true });
50
- hostKey = result.stdout.trim();
51
- } catch { throw new Error('无法通过已配置 SSH 读取中继注册凭据。'); }
52
- }
53
- if (!validSecret(hostKey)) throw new Error('中继注册凭据格式无效。');
54
- return hostKey;
55
- }
56
-
57
- export async function selfRelay(command, stateRoot, requiredMode = '') {
42
+ return { ...config, url: resolved, pairingUrl: pairing.href };
43
+ }
44
+ async function registrationKey(config) {
45
+ if (config.url === officialConfig.url) return undefined;
46
+ const hostKeyFile = process.env.RELAY_HOST_KEY_FILE || config.hostKeyFile;
47
+ let hostKey;
48
+ if (hostKeyFile) hostKey = fs.readFileSync(hostKeyFile, 'utf8').trim();
49
+ else {
50
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(config.sshHost || '') ||
51
+ !/^\/[a-zA-Z0-9/_.-]+$/.test(config.sshKeyPath || '')) throw new Error('中继注册凭据配置不完整或无效。');
52
+ try {
53
+ const result = await exec('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', config.sshHost,
54
+ `sudo -n cat ${config.sshKeyPath}`], { encoding: 'utf8', timeout: 15000, windowsHide: true });
55
+ hostKey = result.stdout.trim();
56
+ } catch { throw new Error('无法通过已配置 SSH 读取中继注册凭据。'); }
57
+ }
58
+ if (!validSecret(hostKey)) throw new Error('中继注册凭据格式无效。');
59
+ return hostKey;
60
+ }
61
+ export async function selfRelay(command, stateRoot, requiredMode = '') {
58
62
  const previous = readRecord(recordPath(stateRoot));
59
63
  let current;
60
64
  if (previous) {
@@ -134,12 +138,12 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
134
138
  fs.rmSync(legacyPath);
135
139
  }
136
140
  let identity = identities.servers[config.url];
137
- if (!identity) {
138
- identity = { id: randomUUID(), secret: randomBytes(32).toString('base64url'), registered: false };
139
- identities.servers[config.url] = identity; await credentialStore('save', identities);
140
- }
141
- const hostKey = identity.registered ? undefined : await registrationKey(config);
142
- let bridgeError = '';
141
+ if (!identity) {
142
+ identity = { id: randomUUID(), secret: randomBytes(32).toString('base64url'), registered: false };
143
+ identities.servers[config.url] = identity; await credentialStore('save', identities);
144
+ }
145
+ const hostKey = identity.registered ? undefined : await registrationKey(config);
146
+ let bridgeError = '';
143
147
  async function refreshHealth() {
144
148
  try { health = await serviceHealth(session()) || { transportMode: health.transportMode, status: 'offline', desktopConnected: false }; bridgeError = ''; }
145
149
  catch (error) { bridgeError = error.message; health = { transportMode: health.transportMode, status: 'offline', desktopConnected: false }; }
@@ -199,7 +203,7 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
199
203
  await refreshHealth();
200
204
  const lanCandidates = await currentLanCandidates();
201
205
  connector?.events.removeAllListeners(); connector?.close();
202
- const current = connector = connectBridgeRelay({ relayUrl: config.url, hostKey: identity.registered ? undefined : hostKey, identity,
206
+ const current = connector = connectBridgeRelay({ relayUrl: config.url, hostKey: identity.registered ? undefined : hostKey, identity,
203
207
  bridgeUrl: `ws://127.0.0.1:${bridge?.port || 45831}/api/socket`, bridgeToken: bridge?.accessUrl ? new URL(bridge.accessUrl).searchParams.get('token') : 'unavailable',
204
208
  bridgeConnection: () => { const latest = session(); return { url: `ws://127.0.0.1:${latest.port}/api/socket`, token: new URL(latest.accessUrl).searchParams.get('token') }; },
205
209
  hostName: hostname(), lanCandidates, p2pProbe: config.p2pProbe === true, iceServers: config.p2pIceServers || [],
@@ -198,11 +198,13 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
198
198
  hostName = normalizeHostName(message.hostName);
199
199
  }
200
200
  catch { return fail(1008, 'Invalid LAN candidates'); }
201
- room = durable ? rooms.get(message.deviceId) : undefined;
202
- if (room && (room.status !== 'active' || !matches(message.deviceSecret, room.hostHash))) return fail();
203
- if (!room) {
204
- if (!matches(message.key, hostHash)) return fail();
205
- if (rooms.size >= 256) return fail(4429, 'Device limit reached');
201
+ room = durable ? rooms.get(message.deviceId) : undefined;
202
+ if (room && (room.status !== 'active' || !matches(message.deviceSecret, room.hostHash))) return fail();
203
+ if (!room) {
204
+ // Durable computers prove future ownership with their generated device secret.
205
+ // The legacy deployment key remains accepted, but is no longer required for first registration.
206
+ if (!durable && !matches(message.key, hostHash)) return fail();
207
+ if (rooms.size >= 256) return fail(4429, 'Device limit reached');
206
208
  const now = new Date().toISOString();
207
209
  room = { id: durable ? message.deviceId : randomUUID(), durable,
208
210
  revision: 0, status: 'active', clients: [], connections: new Map(), observers: new Map(), createdAt: now, updatedAt: now,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "scripts": {