@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,40 +1,40 @@
1
- import fs from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
3
- import { validId } from './device-store.mjs';
4
- import { digest, matches, secret } from './wire.mjs';
5
-
6
- // Operator-owned file, separate from device state. Only hashes are retained here.
7
- export function longPairing(file) {
8
- function read() {
9
- if (!file || !fs.existsSync(file)) return [];
10
- const data = JSON.parse(fs.readFileSync(file, 'utf8'));
11
- if (data.version !== 1 || !Array.isArray(data.invitations) || data.invitations.some(item =>
12
- !validId(item.id) || !validId(item.room) || !/^[a-f0-9]{64}$/.test(item.hash) ||
13
- !Number.isSafeInteger(item.expires))) throw new Error('Invalid long pairing invitations');
14
- return data.invitations;
15
- }
16
- function write(invitations) {
17
- if (!file) throw new Error('Long pairing file is not configured');
18
- const temporary = `${file}.${randomUUID()}.tmp`;
19
- try {
20
- fs.writeFileSync(temporary, JSON.stringify({ version: 1, invitations }), { mode: 0o600, flag: 'wx' });
21
- fs.renameSync(temporary, file);
22
- } finally { fs.rmSync(temporary, { force: true }); }
23
- }
24
- return {
25
- list: () => read().map(({ hash, ...item }) => item),
26
- find: code => read().find(item => Date.now() < item.expires && matches(code, item.hash)),
27
- create(room, expires) {
28
- if (!validId(room) || !Number.isSafeInteger(expires) || expires <= Date.now()) throw new Error('Invalid invitation room or expiry');
29
- const invitations = read(), code = secret();
30
- const item = { id: randomUUID(), room, expires, hash: digest(code) };
31
- write([...invitations, item]);
32
- return { id: item.id, room, expires, code };
33
- },
34
- revoke(id) {
35
- const invitations = read();
36
- if (!invitations.some(item => item.id === id)) throw new Error('Invitation not found');
37
- write(invitations.filter(item => item.id !== id));
38
- },
39
- };
40
- }
1
+ import fs from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { validId } from './device-store.mjs';
4
+ import { digest, matches, secret } from './wire.mjs';
5
+
6
+ // Operator-owned file, separate from device state. Only hashes are retained here.
7
+ export function longPairing(file) {
8
+ function read() {
9
+ if (!file || !fs.existsSync(file)) return [];
10
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
11
+ if (data.version !== 1 || !Array.isArray(data.invitations) || data.invitations.some(item =>
12
+ !validId(item.id) || !validId(item.room) || !/^[a-f0-9]{64}$/.test(item.hash) ||
13
+ !Number.isSafeInteger(item.expires))) throw new Error('Invalid long pairing invitations');
14
+ return data.invitations;
15
+ }
16
+ function write(invitations) {
17
+ if (!file) throw new Error('Long pairing file is not configured');
18
+ const temporary = `${file}.${randomUUID()}.tmp`;
19
+ try {
20
+ fs.writeFileSync(temporary, JSON.stringify({ version: 1, invitations }), { mode: 0o600, flag: 'wx' });
21
+ fs.renameSync(temporary, file);
22
+ } finally { fs.rmSync(temporary, { force: true }); }
23
+ }
24
+ return {
25
+ list: () => read().map(({ hash, ...item }) => item),
26
+ find: code => read().find(item => Date.now() < item.expires && matches(code, item.hash)),
27
+ create(room, expires) {
28
+ if (!validId(room) || !Number.isSafeInteger(expires) || expires <= Date.now()) throw new Error('Invalid invitation room or expiry');
29
+ const invitations = read(), code = secret();
30
+ const item = { id: randomUUID(), room, expires, hash: digest(code) };
31
+ write([...invitations, item]);
32
+ return { id: item.id, room, expires, code };
33
+ },
34
+ revoke(id) {
35
+ const invitations = read();
36
+ if (!invitations.some(item => item.id === id)) throw new Error('Invitation not found');
37
+ write(invitations.filter(item => item.id !== id));
38
+ },
39
+ };
40
+ }
@@ -1,118 +1,118 @@
1
- import fs from 'node:fs';
2
- import { createHash } from 'node:crypto';
3
- import { fileURLToPath } from 'node:url';
4
- import mysql from 'mysql2/promise';
5
- import { deviceStore } from './device-store.mjs';
6
- import { relayAdminState } from './admin-state.mjs';
7
- import { relaySchemaVersions } from '../../database/versions.mjs';
8
- import { connectionInfo } from './device-info.mjs';
9
-
10
- const sqlTime = value => value.replace('T', ' ').replace(/Z$/, '');
11
- const isoTime = value => value.replace(' ', 'T') + 'Z';
12
-
13
- export async function openRelayMysql(configFile, initialMaxRelaySessions) {
14
- const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
15
- if (!config.database || !config.user || !config.password || !(config.socketPath || config.host)) {
16
- throw new Error('Incomplete relay MySQL configuration');
17
- }
18
- const pool = mysql.createPool({ ...config, waitForConnections: true, connectionLimit: 4,
19
- namedPlaceholders: false, dateStrings: true, timezone: 'Z', multipleStatements: false });
20
- try {
21
- const [versions] = await pool.query('SELECT version, sha256 FROM schema_migrations');
22
- const knownVersions = relaySchemaVersions;
23
- if (!versions.some(row => row.version === knownVersions[0]) || versions.length > knownVersions.length) throw new Error('Unexpected relay database schema');
24
- for (const row of versions) {
25
- if (!knownVersions.includes(row.version)) throw new Error('Unexpected relay database schema');
26
- const source = fs.readFileSync(fileURLToPath(new URL(`../../database/${row.version}.sql`, import.meta.url)));
27
- if (row.sha256 !== createHash('sha256').update(source).digest('hex')) throw new Error(`Unexpected relay database schema: ${row.version}`);
28
- }
29
- const [computers] = await pool.query('SELECT id, host_hash, status, revision, created_at, updated_at FROM computers');
30
- const [clients] = await pool.query('SELECT computer_id, client_id, credential_hash, created_at, updated_at, last_connected_at, platform, device_name FROM mobile_authorizations ORDER BY created_at');
31
- const [official] = await pool.query('SELECT computer_id, environment_id, name, hostname, updated_at FROM official_computers');
32
- const records = new Map(computers.map(row => [row.id, { id: row.id, hostHash: row.host_hash, status: row.status,
33
- revision: Number(row.revision), createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at), clients: [] }]));
34
- for (const row of clients) {
35
- const record = records.get(row.computer_id);
36
- if (!record) throw new Error('Orphan mobile authorization');
37
- record.clients.push({ id: row.client_id, hash: row.credential_hash,
38
- createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at),
39
- ...(row.last_connected_at ? { lastConnectedAt: isoTime(row.last_connected_at) } : {}),
40
- ...(row.platform ? { platform: row.platform } : {}), ...(row.device_name ? { deviceName: row.device_name } : {}) });
41
- }
42
- for (const row of official) {
43
- const record = records.get(row.computer_id);
44
- if (!record) throw new Error('Orphan official computer');
45
- record.officialComputer = { environmentId: row.environment_id, name: row.name,
46
- hostname: row.hostname, updatedAt: isoTime(row.updated_at) };
47
- }
48
- const validator = deviceStore();
49
- for (const record of records.values()) validator.save(record);
50
-
51
- const [settings] = await pool.query('SELECT max_relay_sessions FROM relay_settings WHERE id = 1');
52
- if (settings.length !== 1) throw new Error('Missing relay settings');
53
- const [rows] = await pool.query('SELECT id, title, message, status, created_at, updated_at, published_at FROM announcements ORDER BY created_at DESC');
54
- const announcements = rows.map(row => ({ id: row.id, title: row.title, message: row.message, status: row.status,
55
- createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at),
56
- publishedAt: row.published_at ? isoTime(row.published_at) : null }));
57
- const active = announcements.find(item => item.status === 'active');
58
- const initial = { version: 3, maxRelaySessions: settings[0].max_relay_sessions, announcements,
59
- announcement: active ? { id: active.id, title: active.title, message: active.message, publishedAt: active.publishedAt } : null };
60
- const admin = relayAdminState(undefined, initialMaxRelaySessions, { initial, persist: async next => {
61
- const connection = await pool.getConnection();
62
- try {
63
- await connection.beginTransaction();
64
- await connection.execute('UPDATE relay_settings SET max_relay_sessions = ?, updated_at = UTC_TIMESTAMP(3) WHERE id = 1', [next.maxRelaySessions]);
65
- await connection.query('DELETE FROM announcements');
66
- for (const item of next.announcements) await connection.execute(
67
- 'INSERT INTO announcements (id,title,message,status,created_at,updated_at,published_at) VALUES (?,?,?,?,?,?,?)',
68
- [item.id, item.title, item.message, item.status, sqlTime(item.createdAt), sqlTime(item.updatedAt),
69
- item.publishedAt ? sqlTime(item.publishedAt) : null]);
70
- await connection.commit();
71
- } catch (error) { await connection.rollback(); throw error; }
72
- finally { connection.release(); }
73
- } });
74
- return {
75
- pool,
76
- schemaVersions: versions.map(row => row.version),
77
- devices: {
78
- entries: () => [...records.values()],
79
- async recordConnection(id, clientId, at, info) {
80
- const record = records.get(id), client = record?.clients.find(item => item.id === clientId);
81
- if (!client) return;
82
- const updated = connectionInfo(client, at, info);
83
- await pool.execute('UPDATE mobile_authorizations SET last_connected_at=?,platform=?,device_name=? WHERE computer_id=? AND client_id=?',
84
- [sqlTime(updated.lastConnectedAt), updated.platform || null, updated.deviceName || null, id, clientId]);
85
- records.set(id, { ...record, clients: record.clients.map(item => item.id === clientId ? updated : item) });
86
- return updated;
87
- },
88
- async save(record) {
89
- validator.save(record);
90
- const connection = await pool.getConnection();
91
- try {
92
- await connection.beginTransaction();
93
- await connection.execute(`INSERT INTO computers (id,host_hash,status,revision,created_at,updated_at)
94
- VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE host_hash=VALUES(host_hash),status=VALUES(status),
95
- revision=VALUES(revision),updated_at=VALUES(updated_at)`,
96
- [record.id, record.hostHash, record.status, record.revision, sqlTime(record.createdAt), sqlTime(record.updatedAt)]);
97
- await connection.execute('DELETE FROM mobile_authorizations WHERE computer_id = ?', [record.id]);
98
- for (const client of record.clients) await connection.execute(
99
- 'INSERT INTO mobile_authorizations (computer_id,client_id,credential_hash,created_at,updated_at,last_connected_at,platform,device_name) VALUES (?,?,?,?,?,?,?,?)',
100
- [record.id, client.id, client.hash, sqlTime(client.createdAt), sqlTime(client.updatedAt),
101
- client.lastConnectedAt ? sqlTime(client.lastConnectedAt) : null, client.platform || null, client.deviceName || null]);
102
- if (record.officialComputer) {
103
- const item = record.officialComputer;
104
- await connection.execute(`INSERT INTO official_computers (computer_id,environment_id,name,hostname,updated_at)
105
- VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE environment_id=VALUES(environment_id),name=VALUES(name),
106
- hostname=VALUES(hostname),updated_at=VALUES(updated_at)`,
107
- [record.id, item.environmentId, item.name, item.hostname, sqlTime(item.updatedAt)]);
108
- }
109
- await connection.commit(); records.set(record.id, record);
110
- } catch (error) { await connection.rollback(); throw error; }
111
- finally { connection.release(); }
112
- },
113
- },
114
- admin,
115
- close: () => pool.end(),
116
- };
117
- } catch (error) { await pool.end(); throw error; }
118
- }
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { fileURLToPath } from 'node:url';
4
+ import mysql from 'mysql2/promise';
5
+ import { deviceStore } from './device-store.mjs';
6
+ import { relayAdminState } from './admin-state.mjs';
7
+ import { relaySchemaVersions } from '../../database/versions.mjs';
8
+ import { connectionInfo } from './device-info.mjs';
9
+
10
+ const sqlTime = value => value.replace('T', ' ').replace(/Z$/, '');
11
+ const isoTime = value => value.replace(' ', 'T') + 'Z';
12
+
13
+ export async function openRelayMysql(configFile, initialMaxRelaySessions) {
14
+ const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
15
+ if (!config.database || !config.user || !config.password || !(config.socketPath || config.host)) {
16
+ throw new Error('Incomplete relay MySQL configuration');
17
+ }
18
+ const pool = mysql.createPool({ ...config, waitForConnections: true, connectionLimit: 4,
19
+ namedPlaceholders: false, dateStrings: true, timezone: 'Z', multipleStatements: false });
20
+ try {
21
+ const [versions] = await pool.query('SELECT version, sha256 FROM schema_migrations');
22
+ const knownVersions = relaySchemaVersions;
23
+ if (!versions.some(row => row.version === knownVersions[0]) || versions.length > knownVersions.length) throw new Error('Unexpected relay database schema');
24
+ for (const row of versions) {
25
+ if (!knownVersions.includes(row.version)) throw new Error('Unexpected relay database schema');
26
+ const source = fs.readFileSync(fileURLToPath(new URL(`../../database/${row.version}.sql`, import.meta.url)));
27
+ if (row.sha256 !== createHash('sha256').update(source).digest('hex')) throw new Error(`Unexpected relay database schema: ${row.version}`);
28
+ }
29
+ const [computers] = await pool.query('SELECT id, host_hash, status, revision, created_at, updated_at FROM computers');
30
+ const [clients] = await pool.query('SELECT computer_id, client_id, credential_hash, created_at, updated_at, last_connected_at, platform, device_name FROM mobile_authorizations ORDER BY created_at');
31
+ const [official] = await pool.query('SELECT computer_id, environment_id, name, hostname, updated_at FROM official_computers');
32
+ const records = new Map(computers.map(row => [row.id, { id: row.id, hostHash: row.host_hash, status: row.status,
33
+ revision: Number(row.revision), createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at), clients: [] }]));
34
+ for (const row of clients) {
35
+ const record = records.get(row.computer_id);
36
+ if (!record) throw new Error('Orphan mobile authorization');
37
+ record.clients.push({ id: row.client_id, hash: row.credential_hash,
38
+ createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at),
39
+ ...(row.last_connected_at ? { lastConnectedAt: isoTime(row.last_connected_at) } : {}),
40
+ ...(row.platform ? { platform: row.platform } : {}), ...(row.device_name ? { deviceName: row.device_name } : {}) });
41
+ }
42
+ for (const row of official) {
43
+ const record = records.get(row.computer_id);
44
+ if (!record) throw new Error('Orphan official computer');
45
+ record.officialComputer = { environmentId: row.environment_id, name: row.name,
46
+ hostname: row.hostname, updatedAt: isoTime(row.updated_at) };
47
+ }
48
+ const validator = deviceStore();
49
+ for (const record of records.values()) validator.save(record);
50
+
51
+ const [settings] = await pool.query('SELECT max_relay_sessions FROM relay_settings WHERE id = 1');
52
+ if (settings.length !== 1) throw new Error('Missing relay settings');
53
+ const [rows] = await pool.query('SELECT id, title, message, status, created_at, updated_at, published_at FROM announcements ORDER BY created_at DESC');
54
+ const announcements = rows.map(row => ({ id: row.id, title: row.title, message: row.message, status: row.status,
55
+ createdAt: isoTime(row.created_at), updatedAt: isoTime(row.updated_at),
56
+ publishedAt: row.published_at ? isoTime(row.published_at) : null }));
57
+ const active = announcements.find(item => item.status === 'active');
58
+ const initial = { version: 3, maxRelaySessions: settings[0].max_relay_sessions, announcements,
59
+ announcement: active ? { id: active.id, title: active.title, message: active.message, publishedAt: active.publishedAt } : null };
60
+ const admin = relayAdminState(undefined, initialMaxRelaySessions, { initial, persist: async next => {
61
+ const connection = await pool.getConnection();
62
+ try {
63
+ await connection.beginTransaction();
64
+ await connection.execute('UPDATE relay_settings SET max_relay_sessions = ?, updated_at = UTC_TIMESTAMP(3) WHERE id = 1', [next.maxRelaySessions]);
65
+ await connection.query('DELETE FROM announcements');
66
+ for (const item of next.announcements) await connection.execute(
67
+ 'INSERT INTO announcements (id,title,message,status,created_at,updated_at,published_at) VALUES (?,?,?,?,?,?,?)',
68
+ [item.id, item.title, item.message, item.status, sqlTime(item.createdAt), sqlTime(item.updatedAt),
69
+ item.publishedAt ? sqlTime(item.publishedAt) : null]);
70
+ await connection.commit();
71
+ } catch (error) { await connection.rollback(); throw error; }
72
+ finally { connection.release(); }
73
+ } });
74
+ return {
75
+ pool,
76
+ schemaVersions: versions.map(row => row.version),
77
+ devices: {
78
+ entries: () => [...records.values()],
79
+ async recordConnection(id, clientId, at, info) {
80
+ const record = records.get(id), client = record?.clients.find(item => item.id === clientId);
81
+ if (!client) return;
82
+ const updated = connectionInfo(client, at, info);
83
+ await pool.execute('UPDATE mobile_authorizations SET last_connected_at=?,platform=?,device_name=? WHERE computer_id=? AND client_id=?',
84
+ [sqlTime(updated.lastConnectedAt), updated.platform || null, updated.deviceName || null, id, clientId]);
85
+ records.set(id, { ...record, clients: record.clients.map(item => item.id === clientId ? updated : item) });
86
+ return updated;
87
+ },
88
+ async save(record) {
89
+ validator.save(record);
90
+ const connection = await pool.getConnection();
91
+ try {
92
+ await connection.beginTransaction();
93
+ await connection.execute(`INSERT INTO computers (id,host_hash,status,revision,created_at,updated_at)
94
+ VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE host_hash=VALUES(host_hash),status=VALUES(status),
95
+ revision=VALUES(revision),updated_at=VALUES(updated_at)`,
96
+ [record.id, record.hostHash, record.status, record.revision, sqlTime(record.createdAt), sqlTime(record.updatedAt)]);
97
+ await connection.execute('DELETE FROM mobile_authorizations WHERE computer_id = ?', [record.id]);
98
+ for (const client of record.clients) await connection.execute(
99
+ 'INSERT INTO mobile_authorizations (computer_id,client_id,credential_hash,created_at,updated_at,last_connected_at,platform,device_name) VALUES (?,?,?,?,?,?,?,?)',
100
+ [record.id, client.id, client.hash, sqlTime(client.createdAt), sqlTime(client.updatedAt),
101
+ client.lastConnectedAt ? sqlTime(client.lastConnectedAt) : null, client.platform || null, client.deviceName || null]);
102
+ if (record.officialComputer) {
103
+ const item = record.officialComputer;
104
+ await connection.execute(`INSERT INTO official_computers (computer_id,environment_id,name,hostname,updated_at)
105
+ VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE environment_id=VALUES(environment_id),name=VALUES(name),
106
+ hostname=VALUES(hostname),updated_at=VALUES(updated_at)`,
107
+ [record.id, item.environmentId, item.name, item.hostname, sqlTime(item.updatedAt)]);
108
+ }
109
+ await connection.commit(); records.set(record.id, record);
110
+ } catch (error) { await connection.rollback(); throw error; }
111
+ finally { connection.release(); }
112
+ },
113
+ },
114
+ admin,
115
+ close: () => pool.end(),
116
+ };
117
+ } catch (error) { await pool.end(); throw error; }
118
+ }
@@ -1,55 +1,55 @@
1
- import { validId } from './device-store.mjs';
2
-
3
- export function validPhoneConnection(value) {
4
- return value && validId(value.connectionId) && validId(value.clientId) && ['lan', 'self-relay'].includes(value.mode);
5
- }
6
-
7
- export function phoneConnectionEvents(pool) {
8
- const recent = new Map();
9
- const lastConnections = new Map();
10
- return {
11
- async record(computerId, event) {
12
- if (!validPhoneConnection(event) || !validId(event.id) ||
13
- !['connected', 'disconnected'].includes(event.state) ||
14
- typeof event.at !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(event.at) ||
15
- !Number.isFinite(Date.parse(event.at)) || new Date(event.at).toISOString() !== event.at) throw new Error('Invalid phone connection event');
16
- const item = { id: event.id, connectionId: event.connectionId, clientId: event.clientId,
17
- mode: event.mode, state: event.state, at: event.at, receivedAt: new Date().toISOString() };
18
- if (pool) await pool.execute(`INSERT INTO workstation_phone_events
19
- (event_id,computer_id,client_id,connection_id,mode,state,occurred_at,received_at) VALUES (?,?,?,?,?,?,?,?)
20
- ON DUPLICATE KEY UPDATE event_id=event_id`,
21
- [item.id, computerId, item.clientId, item.connectionId, item.mode, item.state,
22
- item.at.slice(0, 23).replace('T', ' '), item.receivedAt.slice(0, 23).replace('T', ' ')]);
23
- else {
24
- const items = recent.get(computerId) || [];
25
- if (!items.some(value => value.id === item.id)) items.push(item);
26
- recent.set(computerId, items.slice(-20));
27
- if (item.state === 'connected') {
28
- const phones = lastConnections.get(computerId) || new Map();
29
- if (!phones.has(item.clientId) || phones.get(item.clientId).at <= item.at) phones.set(item.clientId, item);
30
- lastConnections.set(computerId, phones);
31
- }
32
- }
33
- },
34
- async latest(computerIds) {
35
- if (!computerIds.length) return [];
36
- if (!pool) return computerIds.flatMap(computerId => [...(lastConnections.get(computerId)?.values() || [])]
37
- .map(item => ({ computerId, clientId: item.clientId, mode: item.mode, at: item.at })));
38
- const [rows] = await pool.execute(`SELECT computer_id,client_id,mode,occurred_at FROM (
39
- SELECT computer_id,client_id,mode,occurred_at,
40
- ROW_NUMBER() OVER (PARTITION BY computer_id,client_id ORDER BY occurred_at DESC,sequence_id DESC) AS connection_rank
41
- FROM workstation_phone_events WHERE state='connected' AND computer_id IN (${computerIds.map(() => '?').join(',')})
42
- ) latest WHERE connection_rank=1`, computerIds);
43
- return rows.map(row => ({ computerId: row.computer_id, clientId: row.client_id,
44
- mode: row.mode, at: row.occurred_at.replace(' ', 'T') + 'Z' }));
45
- },
46
- async list(computerId) {
47
- if (!pool) return [...(recent.get(computerId) || [])].reverse();
48
- const [rows] = await pool.execute(`SELECT event_id,client_id,connection_id,mode,state,occurred_at,received_at
49
- FROM workstation_phone_events WHERE computer_id=? ORDER BY sequence_id DESC LIMIT 20`, [computerId]);
50
- return rows.map(row => ({ id: row.event_id, clientId: row.client_id, connectionId: row.connection_id,
51
- mode: row.mode, state: row.state, at: row.occurred_at.replace(' ', 'T') + 'Z',
52
- receivedAt: row.received_at.replace(' ', 'T') + 'Z' }));
53
- },
54
- };
55
- }
1
+ import { validId } from './device-store.mjs';
2
+
3
+ export function validPhoneConnection(value) {
4
+ return value && validId(value.connectionId) && validId(value.clientId) && ['lan', 'self-relay'].includes(value.mode);
5
+ }
6
+
7
+ export function phoneConnectionEvents(pool) {
8
+ const recent = new Map();
9
+ const lastConnections = new Map();
10
+ return {
11
+ async record(computerId, event) {
12
+ if (!validPhoneConnection(event) || !validId(event.id) ||
13
+ !['connected', 'disconnected'].includes(event.state) ||
14
+ typeof event.at !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(event.at) ||
15
+ !Number.isFinite(Date.parse(event.at)) || new Date(event.at).toISOString() !== event.at) throw new Error('Invalid phone connection event');
16
+ const item = { id: event.id, connectionId: event.connectionId, clientId: event.clientId,
17
+ mode: event.mode, state: event.state, at: event.at, receivedAt: new Date().toISOString() };
18
+ if (pool) await pool.execute(`INSERT INTO workstation_phone_events
19
+ (event_id,computer_id,client_id,connection_id,mode,state,occurred_at,received_at) VALUES (?,?,?,?,?,?,?,?)
20
+ ON DUPLICATE KEY UPDATE event_id=event_id`,
21
+ [item.id, computerId, item.clientId, item.connectionId, item.mode, item.state,
22
+ item.at.slice(0, 23).replace('T', ' '), item.receivedAt.slice(0, 23).replace('T', ' ')]);
23
+ else {
24
+ const items = recent.get(computerId) || [];
25
+ if (!items.some(value => value.id === item.id)) items.push(item);
26
+ recent.set(computerId, items.slice(-20));
27
+ if (item.state === 'connected') {
28
+ const phones = lastConnections.get(computerId) || new Map();
29
+ if (!phones.has(item.clientId) || phones.get(item.clientId).at <= item.at) phones.set(item.clientId, item);
30
+ lastConnections.set(computerId, phones);
31
+ }
32
+ }
33
+ },
34
+ async latest(computerIds) {
35
+ if (!computerIds.length) return [];
36
+ if (!pool) return computerIds.flatMap(computerId => [...(lastConnections.get(computerId)?.values() || [])]
37
+ .map(item => ({ computerId, clientId: item.clientId, mode: item.mode, at: item.at })));
38
+ const [rows] = await pool.execute(`SELECT computer_id,client_id,mode,occurred_at FROM (
39
+ SELECT computer_id,client_id,mode,occurred_at,
40
+ ROW_NUMBER() OVER (PARTITION BY computer_id,client_id ORDER BY occurred_at DESC,sequence_id DESC) AS connection_rank
41
+ FROM workstation_phone_events WHERE state='connected' AND computer_id IN (${computerIds.map(() => '?').join(',')})
42
+ ) latest WHERE connection_rank=1`, computerIds);
43
+ return rows.map(row => ({ computerId: row.computer_id, clientId: row.client_id,
44
+ mode: row.mode, at: row.occurred_at.replace(' ', 'T') + 'Z' }));
45
+ },
46
+ async list(computerId) {
47
+ if (!pool) return [...(recent.get(computerId) || [])].reverse();
48
+ const [rows] = await pool.execute(`SELECT event_id,client_id,connection_id,mode,state,occurred_at,received_at
49
+ FROM workstation_phone_events WHERE computer_id=? ORDER BY sequence_id DESC LIMIT 20`, [computerId]);
50
+ return rows.map(row => ({ id: row.event_id, clientId: row.client_id, connectionId: row.connection_id,
51
+ mode: row.mode, state: row.state, at: row.occurred_at.replace(' ', 'T') + 'Z',
52
+ receivedAt: row.received_at.replace(' ', 'T') + 'Z' }));
53
+ },
54
+ };
55
+ }