@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.
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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,
|