@walkhi/code-relax 0.1.0-beta.66 → 0.1.0-beta.68
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/bin/codex-remote.mjs +4 -2
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-harmony-release.mjs +46 -46
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/bin/set-relay-admin-password.mjs +11 -11
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/shared/app-server-events.cjs +511 -511
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-ice-policy.cjs +22 -22
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -69
- package/dist/shared/release-version.cjs +31 -31
- package/dist/src/archived-threads.mjs +22 -22
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +62 -62
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +19 -19
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-permission-mode.mjs +17 -17
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/file-opener.mjs +9 -9
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -56
- package/dist/src/platform/linux/desktop-installation.mjs +36 -36
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/linux/network-info.mjs +11 -11
- package/dist/src/platform/linux/project-locations.mjs +17 -17
- package/dist/src/platform/linux/resident-startup.mjs +52 -52
- package/dist/src/platform/linux/retained-runtime.mjs +76 -76
- package/dist/src/platform/linux/service-host.mjs +18 -18
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/macos/network-info.mjs +8 -8
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -23
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/power-inhibitor.mjs +22 -22
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
- package/dist/src/platform/windows/keep-awake.ps1 +20 -20
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-create.mjs +121 -121
- package/dist/src/project-management.mjs +1 -1
- package/dist/src/project-remove.mjs +55 -55
- package/dist/src/project-update.mjs +59 -59
- package/dist/src/relay-config-import.mjs +45 -45
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-auth.mjs +59 -59
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-login-page.mjs +7 -7
- package/dist/src/self-relay/admin-server.mjs +105 -105
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
- package/dist/src/self-relay/admin-workstations.mjs +143 -143
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connection-reporting.mjs +38 -38
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +6 -6
- package/dist/src/self-relay/device-info.mjs +27 -27
- package/dist/src/self-relay/device-store.mjs +92 -92
- package/dist/src/self-relay/lan-authorizations.mjs +71 -71
- package/dist/src/self-relay/lifecycle.mjs +278 -278
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -118
- package/dist/src/self-relay/phone-connections.mjs +55 -55
- package/dist/src/self-relay/server.mjs +1066 -1066
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +1947 -1947
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-power.mjs +58 -58
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/service-update-worker.mjs +42 -42
- package/dist/src/service-update.mjs +80 -80
- package/dist/src/shared-recovery.mjs +152 -152
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +4 -4
- package/tools/postinstall.mjs +1 -1
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { connectionInfo, validConnectionInfo } from './device-info.mjs';
|
|
5
|
-
|
|
6
|
-
export const validId = value => typeof value === 'string' && /^[\w-]{16,128}$/.test(value);
|
|
7
|
-
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
8
|
-
export function deviceStore(file, { initialize = false } = {}) {
|
|
9
|
-
let devices = new Map();
|
|
10
|
-
function validate(record) {
|
|
11
|
-
if (!validId(record.id) || !hash(record.hostHash) || !['active', 'revoked'].includes(record.status) ||
|
|
12
|
-
!Number.isSafeInteger(record.revision) || record.revision < 0 ||
|
|
13
|
-
!Array.isArray(record.clients) ||
|
|
14
|
-
record.clients.some(client => !validId(client.id) || !hash(client.hash) || !validConnectionInfo(client) ||
|
|
15
|
-
!Number.isFinite(Date.parse(client.createdAt)) || !Number.isFinite(Date.parse(client.updatedAt))) ||
|
|
16
|
-
new Set(record.clients.map(client => client.id)).size !== record.clients.length ||
|
|
17
|
-
!Number.isFinite(Date.parse(record.createdAt)) || !Number.isFinite(Date.parse(record.updatedAt)) ||
|
|
18
|
-
!validOfficialComputer(record.officialComputer)) {
|
|
19
|
-
throw new Error('Invalid relay device record');
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function write(next) {
|
|
23
|
-
if (!file) return;
|
|
24
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
25
|
-
let fd;
|
|
26
|
-
try {
|
|
27
|
-
fd = fs.openSync(temporary, 'wx', 0o600);
|
|
28
|
-
fs.writeFileSync(fd, JSON.stringify({ version: 4, devices: [...next.values()] }));
|
|
29
|
-
fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
|
|
30
|
-
fs.renameSync(temporary, file);
|
|
31
|
-
if (process.platform !== 'win32') {
|
|
32
|
-
const directory = fs.openSync(path.dirname(file), 'r');
|
|
33
|
-
try { fs.fsyncSync(directory); } finally { fs.closeSync(directory); }
|
|
34
|
-
}
|
|
35
|
-
} finally {
|
|
36
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
37
|
-
fs.rmSync(temporary, { force: true });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (file) {
|
|
41
|
-
if (!fs.existsSync(file)) {
|
|
42
|
-
if (!initialize) throw new Error('Device store missing; explicit --init-state required for first installation');
|
|
43
|
-
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); write(devices);
|
|
44
|
-
} else {
|
|
45
|
-
if (initialize) throw new Error('Device store already exists; initialization refused');
|
|
46
|
-
const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
|
-
if (![2, 3, 4].includes(saved.version) || !Array.isArray(saved.devices)) throw new Error('Invalid relay device store version');
|
|
48
|
-
for (const source of saved.devices) {
|
|
49
|
-
const record = saved.version === 2 ? {
|
|
50
|
-
...source,
|
|
51
|
-
clients: source.clientHash == null ? [] : [{
|
|
52
|
-
id: source.clientId,
|
|
53
|
-
hash: source.clientHash,
|
|
54
|
-
createdAt: source.updatedAt,
|
|
55
|
-
updatedAt: source.updatedAt,
|
|
56
|
-
}],
|
|
57
|
-
} : source;
|
|
58
|
-
delete record.clientHash; delete record.clientId;
|
|
59
|
-
validate(record);
|
|
60
|
-
if (devices.has(record.id)) throw new Error('Duplicate relay device identity');
|
|
61
|
-
devices.set(record.id, record);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
entries: () => [...devices.values()],
|
|
67
|
-
recordConnection(id, clientId, at, info) {
|
|
68
|
-
const record = devices.get(id), client = record?.clients.find(item => item.id === clientId);
|
|
69
|
-
if (!client) return;
|
|
70
|
-
const updated = connectionInfo(client, at, info);
|
|
71
|
-
this.save({ ...record, clients: record.clients.map(item => item.id === clientId ? updated : item) });
|
|
72
|
-
return updated;
|
|
73
|
-
},
|
|
74
|
-
// Synchronous single-writer commit: publish only after durable replacement.
|
|
75
|
-
save(record) {
|
|
76
|
-
validate(record);
|
|
77
|
-
const next = new Map(devices); next.set(record.id, record); write(next); devices = next;
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function validOfficialComputer(value) {
|
|
83
|
-
if (value === undefined) return true;
|
|
84
|
-
return value && typeof value === 'object' && plain(value.environmentId, 256, false) &&
|
|
85
|
-
plain(value.name, 80, true) && plain(value.hostname, 80, true) &&
|
|
86
|
-
Number.isFinite(Date.parse(value.updatedAt));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function plain(value, limit, empty) {
|
|
90
|
-
return typeof value === 'string' && (empty || value.trim().length > 0) &&
|
|
91
|
-
value.trim().length <= limit && !/[\u0000-\u001f\u007f]/.test(value);
|
|
92
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { connectionInfo, validConnectionInfo } from './device-info.mjs';
|
|
5
|
+
|
|
6
|
+
export const validId = value => typeof value === 'string' && /^[\w-]{16,128}$/.test(value);
|
|
7
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
8
|
+
export function deviceStore(file, { initialize = false } = {}) {
|
|
9
|
+
let devices = new Map();
|
|
10
|
+
function validate(record) {
|
|
11
|
+
if (!validId(record.id) || !hash(record.hostHash) || !['active', 'revoked'].includes(record.status) ||
|
|
12
|
+
!Number.isSafeInteger(record.revision) || record.revision < 0 ||
|
|
13
|
+
!Array.isArray(record.clients) ||
|
|
14
|
+
record.clients.some(client => !validId(client.id) || !hash(client.hash) || !validConnectionInfo(client) ||
|
|
15
|
+
!Number.isFinite(Date.parse(client.createdAt)) || !Number.isFinite(Date.parse(client.updatedAt))) ||
|
|
16
|
+
new Set(record.clients.map(client => client.id)).size !== record.clients.length ||
|
|
17
|
+
!Number.isFinite(Date.parse(record.createdAt)) || !Number.isFinite(Date.parse(record.updatedAt)) ||
|
|
18
|
+
!validOfficialComputer(record.officialComputer)) {
|
|
19
|
+
throw new Error('Invalid relay device record');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function write(next) {
|
|
23
|
+
if (!file) return;
|
|
24
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
25
|
+
let fd;
|
|
26
|
+
try {
|
|
27
|
+
fd = fs.openSync(temporary, 'wx', 0o600);
|
|
28
|
+
fs.writeFileSync(fd, JSON.stringify({ version: 4, devices: [...next.values()] }));
|
|
29
|
+
fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
|
|
30
|
+
fs.renameSync(temporary, file);
|
|
31
|
+
if (process.platform !== 'win32') {
|
|
32
|
+
const directory = fs.openSync(path.dirname(file), 'r');
|
|
33
|
+
try { fs.fsyncSync(directory); } finally { fs.closeSync(directory); }
|
|
34
|
+
}
|
|
35
|
+
} finally {
|
|
36
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
37
|
+
fs.rmSync(temporary, { force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (file) {
|
|
41
|
+
if (!fs.existsSync(file)) {
|
|
42
|
+
if (!initialize) throw new Error('Device store missing; explicit --init-state required for first installation');
|
|
43
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); write(devices);
|
|
44
|
+
} else {
|
|
45
|
+
if (initialize) throw new Error('Device store already exists; initialization refused');
|
|
46
|
+
const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
|
+
if (![2, 3, 4].includes(saved.version) || !Array.isArray(saved.devices)) throw new Error('Invalid relay device store version');
|
|
48
|
+
for (const source of saved.devices) {
|
|
49
|
+
const record = saved.version === 2 ? {
|
|
50
|
+
...source,
|
|
51
|
+
clients: source.clientHash == null ? [] : [{
|
|
52
|
+
id: source.clientId,
|
|
53
|
+
hash: source.clientHash,
|
|
54
|
+
createdAt: source.updatedAt,
|
|
55
|
+
updatedAt: source.updatedAt,
|
|
56
|
+
}],
|
|
57
|
+
} : source;
|
|
58
|
+
delete record.clientHash; delete record.clientId;
|
|
59
|
+
validate(record);
|
|
60
|
+
if (devices.has(record.id)) throw new Error('Duplicate relay device identity');
|
|
61
|
+
devices.set(record.id, record);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
entries: () => [...devices.values()],
|
|
67
|
+
recordConnection(id, clientId, at, info) {
|
|
68
|
+
const record = devices.get(id), client = record?.clients.find(item => item.id === clientId);
|
|
69
|
+
if (!client) return;
|
|
70
|
+
const updated = connectionInfo(client, at, info);
|
|
71
|
+
this.save({ ...record, clients: record.clients.map(item => item.id === clientId ? updated : item) });
|
|
72
|
+
return updated;
|
|
73
|
+
},
|
|
74
|
+
// Synchronous single-writer commit: publish only after durable replacement.
|
|
75
|
+
save(record) {
|
|
76
|
+
validate(record);
|
|
77
|
+
const next = new Map(devices); next.set(record.id, record); write(next); devices = next;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validOfficialComputer(value) {
|
|
83
|
+
if (value === undefined) return true;
|
|
84
|
+
return value && typeof value === 'object' && plain(value.environmentId, 256, false) &&
|
|
85
|
+
plain(value.name, 80, true) && plain(value.hostname, 80, true) &&
|
|
86
|
+
Number.isFinite(Date.parse(value.updatedAt));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function plain(value, limit, empty) {
|
|
90
|
+
return typeof value === 'string' && (empty || value.trim().length > 0) &&
|
|
91
|
+
value.trim().length <= limit && !/[\u0000-\u001f\u007f]/.test(value);
|
|
92
|
+
}
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { EventEmitter } from 'node:events';
|
|
5
|
-
import { digest, matches } from './wire.mjs';
|
|
6
|
-
import { validId } from './device-store.mjs';
|
|
7
|
-
import { deviceInfo } from './device-info.mjs';
|
|
8
|
-
|
|
9
|
-
const recordPath = stateRoot => path.join(stateRoot, 'lan-device-authorizations.json');
|
|
10
|
-
const validHash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
11
|
-
const authorizationChanges = new EventEmitter();
|
|
12
|
-
const deviceConnections = new EventEmitter();
|
|
13
|
-
const authorizationKey = stateRoot => {
|
|
14
|
-
const resolved = path.resolve(stateRoot);
|
|
15
|
-
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function onLanAuthorizationsChanged(stateRoot, listener) {
|
|
19
|
-
const key = authorizationKey(stateRoot);
|
|
20
|
-
authorizationChanges.on(key, listener);
|
|
21
|
-
return () => authorizationChanges.off(key, listener);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function saveLanAuthorizations(stateRoot, hostId, clients, revision) {
|
|
25
|
-
if (!validId(hostId) || !Array.isArray(clients) ||
|
|
26
|
-
clients.some(client => !validId(client.id) || !validHash(client.hash)) ||
|
|
27
|
-
!Number.isSafeInteger(revision) || revision < 0) throw new Error('Invalid LAN device authorization state');
|
|
28
|
-
const file = recordPath(stateRoot), temporary = `${file}.${randomUUID()}.tmp`;
|
|
29
|
-
const previous = readAuthorizations(stateRoot);
|
|
30
|
-
const known = new Map(previous?.hostId === hostId ? previous.clients.map(client => [client.id, client]) : []);
|
|
31
|
-
const record = { version: 1, hostId, revision, clients: clients.map(client => ({ id: client.id, hash: client.hash,
|
|
32
|
-
...(known.get(client.id)?.lastLanConnectedAt ? { lastLanConnectedAt: known.get(client.id).lastLanConnectedAt,
|
|
33
|
-
lanInfo: known.get(client.id).lanInfo } : {}), lastConnectedAt: client.lastConnectedAt || null })) };
|
|
34
|
-
try {
|
|
35
|
-
fs.writeFileSync(temporary, JSON.stringify(record), { mode: 0o600, flag: 'wx' });
|
|
36
|
-
fs.renameSync(temporary, file);
|
|
37
|
-
} finally { fs.rmSync(temporary, { force: true }); }
|
|
38
|
-
authorizationChanges.emit(authorizationKey(stateRoot));
|
|
39
|
-
}
|
|
40
|
-
function readAuthorizations(stateRoot) {
|
|
41
|
-
try { return JSON.parse(fs.readFileSync(recordPath(stateRoot), 'utf8')); }
|
|
42
|
-
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
43
|
-
}
|
|
44
|
-
export function recordLanConnection(stateRoot, clientId, info) {
|
|
45
|
-
const record = readAuthorizations(stateRoot), client = record?.clients.find(item => item.id === clientId);
|
|
46
|
-
if (!client) return;
|
|
47
|
-
client.lastLanConnectedAt = new Date().toISOString(); client.lanInfo = { ...client.lanInfo, ...deviceInfo(info) };
|
|
48
|
-
const file = recordPath(stateRoot), temporary = `${file}.${randomUUID()}.tmp`;
|
|
49
|
-
try {
|
|
50
|
-
fs.writeFileSync(temporary, JSON.stringify(record), { mode: 0o600, flag: 'wx' }); fs.renameSync(temporary, file);
|
|
51
|
-
} finally { fs.rmSync(temporary, { force: true }); }
|
|
52
|
-
deviceConnections.emit(authorizationKey(stateRoot), { clientId, at: client.lastLanConnectedAt, ...client.lanInfo });
|
|
53
|
-
}
|
|
54
|
-
export function pendingLanConnections(stateRoot) {
|
|
55
|
-
return (readAuthorizations(stateRoot)?.clients || []).filter(client => client.lastLanConnectedAt &&
|
|
56
|
-
(!client.lastConnectedAt || client.lastLanConnectedAt > client.lastConnectedAt))
|
|
57
|
-
.map(client => ({ clientId: client.id, at: client.lastLanConnectedAt, ...client.lanInfo }));
|
|
58
|
-
}
|
|
59
|
-
export function onLanDeviceConnected(stateRoot, listener) {
|
|
60
|
-
const key = authorizationKey(stateRoot); deviceConnections.on(key, listener);
|
|
61
|
-
return () => deviceConnections.off(key, listener);
|
|
62
|
-
}
|
|
63
|
-
export function authorizeLanDevice(stateRoot, clientId, credential) {
|
|
64
|
-
if (!validId(clientId)) return false;
|
|
65
|
-
let record;
|
|
66
|
-
try { record = JSON.parse(fs.readFileSync(recordPath(stateRoot), 'utf8')); }
|
|
67
|
-
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
68
|
-
if (record?.version !== 1 || !validId(record.hostId) || !Array.isArray(record.clients)) return false;
|
|
69
|
-
const client = record.clients.find(item => item?.id === clientId && validHash(item.hash));
|
|
70
|
-
return Boolean(client && matches(credential, client.hash));
|
|
71
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { digest, matches } from './wire.mjs';
|
|
6
|
+
import { validId } from './device-store.mjs';
|
|
7
|
+
import { deviceInfo } from './device-info.mjs';
|
|
8
|
+
|
|
9
|
+
const recordPath = stateRoot => path.join(stateRoot, 'lan-device-authorizations.json');
|
|
10
|
+
const validHash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
11
|
+
const authorizationChanges = new EventEmitter();
|
|
12
|
+
const deviceConnections = new EventEmitter();
|
|
13
|
+
const authorizationKey = stateRoot => {
|
|
14
|
+
const resolved = path.resolve(stateRoot);
|
|
15
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function onLanAuthorizationsChanged(stateRoot, listener) {
|
|
19
|
+
const key = authorizationKey(stateRoot);
|
|
20
|
+
authorizationChanges.on(key, listener);
|
|
21
|
+
return () => authorizationChanges.off(key, listener);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveLanAuthorizations(stateRoot, hostId, clients, revision) {
|
|
25
|
+
if (!validId(hostId) || !Array.isArray(clients) ||
|
|
26
|
+
clients.some(client => !validId(client.id) || !validHash(client.hash)) ||
|
|
27
|
+
!Number.isSafeInteger(revision) || revision < 0) throw new Error('Invalid LAN device authorization state');
|
|
28
|
+
const file = recordPath(stateRoot), temporary = `${file}.${randomUUID()}.tmp`;
|
|
29
|
+
const previous = readAuthorizations(stateRoot);
|
|
30
|
+
const known = new Map(previous?.hostId === hostId ? previous.clients.map(client => [client.id, client]) : []);
|
|
31
|
+
const record = { version: 1, hostId, revision, clients: clients.map(client => ({ id: client.id, hash: client.hash,
|
|
32
|
+
...(known.get(client.id)?.lastLanConnectedAt ? { lastLanConnectedAt: known.get(client.id).lastLanConnectedAt,
|
|
33
|
+
lanInfo: known.get(client.id).lanInfo } : {}), lastConnectedAt: client.lastConnectedAt || null })) };
|
|
34
|
+
try {
|
|
35
|
+
fs.writeFileSync(temporary, JSON.stringify(record), { mode: 0o600, flag: 'wx' });
|
|
36
|
+
fs.renameSync(temporary, file);
|
|
37
|
+
} finally { fs.rmSync(temporary, { force: true }); }
|
|
38
|
+
authorizationChanges.emit(authorizationKey(stateRoot));
|
|
39
|
+
}
|
|
40
|
+
function readAuthorizations(stateRoot) {
|
|
41
|
+
try { return JSON.parse(fs.readFileSync(recordPath(stateRoot), 'utf8')); }
|
|
42
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
43
|
+
}
|
|
44
|
+
export function recordLanConnection(stateRoot, clientId, info) {
|
|
45
|
+
const record = readAuthorizations(stateRoot), client = record?.clients.find(item => item.id === clientId);
|
|
46
|
+
if (!client) return;
|
|
47
|
+
client.lastLanConnectedAt = new Date().toISOString(); client.lanInfo = { ...client.lanInfo, ...deviceInfo(info) };
|
|
48
|
+
const file = recordPath(stateRoot), temporary = `${file}.${randomUUID()}.tmp`;
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(temporary, JSON.stringify(record), { mode: 0o600, flag: 'wx' }); fs.renameSync(temporary, file);
|
|
51
|
+
} finally { fs.rmSync(temporary, { force: true }); }
|
|
52
|
+
deviceConnections.emit(authorizationKey(stateRoot), { clientId, at: client.lastLanConnectedAt, ...client.lanInfo });
|
|
53
|
+
}
|
|
54
|
+
export function pendingLanConnections(stateRoot) {
|
|
55
|
+
return (readAuthorizations(stateRoot)?.clients || []).filter(client => client.lastLanConnectedAt &&
|
|
56
|
+
(!client.lastConnectedAt || client.lastLanConnectedAt > client.lastConnectedAt))
|
|
57
|
+
.map(client => ({ clientId: client.id, at: client.lastLanConnectedAt, ...client.lanInfo }));
|
|
58
|
+
}
|
|
59
|
+
export function onLanDeviceConnected(stateRoot, listener) {
|
|
60
|
+
const key = authorizationKey(stateRoot); deviceConnections.on(key, listener);
|
|
61
|
+
return () => deviceConnections.off(key, listener);
|
|
62
|
+
}
|
|
63
|
+
export function authorizeLanDevice(stateRoot, clientId, credential) {
|
|
64
|
+
if (!validId(clientId)) return false;
|
|
65
|
+
let record;
|
|
66
|
+
try { record = JSON.parse(fs.readFileSync(recordPath(stateRoot), 'utf8')); }
|
|
67
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
68
|
+
if (record?.version !== 1 || !validId(record.hostId) || !Array.isArray(record.clients)) return false;
|
|
69
|
+
const client = record.clients.find(item => item?.id === clientId && validHash(item.hash));
|
|
70
|
+
return Boolean(client && matches(credential, client.hash));
|
|
71
|
+
}
|