@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.
Files changed (120) hide show
  1. package/dist/bin/codex-remote.mjs +4 -2
  2. package/dist/bin/manage-relay-admin.mjs +20 -20
  3. package/dist/bin/migrate-relay-db.mjs +109 -109
  4. package/dist/bin/publish-harmony-release.mjs +46 -46
  5. package/dist/bin/self-relay-admin.mjs +8 -8
  6. package/dist/bin/self-relay-demo.mjs +33 -33
  7. package/dist/bin/self-relay-server.mjs +66 -66
  8. package/dist/bin/set-relay-admin-password.mjs +11 -11
  9. package/dist/build-platform.json +1 -1
  10. package/dist/get-lan-networks.ps1 +40 -40
  11. package/dist/shared/app-server-events.cjs +511 -511
  12. package/dist/shared/p2p-data-channel.cjs +72 -72
  13. package/dist/shared/p2p-diagnostics.cjs +28 -28
  14. package/dist/shared/p2p-ice-policy.cjs +22 -22
  15. package/dist/shared/plan-pricing.cjs +32 -32
  16. package/dist/shared/question-attention.cjs +69 -69
  17. package/dist/shared/release-version.cjs +31 -31
  18. package/dist/src/archived-threads.mjs +22 -22
  19. package/dist/src/background-process.mjs +18 -18
  20. package/dist/src/code-browser.mjs +100 -100
  21. package/dist/src/desktop-launcher.mjs +62 -62
  22. package/dist/src/dots-task-files.mjs +52 -52
  23. package/dist/src/execution-health.mjs +26 -26
  24. package/dist/src/message-images.mjs +9 -9
  25. package/dist/src/plan-pricing.mjs +47 -47
  26. package/dist/src/platform/codex-runtime.mjs +3 -3
  27. package/dist/src/platform/credential-store.mjs +19 -19
  28. package/dist/src/platform/desktop-host.mjs +3 -3
  29. package/dist/src/platform/desktop-permission-mode.mjs +17 -17
  30. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  31. package/dist/src/platform/file-opener.mjs +9 -9
  32. package/dist/src/platform/linux/README.md +3 -3
  33. package/dist/src/platform/linux/codex-runtime.mjs +56 -56
  34. package/dist/src/platform/linux/desktop-installation.mjs +36 -36
  35. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
  36. package/dist/src/platform/linux/network-info.mjs +11 -11
  37. package/dist/src/platform/linux/project-locations.mjs +17 -17
  38. package/dist/src/platform/linux/resident-startup.mjs +52 -52
  39. package/dist/src/platform/linux/retained-runtime.mjs +76 -76
  40. package/dist/src/platform/linux/service-host.mjs +18 -18
  41. package/dist/src/platform/macos/README.md +3 -3
  42. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
  43. package/dist/src/platform/macos/network-info.mjs +8 -8
  44. package/dist/src/platform/macos/project-locations.mjs +8 -8
  45. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  46. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  47. package/dist/src/platform/macos/service-host.mjs +18 -18
  48. package/dist/src/platform/macos/system.mjs +29 -29
  49. package/dist/src/platform/network-info.mjs +3 -3
  50. package/dist/src/platform/path-identity.mjs +5 -5
  51. package/dist/src/platform/posix/network-candidates.mjs +23 -23
  52. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  53. package/dist/src/platform/power-inhibitor.mjs +22 -22
  54. package/dist/src/platform/project-locations.mjs +3 -3
  55. package/dist/src/platform/runtime-paths.mjs +8 -8
  56. package/dist/src/platform/select.mjs +5 -5
  57. package/dist/src/platform/service-host.mjs +3 -3
  58. package/dist/src/platform/user-environment.mjs +3 -3
  59. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  60. package/dist/src/platform/windows/background-process.mjs +30 -30
  61. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  62. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  63. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
  64. package/dist/src/platform/windows/keep-awake.ps1 +20 -20
  65. package/dist/src/platform/windows/network-info.mjs +34 -34
  66. package/dist/src/platform/windows/resident-start.js +20 -20
  67. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  68. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  69. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  70. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  71. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  72. package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
  73. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  74. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  75. package/dist/src/project-create.mjs +121 -121
  76. package/dist/src/project-management.mjs +1 -1
  77. package/dist/src/project-remove.mjs +55 -55
  78. package/dist/src/project-update.mjs +59 -59
  79. package/dist/src/relay-config-import.mjs +45 -45
  80. package/dist/src/self-relay/account-connections.mjs +52 -52
  81. package/dist/src/self-relay/admin-auth.mjs +59 -59
  82. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  83. package/dist/src/self-relay/admin-login-page.mjs +7 -7
  84. package/dist/src/self-relay/admin-server.mjs +105 -105
  85. package/dist/src/self-relay/admin-state.mjs +127 -127
  86. package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
  87. package/dist/src/self-relay/admin-workstations.mjs +143 -143
  88. package/dist/src/self-relay/android-releases.mjs +61 -61
  89. package/dist/src/self-relay/client.mjs +61 -61
  90. package/dist/src/self-relay/connection-reporting.mjs +38 -38
  91. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  92. package/dist/src/self-relay/demo.mjs +6 -6
  93. package/dist/src/self-relay/device-info.mjs +27 -27
  94. package/dist/src/self-relay/device-store.mjs +92 -92
  95. package/dist/src/self-relay/lan-authorizations.mjs +71 -71
  96. package/dist/src/self-relay/lifecycle.mjs +278 -278
  97. package/dist/src/self-relay/long-pairing.mjs +40 -40
  98. package/dist/src/self-relay/mysql-state.mjs +118 -118
  99. package/dist/src/self-relay/phone-connections.mjs +55 -55
  100. package/dist/src/self-relay/server.mjs +1066 -1066
  101. package/dist/src/self-relay/usage-daily.mjs +55 -55
  102. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  103. package/dist/src/self-relay/wire.mjs +36 -36
  104. package/dist/src/server.mjs +1947 -1947
  105. package/dist/src/service-control.mjs +82 -82
  106. package/dist/src/service-health.mjs +33 -33
  107. package/dist/src/service-power.mjs +58 -58
  108. package/dist/src/service-restart.mjs +81 -81
  109. package/dist/src/service-update-worker.mjs +42 -42
  110. package/dist/src/service-update.mjs +80 -80
  111. package/dist/src/shared-recovery.mjs +152 -152
  112. package/dist/src/skill-install.mjs +51 -51
  113. package/dist/src/thread-title-generation.mjs +150 -150
  114. package/dist/src/turn-model-history.mjs +47 -47
  115. package/dist/tools/rtc-route-helper.ps1 +99 -99
  116. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  117. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  118. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  119. package/package.json +4 -4
  120. 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
+ }