@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.31

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 (185) hide show
  1. package/README.md +95 -89
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +374 -367
  4. package/dist/bin/manage-relay-admin.mjs +20 -20
  5. package/dist/bin/migrate-relay-db.mjs +109 -109
  6. package/dist/bin/publish-android-release.mjs +62 -58
  7. package/dist/bin/self-relay-admin.mjs +8 -8
  8. package/dist/bin/self-relay-demo.mjs +33 -33
  9. package/dist/bin/self-relay-server.mjs +66 -66
  10. package/dist/build-platform.json +1 -1
  11. package/dist/get-lan-networks.ps1 +40 -40
  12. package/dist/get-shared-service-url.ps1 +6 -6
  13. package/dist/shared/app-server-events.cjs +422 -412
  14. package/dist/shared/code-browser.cjs +159 -159
  15. package/dist/shared/file-changes.cjs +31 -31
  16. package/dist/shared/file-citations.cjs +117 -117
  17. package/dist/shared/file-mentions.cjs +92 -92
  18. package/dist/shared/message-delivery.cjs +54 -54
  19. package/dist/shared/p2p-data-channel.cjs +72 -72
  20. package/dist/shared/p2p-diagnostics.cjs +28 -28
  21. package/dist/shared/p2p-file-channel.cjs +74 -74
  22. package/dist/shared/plan-pricing.cjs +32 -32
  23. package/dist/shared/question-attention.cjs +40 -0
  24. package/dist/shared/server-requests.cjs +219 -219
  25. package/dist/shared/timeline-sync.cjs +173 -173
  26. package/dist/skills/code-relax/SKILL.md +22 -22
  27. package/dist/src/archived-threads.mjs +22 -0
  28. package/dist/src/background-process.mjs +18 -18
  29. package/dist/src/bounded-process-log.mjs +63 -63
  30. package/dist/src/celsius-client.mjs +107 -107
  31. package/dist/src/code-browser.mjs +100 -100
  32. package/dist/src/desktop-launcher.mjs +46 -46
  33. package/dist/src/dots-approval-timeline.mjs +99 -99
  34. package/dist/src/dots-avatar.mjs +42 -42
  35. package/dist/src/dots-chat-projection.mjs +50 -50
  36. package/dist/src/dots-chat-server.mjs +461 -461
  37. package/dist/src/dots-message-images.mjs +319 -319
  38. package/dist/src/dots-profile.mjs +77 -77
  39. package/dist/src/dots-room-activity.mjs +94 -94
  40. package/dist/src/dots-room-client.mjs +135 -135
  41. package/dist/src/dots-room-projection.mjs +94 -94
  42. package/dist/src/dots-schedules.mjs +66 -66
  43. package/dist/src/dots-summary-tasks.mjs +32 -32
  44. package/dist/src/dots-task-catalog.mjs +430 -430
  45. package/dist/src/dots-task-files.mjs +52 -52
  46. package/dist/src/execution-health.mjs +26 -26
  47. package/dist/src/file-transfer-range.mjs +22 -22
  48. package/dist/src/file-upload-session.mjs +130 -130
  49. package/dist/src/hosted-history-app-server.mjs +103 -103
  50. package/dist/src/hosted-history-cache.mjs +113 -113
  51. package/dist/src/lan-socket-server.mjs +265 -263
  52. package/dist/src/managed-app-server.mjs +244 -244
  53. package/dist/src/message-images.mjs +9 -9
  54. package/dist/src/message-upload-files.mjs +74 -74
  55. package/dist/src/onboarding.mjs +12 -12
  56. package/dist/src/plan-pricing.mjs +47 -47
  57. package/dist/src/platform/README.md +12 -12
  58. package/dist/src/platform/codex-protocol.mjs +46 -46
  59. package/dist/src/platform/codex-runtime.mjs +3 -3
  60. package/dist/src/platform/credential-store.mjs +3 -3
  61. package/dist/src/platform/desktop-host.mjs +3 -3
  62. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  63. package/dist/src/platform/desktop-project-store.mjs +208 -149
  64. package/dist/src/platform/file-opener.mjs +9 -0
  65. package/dist/src/platform/linux/README.md +3 -3
  66. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  67. package/dist/src/platform/linux/credential-store.mjs +28 -0
  68. package/dist/src/platform/linux/desktop-host.mjs +83 -0
  69. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  70. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  71. package/dist/src/platform/linux/network-info.mjs +11 -0
  72. package/dist/src/platform/linux/process.mjs +42 -0
  73. package/dist/src/platform/linux/project-locations.mjs +17 -0
  74. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  75. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  76. package/dist/src/platform/linux/service-host.mjs +18 -0
  77. package/dist/src/platform/linux/system.mjs +49 -0
  78. package/dist/src/platform/linux/user-environment.mjs +7 -0
  79. package/dist/src/platform/macos/README.md +3 -3
  80. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  81. package/dist/src/platform/macos/credential-store.mjs +10 -10
  82. package/dist/src/platform/macos/desktop-host.mjs +82 -82
  83. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
  84. package/dist/src/platform/macos/network-info.mjs +8 -28
  85. package/dist/src/platform/macos/project-locations.mjs +8 -8
  86. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  87. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  88. package/dist/src/platform/macos/service-host.mjs +18 -18
  89. package/dist/src/platform/macos/system.mjs +29 -29
  90. package/dist/src/platform/macos/user-environment.mjs +7 -7
  91. package/dist/src/platform/native-package.mjs +58 -46
  92. package/dist/src/platform/network-info.mjs +3 -3
  93. package/dist/src/platform/path-identity.mjs +5 -5
  94. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  95. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  96. package/dist/src/platform/project-locations.mjs +3 -3
  97. package/dist/src/platform/runtime-paths.mjs +8 -8
  98. package/dist/src/platform/select.mjs +5 -5
  99. package/dist/src/platform/service-host.mjs +3 -3
  100. package/dist/src/platform/user-environment.mjs +3 -3
  101. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  102. package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
  103. package/dist/src/platform/windows/background-process.mjs +30 -30
  104. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  105. package/dist/src/platform/windows/credential-store.mjs +27 -27
  106. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  107. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  108. package/dist/src/platform/windows/desktop-host.mjs +58 -58
  109. package/dist/src/platform/windows/desktop-info.ps1 +14 -14
  110. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  111. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  112. package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
  113. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  114. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  115. package/dist/src/platform/windows/network-info.mjs +34 -34
  116. package/dist/src/platform/windows/project-locations.mjs +16 -16
  117. package/dist/src/platform/windows/resident-start.js +20 -20
  118. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  119. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  120. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  121. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  122. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  123. package/dist/src/platform/windows/service-host.mjs +31 -31
  124. package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
  125. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  126. package/dist/src/platform/windows/user-environment.mjs +50 -50
  127. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  128. package/dist/src/project-archive.mjs +30 -0
  129. package/dist/src/project-create.mjs +121 -119
  130. package/dist/src/project-management.mjs +59 -69
  131. package/dist/src/project-remove.mjs +55 -0
  132. package/dist/src/project-update.mjs +56 -0
  133. package/dist/src/runtime-discovery.mjs +47 -47
  134. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  135. package/dist/src/self-relay/account-connections.mjs +52 -52
  136. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  137. package/dist/src/self-relay/admin-page.mjs +80 -80
  138. package/dist/src/self-relay/admin-server.mjs +74 -74
  139. package/dist/src/self-relay/admin-state.mjs +127 -127
  140. package/dist/src/self-relay/admin-style.mjs +14 -14
  141. package/dist/src/self-relay/android-releases.mjs +61 -61
  142. package/dist/src/self-relay/client.mjs +61 -61
  143. package/dist/src/self-relay/connector.mjs +439 -438
  144. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  145. package/dist/src/self-relay/demo.mjs +37 -37
  146. package/dist/src/self-relay/device-info.mjs +27 -0
  147. package/dist/src/self-relay/device-store.mjs +92 -84
  148. package/dist/src/self-relay/diagnostics.mjs +54 -54
  149. package/dist/src/self-relay/email-accounts.mjs +183 -183
  150. package/dist/src/self-relay/file-wire.mjs +13 -13
  151. package/dist/src/self-relay/lan-authorizations.mjs +71 -42
  152. package/dist/src/self-relay/lifecycle.mjs +264 -250
  153. package/dist/src/self-relay/long-pairing.mjs +40 -40
  154. package/dist/src/self-relay/mysql-state.mjs +118 -105
  155. package/dist/src/self-relay/p2p-probe.mjs +159 -159
  156. package/dist/src/self-relay/privacy-page.mjs +26 -26
  157. package/dist/src/self-relay/public-releases.mjs +34 -0
  158. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  159. package/dist/src/self-relay/server.mjs +1006 -932
  160. package/dist/src/self-relay/usage-daily.mjs +55 -55
  161. package/dist/src/self-relay/usage-meter.mjs +141 -141
  162. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  163. package/dist/src/self-relay/wire.mjs +36 -36
  164. package/dist/src/server.mjs +1861 -1778
  165. package/dist/src/service-control.mjs +82 -82
  166. package/dist/src/service-doctor.mjs +110 -110
  167. package/dist/src/service-health.mjs +33 -33
  168. package/dist/src/service-install.mjs +91 -91
  169. package/dist/src/service-lifecycle.mjs +138 -138
  170. package/dist/src/service-restart.mjs +81 -81
  171. package/dist/src/shared-app-server.mjs +615 -611
  172. package/dist/src/shared-catalog.mjs +132 -132
  173. package/dist/src/shared-recovery.mjs +146 -146
  174. package/dist/src/shared-thread-stream.mjs +164 -164
  175. package/dist/src/skill-install.mjs +51 -51
  176. package/dist/src/thread-title-generation.mjs +150 -150
  177. package/dist/src/turn-model-history.mjs +47 -47
  178. package/dist/src/usage-reset.mjs +17 -17
  179. package/dist/src/workstation-downloads.mjs +120 -120
  180. package/dist/tools/rtc-route-helper.ps1 +99 -99
  181. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  182. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  183. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  184. package/package.json +7 -6
  185. package/tools/postinstall.mjs +127 -127
@@ -1,55 +1,55 @@
1
- const dayMs = 86400000;
2
- const chinaOffsetMs = 8 * 3600000;
3
-
4
- export function usageDaySlices(start, end) {
5
- const slices = [];
6
- for (let cursor = start; cursor < end;) {
7
- const date = new Date(cursor + chinaOffsetMs).toISOString().slice(0, 10);
8
- const nextDay = Date.parse(`${date}T00:00:00Z`) + dayMs - chinaOffsetMs;
9
- const until = Math.min(end, nextDay);
10
- slices.push({ date, start: cursor, end: until });
11
- cursor = until;
12
- }
13
- return slices;
14
- }
15
-
16
- const readIntervals = value => Array.isArray(value) ? value : JSON.parse(value || '[]');
17
-
18
- export function mergeUsageIntervals(existing, start, end) {
19
- const sorted = [...existing, [start, end]].sort((a, b) => a[0] - b[0]);
20
- const merged = [];
21
- for (const range of sorted) {
22
- const last = merged.at(-1);
23
- if (last && range[0] <= last[1]) last[1] = Math.max(last[1], range[1]);
24
- else merged.push([...range]);
25
- }
26
- return { intervals: merged, seconds: merged.reduce((sum, [from, until]) => sum + until - from, 0) / 1000 };
27
- }
28
-
29
- export async function recordDailyUsage(db, userId, computerId, free, start, end, now) {
30
- for (const slice of usageDaySlices(start, end)) {
31
- await db.execute(`INSERT IGNORE INTO relay_usage_user_days
32
- (user_id,usage_date,intervals,updated_at) VALUES (?,?,'[]',?)`, [userId, slice.date, now]);
33
- const [userRows] = await db.execute(`SELECT intervals FROM relay_usage_user_days
34
- WHERE user_id=? AND usage_date=? FOR UPDATE`, [userId, slice.date]);
35
- const user = mergeUsageIntervals(readIntervals(userRows[0].intervals), slice.start, slice.end);
36
- await db.execute(`UPDATE relay_usage_user_days SET intervals=?,task_seconds=?,updated_at=?
37
- WHERE user_id=? AND usage_date=?`,
38
- [JSON.stringify(user.intervals), user.seconds, now, userId, slice.date]);
39
-
40
- await db.execute(`INSERT IGNORE INTO relay_usage_computer_days
41
- (computer_id,usage_date,intervals,free_intervals,updated_at) VALUES (?,?,'[]','[]',?)`,
42
- [computerId, slice.date, now]);
43
- const [computerRows] = await db.execute(`SELECT intervals,free_intervals FROM relay_usage_computer_days
44
- WHERE computer_id=? AND usage_date=? FOR UPDATE`, [computerId, slice.date]);
45
- const computer = mergeUsageIntervals(readIntervals(computerRows[0].intervals), slice.start, slice.end);
46
- const freeIntervals = readIntervals(computerRows[0].free_intervals);
47
- const freeComputer = free ? mergeUsageIntervals(freeIntervals, slice.start, slice.end) : null;
48
- await db.execute(`UPDATE relay_usage_computer_days SET intervals=?,task_seconds=?,
49
- free_intervals=?,free_task_seconds=COALESCE(?,free_task_seconds),updated_at=?
50
- WHERE computer_id=? AND usage_date=?`,
51
- [JSON.stringify(computer.intervals), computer.seconds,
52
- JSON.stringify(freeComputer?.intervals || freeIntervals), freeComputer?.seconds ?? null,
53
- now, computerId, slice.date]);
54
- }
55
- }
1
+ const dayMs = 86400000;
2
+ const chinaOffsetMs = 8 * 3600000;
3
+
4
+ export function usageDaySlices(start, end) {
5
+ const slices = [];
6
+ for (let cursor = start; cursor < end;) {
7
+ const date = new Date(cursor + chinaOffsetMs).toISOString().slice(0, 10);
8
+ const nextDay = Date.parse(`${date}T00:00:00Z`) + dayMs - chinaOffsetMs;
9
+ const until = Math.min(end, nextDay);
10
+ slices.push({ date, start: cursor, end: until });
11
+ cursor = until;
12
+ }
13
+ return slices;
14
+ }
15
+
16
+ const readIntervals = value => Array.isArray(value) ? value : JSON.parse(value || '[]');
17
+
18
+ export function mergeUsageIntervals(existing, start, end) {
19
+ const sorted = [...existing, [start, end]].sort((a, b) => a[0] - b[0]);
20
+ const merged = [];
21
+ for (const range of sorted) {
22
+ const last = merged.at(-1);
23
+ if (last && range[0] <= last[1]) last[1] = Math.max(last[1], range[1]);
24
+ else merged.push([...range]);
25
+ }
26
+ return { intervals: merged, seconds: merged.reduce((sum, [from, until]) => sum + until - from, 0) / 1000 };
27
+ }
28
+
29
+ export async function recordDailyUsage(db, userId, computerId, free, start, end, now) {
30
+ for (const slice of usageDaySlices(start, end)) {
31
+ await db.execute(`INSERT IGNORE INTO relay_usage_user_days
32
+ (user_id,usage_date,intervals,updated_at) VALUES (?,?,'[]',?)`, [userId, slice.date, now]);
33
+ const [userRows] = await db.execute(`SELECT intervals FROM relay_usage_user_days
34
+ WHERE user_id=? AND usage_date=? FOR UPDATE`, [userId, slice.date]);
35
+ const user = mergeUsageIntervals(readIntervals(userRows[0].intervals), slice.start, slice.end);
36
+ await db.execute(`UPDATE relay_usage_user_days SET intervals=?,task_seconds=?,updated_at=?
37
+ WHERE user_id=? AND usage_date=?`,
38
+ [JSON.stringify(user.intervals), user.seconds, now, userId, slice.date]);
39
+
40
+ await db.execute(`INSERT IGNORE INTO relay_usage_computer_days
41
+ (computer_id,usage_date,intervals,free_intervals,updated_at) VALUES (?,?,'[]','[]',?)`,
42
+ [computerId, slice.date, now]);
43
+ const [computerRows] = await db.execute(`SELECT intervals,free_intervals FROM relay_usage_computer_days
44
+ WHERE computer_id=? AND usage_date=? FOR UPDATE`, [computerId, slice.date]);
45
+ const computer = mergeUsageIntervals(readIntervals(computerRows[0].intervals), slice.start, slice.end);
46
+ const freeIntervals = readIntervals(computerRows[0].free_intervals);
47
+ const freeComputer = free ? mergeUsageIntervals(freeIntervals, slice.start, slice.end) : null;
48
+ await db.execute(`UPDATE relay_usage_computer_days SET intervals=?,task_seconds=?,
49
+ free_intervals=?,free_task_seconds=COALESCE(?,free_task_seconds),updated_at=?
50
+ WHERE computer_id=? AND usage_date=?`,
51
+ [JSON.stringify(computer.intervals), computer.seconds,
52
+ JSON.stringify(freeComputer?.intervals || freeIntervals), freeComputer?.seconds ?? null,
53
+ now, computerId, slice.date]);
54
+ }
55
+ }
@@ -1,141 +1,141 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { recordDailyUsage } from './usage-daily.mjs';
3
-
4
- const epoch = value => new Date(`${value.replace(' ', 'T')}Z`).getTime();
5
-
6
- // Count confirmed intervals only; a lost connection never creates estimated time.
7
- export function usageMeter(pool) {
8
- async function action(connection, computerId, clientId, kind, key) {
9
- if (!connection.userId || !['message', 'turn'].includes(kind) || typeof key !== 'string' || !key || key.length > 128) return;
10
- const column = kind === 'message' ? 'message_count' : 'turn_count';
11
- const db = await pool.getConnection();
12
- try {
13
- await db.beginTransaction();
14
- const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
15
- if (!users.length) { await db.commit(); return false; }
16
- const [result] = await db.execute(`INSERT IGNORE INTO relay_usage_actions
17
- (computer_id,kind,action_key,user_id,client_id,occurred_at)
18
- VALUES (?,?,?,?,?,UTC_TIMESTAMP(3))`,
19
- [computerId, kind, key, connection.userId, clientId]);
20
- if (result.affectedRows === 1) {
21
- await db.execute(`INSERT INTO relay_usage_totals (user_id,${column},updated_at)
22
- VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
23
- updated_at=VALUES(updated_at)`, [connection.userId]);
24
- await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,${column},updated_at)
25
- VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
26
- updated_at=VALUES(updated_at)`, [computerId]);
27
- }
28
- await db.commit();
29
- return result.affectedRows === 1;
30
- } catch (error) { await db.rollback(); throw error; }
31
- finally { db.release(); }
32
- }
33
- async function turnOrigin(computerId, key) {
34
- const [rows] = await pool.execute(`SELECT a.user_id,a.client_id FROM relay_usage_actions a JOIN users u ON u.id=a.user_id
35
- WHERE a.computer_id=? AND a.kind='turn' AND a.action_key=? AND u.status='active'`, [computerId, key]);
36
- return rows[0] ? { userId: rows[0].user_id, clientId: rows[0].client_id } : null;
37
- }
38
- async function pulse(connection, active, computerId, clientId, externalKey = null) {
39
- if (!connection.userId) return;
40
- if (!active && !connection.usageSessionId) return;
41
- if (active && !connection.usageSessionId) {
42
- const id = randomUUID();
43
- const [inserted] = await pool.execute(`INSERT INTO relay_usage_sessions
44
- (id,kind,external_key,user_id,computer_id,client_id,started_at,confirmed_until,charged_seconds)
45
- SELECT ?,'task',?,?,?,?,UTC_TIMESTAMP(3),UTC_TIMESTAMP(3),0 FROM users WHERE id=? AND status='active'`, [id, externalKey, connection.userId, computerId, clientId, connection.userId]);
46
- if (!inserted.affectedRows) return;
47
- connection.usageSessionId = id;
48
- return;
49
- }
50
- const id = connection.usageSessionId;
51
- const db = await pool.getConnection();
52
- try {
53
- await db.beginTransaction();
54
- const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
55
- if (!users.length) { await db.commit(); connection.usageSessionId = undefined; return; }
56
- const [rows] = await db.execute('SELECT user_id,computer_id,confirmed_until,ended_at FROM relay_usage_sessions WHERE id=? FOR UPDATE', [id]);
57
- const row = rows[0];
58
- if (!row || row.ended_at) throw new Error('Invalid relay usage session');
59
- const [clock] = await db.query('SELECT UTC_TIMESTAMP(3) AS now');
60
- const now = clock[0].now;
61
- const nowMs = epoch(now);
62
- const chargeStart = Math.max(epoch(row.confirmed_until), nowMs - 20000);
63
- const seconds = Math.max(0, nowMs - chargeStart) / 1000;
64
- const [plans] = await db.execute('SELECT tier,valid_until FROM relay_usage_entitlements WHERE user_id=?', [row.user_id]);
65
- const subscribed = plans[0]?.tier === 'subscriber' && plans[0].valid_until && epoch(plans[0].valid_until) > epoch(now);
66
- const freeSeconds = subscribed ? 0 : seconds;
67
- await db.execute(`UPDATE relay_usage_sessions SET confirmed_until=?,ended_at=?,charged_seconds=charged_seconds+?,
68
- free_charged_seconds=free_charged_seconds+? WHERE id=?`, [now, active ? null : now, seconds, freeSeconds, id]);
69
- if (seconds > 0) {
70
- await recordDailyUsage(db, row.user_id, row.computer_id, !subscribed,
71
- chargeStart, nowMs, now);
72
- await db.execute(`INSERT INTO relay_usage_totals (user_id,task_seconds,updated_at)
73
- VALUES (?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
74
- updated_at=VALUES(updated_at)`, [row.user_id, seconds, now]);
75
- await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,task_seconds,free_task_seconds,updated_at)
76
- VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
77
- free_task_seconds=free_task_seconds+VALUES(free_task_seconds),updated_at=VALUES(updated_at)`,
78
- [row.computer_id, seconds, freeSeconds, now]);
79
- }
80
- await db.commit();
81
- if (!active) connection.usageSessionId = undefined;
82
- } catch (error) { await db.rollback(); throw error; }
83
- finally { db.release(); }
84
- }
85
-
86
- async function lost(connection) {
87
- const id = connection.usageSessionId;
88
- connection.usageSessionId = undefined;
89
- if (!id) return;
90
- await pool.execute(`UPDATE relay_usage_sessions SET ended_at=confirmed_until
91
- WHERE id=? AND ended_at IS NULL`, [id]);
92
- }
93
-
94
- return { action, turnOrigin, pulse, lost };
95
- }
96
-
97
- export async function usageReport(pool, page, view = 'user') {
98
- if (view === 'computer') {
99
- const offset = (page - 1) * 30;
100
- const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_computer_days
101
- WHERE usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND task_seconds>0`);
102
- const [rows] = await pool.execute(`SELECT c.id,
103
- d.task_seconds AS today_task_seconds,
104
- d.free_task_seconds AS today_free_task_seconds,
105
- COALESCE(a.message_count,0) AS message_count,
106
- COALESCE(a.turn_count,0) AS turn_count,
107
- COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
108
- FROM relay_usage_computer_days d JOIN computers c ON c.id=d.computer_id
109
- LEFT JOIN (SELECT computer_id,SUM(kind='message') AS message_count,
110
- SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
111
- WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
112
- AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
113
- GROUP BY computer_id) a ON a.computer_id=c.id
114
- LEFT JOIN (SELECT computer_id,MAX(confirmed_until) AS last_used_at
115
- FROM relay_usage_sessions GROUP BY computer_id) s ON s.computer_id=c.id
116
- WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
117
- ORDER BY d.task_seconds DESC,c.id LIMIT ? OFFSET ?`, [30, offset]);
118
- return { page, pageSize: 30, total: counts[0].total, rows };
119
- }
120
- const offset = (page - 1) * 30;
121
- const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_user_days d
122
- JOIN user_login_identities i ON i.user_id=d.user_id AND i.kind='email'
123
- WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0`);
124
- const [rows] = await pool.execute(`SELECT u.id,i.normalized_value AS email,
125
- d.task_seconds AS today_task_seconds,
126
- COALESCE(a.message_count,0) AS message_count,
127
- COALESCE(a.turn_count,0) AS turn_count,
128
- COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
129
- FROM relay_usage_user_days d JOIN users u ON u.id=d.user_id
130
- JOIN user_login_identities i ON i.user_id=u.id AND i.kind='email'
131
- LEFT JOIN (SELECT user_id,SUM(kind='message') AS message_count,
132
- SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
133
- WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
134
- AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
135
- GROUP BY user_id) a ON a.user_id=u.id
136
- LEFT JOIN (SELECT user_id,MAX(confirmed_until) AS last_used_at
137
- FROM relay_usage_sessions GROUP BY user_id) s ON s.user_id=u.id
138
- WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
139
- ORDER BY d.task_seconds DESC,u.id LIMIT ? OFFSET ?`, [30, offset]);
140
- return { page, pageSize: 30, total: counts[0].total, rows };
141
- }
1
+ import { randomUUID } from 'node:crypto';
2
+ import { recordDailyUsage } from './usage-daily.mjs';
3
+
4
+ const epoch = value => new Date(`${value.replace(' ', 'T')}Z`).getTime();
5
+
6
+ // Count confirmed intervals only; a lost connection never creates estimated time.
7
+ export function usageMeter(pool) {
8
+ async function action(connection, computerId, clientId, kind, key) {
9
+ if (!connection.userId || !['message', 'turn'].includes(kind) || typeof key !== 'string' || !key || key.length > 128) return;
10
+ const column = kind === 'message' ? 'message_count' : 'turn_count';
11
+ const db = await pool.getConnection();
12
+ try {
13
+ await db.beginTransaction();
14
+ const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
15
+ if (!users.length) { await db.commit(); return false; }
16
+ const [result] = await db.execute(`INSERT IGNORE INTO relay_usage_actions
17
+ (computer_id,kind,action_key,user_id,client_id,occurred_at)
18
+ VALUES (?,?,?,?,?,UTC_TIMESTAMP(3))`,
19
+ [computerId, kind, key, connection.userId, clientId]);
20
+ if (result.affectedRows === 1) {
21
+ await db.execute(`INSERT INTO relay_usage_totals (user_id,${column},updated_at)
22
+ VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
23
+ updated_at=VALUES(updated_at)`, [connection.userId]);
24
+ await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,${column},updated_at)
25
+ VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
26
+ updated_at=VALUES(updated_at)`, [computerId]);
27
+ }
28
+ await db.commit();
29
+ return result.affectedRows === 1;
30
+ } catch (error) { await db.rollback(); throw error; }
31
+ finally { db.release(); }
32
+ }
33
+ async function turnOrigin(computerId, key) {
34
+ const [rows] = await pool.execute(`SELECT a.user_id,a.client_id FROM relay_usage_actions a JOIN users u ON u.id=a.user_id
35
+ WHERE a.computer_id=? AND a.kind='turn' AND a.action_key=? AND u.status='active'`, [computerId, key]);
36
+ return rows[0] ? { userId: rows[0].user_id, clientId: rows[0].client_id } : null;
37
+ }
38
+ async function pulse(connection, active, computerId, clientId, externalKey = null) {
39
+ if (!connection.userId) return;
40
+ if (!active && !connection.usageSessionId) return;
41
+ if (active && !connection.usageSessionId) {
42
+ const id = randomUUID();
43
+ const [inserted] = await pool.execute(`INSERT INTO relay_usage_sessions
44
+ (id,kind,external_key,user_id,computer_id,client_id,started_at,confirmed_until,charged_seconds)
45
+ SELECT ?,'task',?,?,?,?,UTC_TIMESTAMP(3),UTC_TIMESTAMP(3),0 FROM users WHERE id=? AND status='active'`, [id, externalKey, connection.userId, computerId, clientId, connection.userId]);
46
+ if (!inserted.affectedRows) return;
47
+ connection.usageSessionId = id;
48
+ return;
49
+ }
50
+ const id = connection.usageSessionId;
51
+ const db = await pool.getConnection();
52
+ try {
53
+ await db.beginTransaction();
54
+ const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
55
+ if (!users.length) { await db.commit(); connection.usageSessionId = undefined; return; }
56
+ const [rows] = await db.execute('SELECT user_id,computer_id,confirmed_until,ended_at FROM relay_usage_sessions WHERE id=? FOR UPDATE', [id]);
57
+ const row = rows[0];
58
+ if (!row || row.ended_at) throw new Error('Invalid relay usage session');
59
+ const [clock] = await db.query('SELECT UTC_TIMESTAMP(3) AS now');
60
+ const now = clock[0].now;
61
+ const nowMs = epoch(now);
62
+ const chargeStart = Math.max(epoch(row.confirmed_until), nowMs - 20000);
63
+ const seconds = Math.max(0, nowMs - chargeStart) / 1000;
64
+ const [plans] = await db.execute('SELECT tier,valid_until FROM relay_usage_entitlements WHERE user_id=?', [row.user_id]);
65
+ const subscribed = plans[0]?.tier === 'subscriber' && plans[0].valid_until && epoch(plans[0].valid_until) > epoch(now);
66
+ const freeSeconds = subscribed ? 0 : seconds;
67
+ await db.execute(`UPDATE relay_usage_sessions SET confirmed_until=?,ended_at=?,charged_seconds=charged_seconds+?,
68
+ free_charged_seconds=free_charged_seconds+? WHERE id=?`, [now, active ? null : now, seconds, freeSeconds, id]);
69
+ if (seconds > 0) {
70
+ await recordDailyUsage(db, row.user_id, row.computer_id, !subscribed,
71
+ chargeStart, nowMs, now);
72
+ await db.execute(`INSERT INTO relay_usage_totals (user_id,task_seconds,updated_at)
73
+ VALUES (?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
74
+ updated_at=VALUES(updated_at)`, [row.user_id, seconds, now]);
75
+ await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,task_seconds,free_task_seconds,updated_at)
76
+ VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
77
+ free_task_seconds=free_task_seconds+VALUES(free_task_seconds),updated_at=VALUES(updated_at)`,
78
+ [row.computer_id, seconds, freeSeconds, now]);
79
+ }
80
+ await db.commit();
81
+ if (!active) connection.usageSessionId = undefined;
82
+ } catch (error) { await db.rollback(); throw error; }
83
+ finally { db.release(); }
84
+ }
85
+
86
+ async function lost(connection) {
87
+ const id = connection.usageSessionId;
88
+ connection.usageSessionId = undefined;
89
+ if (!id) return;
90
+ await pool.execute(`UPDATE relay_usage_sessions SET ended_at=confirmed_until
91
+ WHERE id=? AND ended_at IS NULL`, [id]);
92
+ }
93
+
94
+ return { action, turnOrigin, pulse, lost };
95
+ }
96
+
97
+ export async function usageReport(pool, page, view = 'user') {
98
+ if (view === 'computer') {
99
+ const offset = (page - 1) * 30;
100
+ const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_computer_days
101
+ WHERE usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND task_seconds>0`);
102
+ const [rows] = await pool.execute(`SELECT c.id,
103
+ d.task_seconds AS today_task_seconds,
104
+ d.free_task_seconds AS today_free_task_seconds,
105
+ COALESCE(a.message_count,0) AS message_count,
106
+ COALESCE(a.turn_count,0) AS turn_count,
107
+ COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
108
+ FROM relay_usage_computer_days d JOIN computers c ON c.id=d.computer_id
109
+ LEFT JOIN (SELECT computer_id,SUM(kind='message') AS message_count,
110
+ SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
111
+ WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
112
+ AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
113
+ GROUP BY computer_id) a ON a.computer_id=c.id
114
+ LEFT JOIN (SELECT computer_id,MAX(confirmed_until) AS last_used_at
115
+ FROM relay_usage_sessions GROUP BY computer_id) s ON s.computer_id=c.id
116
+ WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
117
+ ORDER BY d.task_seconds DESC,c.id LIMIT ? OFFSET ?`, [30, offset]);
118
+ return { page, pageSize: 30, total: counts[0].total, rows };
119
+ }
120
+ const offset = (page - 1) * 30;
121
+ const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_user_days d
122
+ JOIN user_login_identities i ON i.user_id=d.user_id AND i.kind='email'
123
+ WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0`);
124
+ const [rows] = await pool.execute(`SELECT u.id,i.normalized_value AS email,
125
+ d.task_seconds AS today_task_seconds,
126
+ COALESCE(a.message_count,0) AS message_count,
127
+ COALESCE(a.turn_count,0) AS turn_count,
128
+ COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
129
+ FROM relay_usage_user_days d JOIN users u ON u.id=d.user_id
130
+ JOIN user_login_identities i ON i.user_id=u.id AND i.kind='email'
131
+ LEFT JOIN (SELECT user_id,SUM(kind='message') AS message_count,
132
+ SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
133
+ WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
134
+ AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
135
+ GROUP BY user_id) a ON a.user_id=u.id
136
+ LEFT JOIN (SELECT user_id,MAX(confirmed_until) AS last_used_at
137
+ FROM relay_usage_sessions GROUP BY user_id) s ON s.user_id=u.id
138
+ WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
139
+ ORDER BY d.task_seconds DESC,u.id LIMIT ? OFFSET ?`, [30, offset]);
140
+ return { page, pageSize: 30, total: counts[0].total, rows };
141
+ }
@@ -1,75 +1,75 @@
1
- import net from 'node:net';
2
-
3
- const pipePath = String.raw`\\.\pipe\CodeRelaxRtcRouteHelper`;
4
-
5
- export function createWindowsRouteHelper({ enabled = false, token = '', iceServers = [], report = () => {} } = {}) {
6
- const addresses = new Set();
7
- const routeAddresses = new Set(iceServers.map(stunAddress).filter(publicIpv4));
8
- let chain = Promise.resolve();
9
- const enqueue = task => chain = chain.then(task, task);
10
- if (!enabled || process.platform !== 'win32') return { prepare() { return Promise.resolve(); }, include() { return Promise.resolve(); } };
11
- if (!/^[A-Za-z0-9_-]{32,128}$/.test(token)) throw new Error('Windows RTC 路由辅助凭据无效');
12
- const add = values => enqueue(async () => {
13
- for (const value of values) addresses.add(value);
14
- if (!values.length) return;
15
- const result = await request({ token, action: 'add', addresses: [...addresses], ttlSeconds: 120 });
16
- if (!result.ok) throw new Error(result.error || 'Windows RTC 路由辅助不可用');
17
- report('windows-route-helper', { count: addresses.size });
18
- });
19
- return {
20
- prepare() { return add([...routeAddresses]); },
21
- include(message) {
22
- const discovered = candidateAddresses(message);
23
- return add(discovered.filter(value => !addresses.has(value)));
24
- }
25
- };
26
- }
27
-
28
- function stunAddress(value) {
29
- const match = /^stun:([^:]+):\d{1,5}$/i.exec(value || '');
30
- return match?.[1] || '';
31
- }
32
-
33
- function candidateAddresses(message) {
34
- const text = message?.kind === 'candidate' ? message.candidate : ['offer', 'answer'].includes(message?.kind) ? message.sdp : '';
35
- const result = new Set();
36
- for (const line of String(text || '').split(/\r?\n/)) {
37
- const parts = line.trim().replace(/^a=/, '').split(/\s+/);
38
- if (parts[0]?.startsWith('candidate:') && parts[6] === 'typ' && publicIpv4(parts[4])) result.add(parts[4]);
39
- }
40
- return [...result];
41
- }
42
-
43
- function publicIpv4(value) {
44
- const parts = String(value).split('.').map(Number);
45
- if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return false;
46
- const [a, b] = parts;
47
- return a !== 0 && a !== 10 && a !== 127 && a < 224 && !(a === 169 && b === 254) && !(a === 172 && b >= 16 && b <= 31) &&
48
- !(a === 192 && b === 168) && !(a === 100 && b >= 64 && b <= 127) && !(a === 198 && (b === 18 || b === 19));
49
- }
50
-
51
- function request(value) {
52
- return new Promise((resolve, reject) => {
53
- const socket = net.createConnection(pipePath);
54
- let text = '';
55
- let settled = false;
56
- const rejectOnce = error => { if (!settled) { settled = true; reject(error); } };
57
- const resolveOnce = result => { if (!settled) { settled = true; resolve(result); } };
58
- socket.setTimeout(3000);
59
- socket.setEncoding('utf8');
60
- socket.once('connect', () => socket.write(`${JSON.stringify(value)}\n`));
61
- socket.on('data', chunk => {
62
- text += chunk;
63
- const line = text.indexOf('\n');
64
- if (line < 0) return;
65
- try { resolveOnce(JSON.parse(text.slice(0, line))); socket.end(); }
66
- catch { rejectOnce(new Error('Windows RTC 路由辅助响应无效')); socket.destroy(); }
67
- });
68
- socket.once('timeout', () => socket.destroy(new Error('Windows RTC 路由辅助超时')));
69
- socket.once('error', rejectOnce);
70
- socket.once('close', hadError => {
71
- if (hadError || settled) return;
72
- rejectOnce(new Error('Windows RTC 路由辅助响应无效'));
73
- });
74
- });
75
- }
1
+ import net from 'node:net';
2
+
3
+ const pipePath = String.raw`\\.\pipe\CodeRelaxRtcRouteHelper`;
4
+
5
+ export function createWindowsRouteHelper({ enabled = false, token = '', iceServers = [], report = () => {} } = {}) {
6
+ const addresses = new Set();
7
+ const routeAddresses = new Set(iceServers.map(stunAddress).filter(publicIpv4));
8
+ let chain = Promise.resolve();
9
+ const enqueue = task => chain = chain.then(task, task);
10
+ if (!enabled || process.platform !== 'win32') return { prepare() { return Promise.resolve(); }, include() { return Promise.resolve(); } };
11
+ if (!/^[A-Za-z0-9_-]{32,128}$/.test(token)) throw new Error('Windows RTC 路由辅助凭据无效');
12
+ const add = values => enqueue(async () => {
13
+ for (const value of values) addresses.add(value);
14
+ if (!values.length) return;
15
+ const result = await request({ token, action: 'add', addresses: [...addresses], ttlSeconds: 120 });
16
+ if (!result.ok) throw new Error(result.error || 'Windows RTC 路由辅助不可用');
17
+ report('windows-route-helper', { count: addresses.size });
18
+ });
19
+ return {
20
+ prepare() { return add([...routeAddresses]); },
21
+ include(message) {
22
+ const discovered = candidateAddresses(message);
23
+ return add(discovered.filter(value => !addresses.has(value)));
24
+ }
25
+ };
26
+ }
27
+
28
+ function stunAddress(value) {
29
+ const match = /^stun:([^:]+):\d{1,5}$/i.exec(value || '');
30
+ return match?.[1] || '';
31
+ }
32
+
33
+ function candidateAddresses(message) {
34
+ const text = message?.kind === 'candidate' ? message.candidate : ['offer', 'answer'].includes(message?.kind) ? message.sdp : '';
35
+ const result = new Set();
36
+ for (const line of String(text || '').split(/\r?\n/)) {
37
+ const parts = line.trim().replace(/^a=/, '').split(/\s+/);
38
+ if (parts[0]?.startsWith('candidate:') && parts[6] === 'typ' && publicIpv4(parts[4])) result.add(parts[4]);
39
+ }
40
+ return [...result];
41
+ }
42
+
43
+ function publicIpv4(value) {
44
+ const parts = String(value).split('.').map(Number);
45
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return false;
46
+ const [a, b] = parts;
47
+ return a !== 0 && a !== 10 && a !== 127 && a < 224 && !(a === 169 && b === 254) && !(a === 172 && b >= 16 && b <= 31) &&
48
+ !(a === 192 && b === 168) && !(a === 100 && b >= 64 && b <= 127) && !(a === 198 && (b === 18 || b === 19));
49
+ }
50
+
51
+ function request(value) {
52
+ return new Promise((resolve, reject) => {
53
+ const socket = net.createConnection(pipePath);
54
+ let text = '';
55
+ let settled = false;
56
+ const rejectOnce = error => { if (!settled) { settled = true; reject(error); } };
57
+ const resolveOnce = result => { if (!settled) { settled = true; resolve(result); } };
58
+ socket.setTimeout(3000);
59
+ socket.setEncoding('utf8');
60
+ socket.once('connect', () => socket.write(`${JSON.stringify(value)}\n`));
61
+ socket.on('data', chunk => {
62
+ text += chunk;
63
+ const line = text.indexOf('\n');
64
+ if (line < 0) return;
65
+ try { resolveOnce(JSON.parse(text.slice(0, line))); socket.end(); }
66
+ catch { rejectOnce(new Error('Windows RTC 路由辅助响应无效')); socket.destroy(); }
67
+ });
68
+ socket.once('timeout', () => socket.destroy(new Error('Windows RTC 路由辅助超时')));
69
+ socket.once('error', rejectOnce);
70
+ socket.once('close', hadError => {
71
+ if (hadError || settled) return;
72
+ rejectOnce(new Error('Windows RTC 路由辅助响应无效'));
73
+ });
74
+ });
75
+ }
@@ -1,36 +1,36 @@
1
- import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
2
- import { WebSocket } from 'ws';
3
-
4
- export const secret = () => randomBytes(32).toString('base64url');
5
- export const digest = value => createHash('sha256').update(value).digest('hex');
6
- export const validSecret = value => typeof value === 'string' && /^[\w-]{43}$/.test(value);
7
- export function matches(value, hash) {
8
- return validSecret(value) && typeof hash === 'string' && timingSafeEqual(Buffer.from(digest(value)), Buffer.from(hash));
9
- }
10
- export function send(socket, message) {
11
- if (socket.readyState !== WebSocket.OPEN) return false;
12
- if (socket.bufferedAmount > 64 * 1024 * 1024) { socket.close(1013, 'Receiver too slow'); return false; }
13
- socket.send(JSON.stringify(message)); return true;
14
- }
15
- export function decode(bytes, binary) {
16
- if (binary) throw new Error('Text frames required');
17
- const value = JSON.parse(bytes.toString());
18
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Object required');
19
- return value;
20
- }
21
- export function loopbackUrl(value, pathname) {
22
- const url = new URL(value);
23
- if (url.protocol !== 'ws:' || url.hostname !== '127.0.0.1' || url.pathname !== pathname || url.search || url.hash || url.username || url.password) {
24
- throw new Error('本地原型仅接受指定路径的 ws://127.0.0.1 地址');
25
- }
26
- return url.href;
27
- }
28
-
29
- export function relayAddress(value) {
30
- const url = new URL(value);
31
- if (url.protocol === 'ws:') return loopbackUrl(value, '/relay');
32
- if (url.protocol !== 'wss:' || url.pathname !== '/relay' || url.search || url.hash || url.username || url.password) {
33
- throw new Error('远程中继必须使用无查询凭据的 wss://主机/relay 地址');
34
- }
35
- return url.href;
36
- }
1
+ import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
2
+ import { WebSocket } from 'ws';
3
+
4
+ export const secret = () => randomBytes(32).toString('base64url');
5
+ export const digest = value => createHash('sha256').update(value).digest('hex');
6
+ export const validSecret = value => typeof value === 'string' && /^[\w-]{43}$/.test(value);
7
+ export function matches(value, hash) {
8
+ return validSecret(value) && typeof hash === 'string' && timingSafeEqual(Buffer.from(digest(value)), Buffer.from(hash));
9
+ }
10
+ export function send(socket, message) {
11
+ if (socket.readyState !== WebSocket.OPEN) return false;
12
+ if (socket.bufferedAmount > 64 * 1024 * 1024) { socket.close(1013, 'Receiver too slow'); return false; }
13
+ socket.send(JSON.stringify(message)); return true;
14
+ }
15
+ export function decode(bytes, binary) {
16
+ if (binary) throw new Error('Text frames required');
17
+ const value = JSON.parse(bytes.toString());
18
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Object required');
19
+ return value;
20
+ }
21
+ export function loopbackUrl(value, pathname) {
22
+ const url = new URL(value);
23
+ if (url.protocol !== 'ws:' || url.hostname !== '127.0.0.1' || url.pathname !== pathname || url.search || url.hash || url.username || url.password) {
24
+ throw new Error('本地原型仅接受指定路径的 ws://127.0.0.1 地址');
25
+ }
26
+ return url.href;
27
+ }
28
+
29
+ export function relayAddress(value) {
30
+ const url = new URL(value);
31
+ if (url.protocol === 'ws:') return loopbackUrl(value, '/relay');
32
+ if (url.protocol !== 'wss:' || url.pathname !== '/relay' || url.search || url.hash || url.username || url.password) {
33
+ throw new Error('远程中继必须使用无查询凭据的 wss://主机/relay 地址');
34
+ }
35
+ return url.href;
36
+ }