handmux 0.23.0 → 0.25.0

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 (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -1,124 +0,0 @@
1
- export const DEFAULT_SITE_VERSION = 'mobile';
2
- export const SITE_VERSIONS = new Set(['mobile', 'desktop']);
3
-
4
- export function normalizeSiteVersion(value, fallback = DEFAULT_SITE_VERSION) {
5
- if (value == null || value === '') return fallback;
6
- return SITE_VERSIONS.has(value) ? value : null;
7
- }
8
-
9
- function versionFrom(ua, pattern, fallback = '120.0.0.0') {
10
- return ua.match(pattern)?.[1] || fallback;
11
- }
12
-
13
- function identityPlatform(ua, mobile) {
14
- if (/iPhone|iPad|iPod/i.test(ua)) return { navigator: 'iPhone', clientHint: 'iOS' };
15
- if (/Android/i.test(ua)) return { navigator: 'Linux armv8l', clientHint: 'Android' };
16
- if (/Windows/i.test(ua)) return { navigator: 'Win32', clientHint: 'Windows' };
17
- if (/Macintosh|Mac OS X/i.test(ua)) return { navigator: 'MacIntel', clientHint: 'macOS' };
18
- return mobile
19
- ? { navigator: 'Linux armv8l', clientHint: 'Android' }
20
- : { navigator: 'Linux x86_64', clientHint: 'Linux' };
21
- }
22
-
23
- function isMobileUserAgent(ua) {
24
- return /Android|Mobile|iPhone|iPad|iPod/i.test(ua);
25
- }
26
-
27
- function transformedUserAgent(source, mobile) {
28
- const ua = String(source || '');
29
- if (ua && isMobileUserAgent(ua) === mobile) return ua;
30
-
31
- const webkit = versionFrom(ua, /AppleWebKit\/([\d.]+)/i, '605.1.15');
32
- const safari = versionFrom(ua, /Version\/([\d.]+)/i, '18.0');
33
- if (/Safari\//i.test(ua) && !/(?:Chrome|CriOS|Chromium|Edg|OPR)\//i.test(ua)) {
34
- return mobile
35
- ? `Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/${webkit} (KHTML, like Gecko) Version/${safari} Mobile/15E148 Safari/${webkit}`
36
- : `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/${webkit} (KHTML, like Gecko) Version/${safari} Safari/${webkit}`;
37
- }
38
-
39
- const firefox = ua.match(/Firefox\/([\d.]+)/i)?.[1];
40
- if (firefox) {
41
- return mobile
42
- ? `Mozilla/5.0 (Android 10; Mobile; rv:${firefox}) Gecko/${firefox} Firefox/${firefox}`
43
- : `Mozilla/5.0 (X11; Linux x86_64; rv:${firefox}) Gecko/20100101 Firefox/${firefox}`;
44
- }
45
-
46
- const chrome = versionFrom(ua, /(?:Chrome|CriOS|Chromium|Edg|OPR)\/([\d.]+)/i);
47
- return mobile
48
- ? `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chrome} Mobile Safari/537.36`
49
- : `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chrome} Safari/537.36`;
50
- }
51
-
52
- export function siteVersionIdentity(siteVersion, sourceUserAgent = '') {
53
- const version = normalizeSiteVersion(siteVersion);
54
- if (!version) throw new Error('invalid browser site version');
55
- const mobile = version === 'mobile';
56
- const userAgent = transformedUserAgent(sourceUserAgent, mobile);
57
- const platform = identityPlatform(userAgent, mobile);
58
- return {
59
- version,
60
- mobile,
61
- userAgent,
62
- appVersion: userAgent.replace(/^Mozilla\//, ''),
63
- platform: platform.navigator,
64
- clientHintPlatform: platform.clientHint,
65
- maxTouchPoints: mobile ? 5 : 0,
66
- };
67
- }
68
-
69
- function headerKey(headers, name) {
70
- const lower = name.toLowerCase();
71
- return Object.keys(headers || {}).find((key) => key.toLowerCase() === lower) || lower;
72
- }
73
-
74
- function setHeader(headers, name, value) {
75
- headers[headerKey(headers, name)] = value;
76
- }
77
-
78
- function deleteHeader(headers, name) {
79
- const key = Object.keys(headers || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
80
- if (key) delete headers[key];
81
- }
82
-
83
- export function applySiteVersionHeaders(headers, identity) {
84
- if (!headers || !identity) return headers;
85
- setHeader(headers, 'user-agent', identity.userAgent);
86
- setHeader(headers, 'sec-ch-ua-mobile', identity.mobile ? '?1' : '?0');
87
- setHeader(headers, 'sec-ch-ua-platform', `"${identity.clientHintPlatform}"`);
88
- for (const name of [
89
- 'sec-ch-ua-platform-version', 'sec-ch-ua-arch', 'sec-ch-ua-bitness',
90
- 'sec-ch-ua-model', 'sec-ch-ua-wow64', 'sec-ch-ua-full-version',
91
- 'sec-ch-ua-full-version-list',
92
- ]) deleteHeader(headers, name);
93
- return headers;
94
- }
95
-
96
- export function siteVersionNavigatorScript(identity) {
97
- const encoded = JSON.stringify(identity).replaceAll('<', '\\u003c');
98
- return `(() => {
99
- const profile = ${encoded};
100
- const define = (name, value) => {
101
- try { Object.defineProperty(navigator, name, { configurable: true, get: () => value }); }
102
- catch { /* the native value remains available */ }
103
- };
104
- define('userAgent', profile.userAgent);
105
- define('appVersion', profile.appVersion);
106
- define('platform', profile.platform);
107
- define('maxTouchPoints', profile.maxTouchPoints);
108
- const nativeData = navigator.userAgentData;
109
- if (nativeData) {
110
- const data = {
111
- brands: nativeData.brands || [],
112
- mobile: profile.mobile,
113
- platform: profile.clientHintPlatform,
114
- toJSON() { return { brands: this.brands, mobile: this.mobile, platform: this.platform }; },
115
- async getHighEntropyValues(hints) {
116
- let values = {};
117
- try { values = await nativeData.getHighEntropyValues.call(nativeData, hints); } catch { /* optional API */ }
118
- return { ...values, mobile: profile.mobile, platform: profile.clientHintPlatform };
119
- },
120
- };
121
- define('userAgentData', data);
122
- }
123
- })();`;
124
- }
@@ -1,137 +0,0 @@
1
- import dns from 'node:dns/promises';
2
- import net from 'node:net';
3
-
4
- function ipv4Parts(address) {
5
- const parts = address.split('.').map(Number);
6
- return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255)
7
- ? parts
8
- : null;
9
- }
10
-
11
- function ipv6Words(address) {
12
- const halves = address.split('::');
13
- if (halves.length > 2) return null;
14
- const parseHalf = (half) => {
15
- if (!half) return [];
16
- const words = [];
17
- for (const token of half.split(':')) {
18
- if (token.includes('.')) {
19
- const parts = ipv4Parts(token);
20
- if (!parts) return null;
21
- words.push((parts[0] << 8) | parts[1], (parts[2] << 8) | parts[3]);
22
- } else {
23
- if (!/^[0-9a-f]{1,4}$/.test(token)) return null;
24
- words.push(Number.parseInt(token, 16));
25
- }
26
- }
27
- return words;
28
- };
29
- const left = parseHalf(halves[0]);
30
- const right = parseHalf(halves[1] || '');
31
- if (!left || !right) return null;
32
- if (halves.length === 1) return left.length === 8 ? left : null;
33
- const omitted = 8 - left.length - right.length;
34
- if (omitted < 1) return null;
35
- return [...left, ...Array(omitted).fill(0), ...right];
36
- }
37
-
38
- function mappedIpv4Parts(address) {
39
- const words = ipv6Words(address);
40
- if (!words
41
- || words.slice(0, 5).some((word) => word !== 0)
42
- || words[5] !== 0xffff) return null;
43
- return [words[6] >> 8, words[6] & 0xff, words[7] >> 8, words[7] & 0xff];
44
- }
45
-
46
- function classifyIpv4(parts) {
47
- if (parts[0] === 0) return 'unspecified';
48
- if (parts[0] === 127) return 'loopback';
49
- if (parts[0] === 169 && parts[1] === 254) return 'link-local';
50
- if (parts[0] >= 224) return 'multicast';
51
- return null;
52
- }
53
-
54
- export function classifyIp(raw) {
55
- const address = String(raw || '').replace(/^\[|\]$/g, '').toLowerCase();
56
- if (net.isIP(address) === 4) {
57
- const parts = ipv4Parts(address);
58
- return classifyIpv4(parts);
59
- }
60
- if (net.isIP(address) === 6) {
61
- const mapped = mappedIpv4Parts(address);
62
- if (mapped) return classifyIpv4(mapped);
63
- if (address === '::') return 'unspecified';
64
- if (address === '::1') return 'loopback';
65
- if (/^fe[89ab]/.test(address)) return 'link-local';
66
- if (address.startsWith('ff')) return 'multicast';
67
- return null;
68
- }
69
- return null;
70
- }
71
-
72
- function effectivePort(url) {
73
- if (url.port) return url.port;
74
- return url.protocol === 'https:' ? '443' : '80';
75
- }
76
-
77
- export function createBrowserTargetPolicy({
78
- topLevelUrl,
79
- handmuxOrigin,
80
- lookup = dns.lookup,
81
- } = {}) {
82
- let topOrigin = new URL(topLevelUrl).origin;
83
- let topAddressClass = null;
84
- const appUrl = new URL(handmuxOrigin);
85
- const appOrigin = appUrl.origin;
86
- const appControlPort = effectivePort(appUrl);
87
-
88
- return {
89
- authorizeTopLevel(raw) {
90
- const url = new URL(raw);
91
- if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('browser URL must use http or https');
92
- topOrigin = url.origin;
93
- topAddressClass = null;
94
- },
95
-
96
- async check(raw) {
97
- let url;
98
- try { url = new URL(raw); } catch { return { allowed: false, reason: 'unsupported-protocol' }; }
99
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
100
- return { allowed: false, reason: 'unsupported-protocol' };
101
- }
102
- if (url.origin === appOrigin) return { allowed: false, reason: 'handmux-origin' };
103
-
104
- const hostname = url.hostname.replace(/^\[|\]$/g, '');
105
- let addresses;
106
- if (net.isIP(hostname)) addresses = [{ address: hostname }];
107
- else {
108
- try { addresses = await lookup(hostname, { all: true, verbatim: true }); }
109
- catch { return { allowed: false, reason: 'dns-failed' }; }
110
- }
111
- if (!addresses?.length) return { allowed: false, reason: 'dns-failed' };
112
-
113
- const classifications = addresses.map((item) => classifyIp(item.address));
114
- if (effectivePort(url) === appControlPort && classifications.some((reason) => reason === 'loopback')) {
115
- return { allowed: false, reason: 'handmux-origin' };
116
- }
117
- if (url.origin === topOrigin && topAddressClass == null) {
118
- if (classifications.every((reason) => reason === 'loopback')) topAddressClass = 'loopback';
119
- else if (classifications.every((reason) => reason == null)) topAddressClass = 'ordinary';
120
- }
121
- const loopbackNavigationAllowed = url.origin === topOrigin
122
- && topAddressClass === 'loopback'
123
- && classifications.every((reason) => reason === 'loopback');
124
- for (const reason of classifications) {
125
- if (reason === 'loopback' && loopbackNavigationAllowed) continue;
126
- if (reason) return { allowed: false, reason: reason === 'loopback' ? 'loopback-not-authorized' : reason };
127
- }
128
- return {
129
- allowed: true,
130
- addresses: addresses.map((item) => ({
131
- address: item.address,
132
- family: item.family || net.isIP(item.address),
133
- })),
134
- };
135
- },
136
- };
137
- }
@@ -1,253 +0,0 @@
1
- import http from 'node:http';
2
- import net from 'node:net';
3
- import { randomBytes } from 'node:crypto';
4
- import { fork } from 'node:child_process';
5
- import { fileURLToPath } from 'node:url';
6
- import { claimedBrowserRequest } from './publicProxy.js';
7
- import { BROWSER_INTERNAL_HEADER } from './protocol.js';
8
- import { createBrowserCoordinator } from './coordinator.js';
9
-
10
- const WORKER_FILE = fileURLToPath(new URL('./worker.js', import.meta.url));
11
-
12
- function forwardedHeaders(headers, internalToken, appToken, api) {
13
- const out = { ...headers, [BROWSER_INTERNAL_HEADER]: internalToken };
14
- if (api || out.authorization === `Bearer ${appToken}`) delete out.authorization;
15
- delete out['proxy-authorization'];
16
- return out;
17
- }
18
-
19
- export function createBrowserWorkerClient({
20
- appToken,
21
- previewDomain = null,
22
- handmuxOrigin = 'http://127.0.0.1',
23
- forkWorker = fork,
24
- request = http.request,
25
- connect = net.connect,
26
- randomToken = () => randomBytes(32).toString('base64url'),
27
- parentEnv = process.env,
28
- setTimer = setTimeout,
29
- clearTimer = clearTimeout,
30
- readyTimeoutMs = 10_000,
31
- stableAfterMs = 30_000,
32
- requestTimeoutMs = 15_000,
33
- stopTimeoutMs = 3_000,
34
- } = {}) {
35
- const internalToken = randomToken();
36
- let child = null;
37
- let port = null;
38
- let generation = 0;
39
- let stopping = false;
40
- let restartDelay = 250;
41
- let restartTimer = null;
42
- let readyTimer = null;
43
- let stableTimer = null;
44
- const activeSockets = new Set();
45
- const workerEnv = {};
46
- for (const name of [
47
- 'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TMPDIR', 'TMP', 'TEMP',
48
- 'LANG', 'LC_ALL', 'TZ', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
49
- 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
50
- 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
51
- ]) {
52
- if (parentEnv[name] != null) workerEnv[name] = parentEnv[name];
53
- }
54
-
55
- const clearReadyTimer = () => {
56
- if (readyTimer != null) clearTimer(readyTimer);
57
- readyTimer = null;
58
- };
59
- const clearStableTimer = () => {
60
- if (stableTimer != null) clearTimer(stableTimer);
61
- stableTimer = null;
62
- };
63
- const scheduleRestart = () => {
64
- if (stopping || restartTimer != null) return;
65
- const delay = restartDelay;
66
- restartDelay = Math.min(restartDelay * 2, 5_000);
67
- restartTimer = setTimer(() => {
68
- restartTimer = null;
69
- if (!stopping) start();
70
- }, delay);
71
- };
72
- const start = () => {
73
- let spawned;
74
- try {
75
- spawned = forkWorker(WORKER_FILE, [], {
76
- env: {
77
- ...workerEnv,
78
- HANDMUX_BROWSER_INTERNAL_TOKEN: internalToken,
79
- HANDMUX_BROWSER_CONTROL_ORIGIN: handmuxOrigin,
80
- ...(previewDomain ? { HANDMUX_PREVIEW_DOMAIN: previewDomain } : {}),
81
- },
82
- stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
83
- });
84
- } catch {
85
- child = null;
86
- port = null;
87
- scheduleRestart();
88
- return;
89
- }
90
- child = spawned;
91
- port = null;
92
- readyTimer = setTimer(() => {
93
- if (child === spawned && port == null) spawned.kill('SIGTERM');
94
- }, readyTimeoutMs);
95
- spawned.on('message', (message) => {
96
- if (child !== spawned || message?.type !== 'handmux-browser-ready' || !Number.isInteger(message.port)) return;
97
- port = message.port;
98
- generation += 1;
99
- clearReadyTimer();
100
- clearStableTimer();
101
- stableTimer = setTimer(() => {
102
- stableTimer = null;
103
- if (child === spawned && port != null) restartDelay = 250;
104
- }, stableAfterMs);
105
- });
106
- const finalize = () => {
107
- if (child !== spawned) return;
108
- child = null;
109
- port = null;
110
- generation += 1;
111
- clearReadyTimer();
112
- clearStableTimer();
113
- for (const socket of activeSockets) socket.destroy();
114
- activeSockets.clear();
115
- scheduleRestart();
116
- };
117
- spawned.once('error', () => {
118
- if (child !== spawned) return;
119
- try { spawned.kill('SIGTERM'); } catch { /* already gone */ }
120
- finalize();
121
- });
122
- spawned.once('exit', finalize);
123
- spawned.once('close', finalize);
124
- };
125
-
126
- const unavailable = (res, status) => res.status(status).json({ error: 'browser unavailable' });
127
- const proxyHttp = (api) => (req, res, next) => {
128
- if (!api && !claimedBrowserRequest(req)) return next();
129
- const targetPort = port;
130
- if (!targetPort) return unavailable(res, api ? 503 : 502);
131
- const upstream = request({
132
- hostname: '127.0.0.1',
133
- port: targetPort,
134
- method: req.method,
135
- path: req.originalUrl || req.url,
136
- headers: forwardedHeaders(req.headers, internalToken, appToken, api),
137
- }, (incoming) => {
138
- res.writeHead(incoming.statusCode || 502, incoming.headers);
139
- incoming.pipe(res);
140
- res.once('close', () => { if (!res.writableEnded) incoming.destroy(); });
141
- });
142
- if (api) upstream.setTimeout?.(requestTimeoutMs, () => upstream.destroy(new Error('browser worker request timeout')));
143
- const abort = () => upstream.destroy();
144
- req.once('aborted', abort);
145
- res.once('close', () => { if (!res.writableEnded) abort(); });
146
- upstream.once('error', () => {
147
- if (!res.headersSent) unavailable(res, api ? 503 : 502);
148
- else res.destroy();
149
- });
150
- req.pipe(upstream);
151
- };
152
-
153
- const onUpgrade = (req, socket, head) => {
154
- if (!claimedBrowserRequest(req)) return false;
155
- const targetPort = port;
156
- if (!targetPort) { socket.destroy(); return true; }
157
- const upstream = connect({ host: '127.0.0.1', port: targetPort });
158
- activeSockets.add(socket);
159
- activeSockets.add(upstream);
160
- const cleanup = () => {
161
- activeSockets.delete(socket);
162
- activeSockets.delete(upstream);
163
- };
164
- upstream.once('connect', () => {
165
- const headers = forwardedHeaders(req.headers, internalToken, appToken, false);
166
- const lines = [`${req.method || 'GET'} ${req.url} HTTP/${req.httpVersion || '1.1'}`];
167
- for (const [name, value] of Object.entries(headers)) {
168
- if (Array.isArray(value)) for (const item of value) lines.push(`${name}: ${item}`);
169
- else if (value != null) lines.push(`${name}: ${value}`);
170
- }
171
- upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
172
- if (head?.length) upstream.write(head);
173
- socket.pipe(upstream).pipe(socket);
174
- });
175
- upstream.once('error', () => socket.destroy());
176
- socket.once('error', () => upstream.destroy());
177
- upstream.once('close', cleanup);
178
- socket.once('close', cleanup);
179
- return true;
180
- };
181
-
182
- const proxyRequest = ({ req, method, path, body }) => new Promise((resolve) => {
183
- const targetPort = port;
184
- if (!targetPort) return resolve(null);
185
- const payload = body === undefined ? null : Buffer.from(JSON.stringify(body));
186
- const headers = forwardedHeaders(req.headers, internalToken, appToken, true);
187
- if (payload) {
188
- headers['content-type'] = 'application/json';
189
- headers['content-length'] = String(payload.length);
190
- } else {
191
- delete headers['content-length'];
192
- delete headers['content-type'];
193
- }
194
- const upstream = request({ hostname: '127.0.0.1', port: targetPort, method, path, headers }, (incoming) => {
195
- const chunks = [];
196
- incoming.on('data', (chunk) => chunks.push(chunk));
197
- incoming.on('end', () => resolve({
198
- status: incoming.statusCode || 502,
199
- headers: incoming.headers,
200
- body: Buffer.concat(chunks),
201
- }));
202
- });
203
- upstream.setTimeout?.(requestTimeoutMs, () => upstream.destroy(new Error('browser worker request timeout')));
204
- upstream.once('error', () => resolve(null));
205
- req.once('aborted', () => upstream.destroy());
206
- upstream.end(payload || undefined);
207
- });
208
-
209
- const apiHandler = createBrowserCoordinator({
210
- proxyRequest,
211
- getStatus: () => ({ ready: port != null, generation }),
212
- });
213
-
214
- if (previewDomain) start();
215
- let closePromise = null;
216
- return {
217
- apiHandler,
218
- publicHandler: proxyHttp(false),
219
- onUpgrade,
220
- close() {
221
- if (!closePromise) {
222
- stopping = true;
223
- apiHandler.close();
224
- if (restartTimer != null) clearTimer(restartTimer);
225
- restartTimer = null;
226
- clearReadyTimer();
227
- clearStableTimer();
228
- for (const socket of activeSockets) socket.destroy();
229
- activeSockets.clear();
230
- const current = child;
231
- if (!current) closePromise = Promise.resolve();
232
- else closePromise = new Promise((resolve) => {
233
- let finished = false;
234
- let forceTimer = setTimer(() => {
235
- forceTimer = null;
236
- current.kill('SIGKILL');
237
- }, stopTimeoutMs);
238
- const finish = () => {
239
- if (finished) return;
240
- finished = true;
241
- if (forceTimer != null) clearTimer(forceTimer);
242
- resolve();
243
- };
244
- current.once('error', finish);
245
- current.once('exit', finish);
246
- current.once('close', finish);
247
- try { current.kill('SIGTERM'); } catch { finish(); }
248
- });
249
- }
250
- return closePromise;
251
- },
252
- };
253
- }
@@ -1,98 +0,0 @@
1
- import http from 'node:http';
2
- import express from 'express';
3
- import { tokenEquals } from '../auth.js';
4
- import { createBrowserPreviewManager } from './manager.js';
5
- import { createBrowserBootstrapStore } from './bootstrap.js';
6
- import { createBrowserPublicProxy } from './publicProxy.js';
7
- import { browserRoutes } from './routes.js';
8
- import { BROWSER_INTERNAL_HEADER } from './protocol.js';
9
-
10
- function isLoopback(address) {
11
- return address === '127.0.0.1' || address === '::1' || String(address || '').startsWith('::ffff:127.');
12
- }
13
-
14
- function authenticated(req, token) {
15
- const provided = req.headers[BROWSER_INTERNAL_HEADER];
16
- return isLoopback(req.socket?.remoteAddress)
17
- && typeof provided === 'string'
18
- && tokenEquals(provided, token);
19
- }
20
-
21
- export async function createBrowserWorkerServer({
22
- internalToken,
23
- previewDomain = null,
24
- handmuxOrigin = 'http://127.0.0.1',
25
- browser: suppliedBrowser = null,
26
- managerFactory = createBrowserPreviewManager,
27
- browserPublicFactory = createBrowserPublicProxy,
28
- host = '127.0.0.1',
29
- port = 0,
30
- } = {}) {
31
- if (!internalToken) throw new Error('browser worker internal token required');
32
- const browserBootstrap = createBrowserBootstrapStore();
33
- const browser = suppliedBrowser || await managerFactory({
34
- handmuxOrigin,
35
- previewDomain,
36
- browserBootstrap,
37
- });
38
- const browserPublic = browserPublicFactory({ browser, browserBootstrap });
39
- const app = express();
40
-
41
- app.use((req, res, next) => {
42
- if (!authenticated(req, internalToken)) return res.status(401).json({ error: 'browser worker unauthorized' });
43
- delete req.headers[BROWSER_INTERNAL_HEADER];
44
- next();
45
- });
46
- app.get('/_browser-worker/health', (_req, res) => res.json({ ok: true }));
47
- app.use(
48
- '/api/browser-proxy',
49
- express.json(),
50
- browserRoutes({ browser, previewDomain, browserBootstrap }),
51
- );
52
- app.use(browserPublic.handler);
53
-
54
- const server = http.createServer(app);
55
- const sockets = new Set();
56
- server.on('connection', (socket) => {
57
- sockets.add(socket);
58
- socket.once('close', () => sockets.delete(socket));
59
- });
60
- server.on('upgrade', (req, socket, head) => {
61
- if (!authenticated(req, internalToken)) {
62
- socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
63
- return;
64
- }
65
- delete req.headers[BROWSER_INTERNAL_HEADER];
66
- if (!browserPublic.onUpgrade(req, socket, head)) socket.destroy();
67
- });
68
-
69
- await new Promise((resolve, reject) => {
70
- const onError = (error) => { server.off('listening', onListening); reject(error); };
71
- const onListening = () => { server.off('error', onError); resolve(); };
72
- server.once('error', onError);
73
- server.once('listening', onListening);
74
- server.listen(port, host);
75
- });
76
-
77
- let closePromise = null;
78
- return {
79
- host,
80
- port: server.address().port,
81
- server,
82
- browser,
83
- close() {
84
- if (!closePromise) {
85
- closePromise = (async () => {
86
- const stopped = new Promise((resolve) => server.close(resolve));
87
- server.closeIdleConnections?.();
88
- for (const socket of sockets) socket.destroy();
89
- try { await browser.close?.(); } finally {
90
- server.closeAllConnections?.();
91
- await stopped;
92
- }
93
- })();
94
- }
95
- return closePromise;
96
- },
97
- };
98
- }