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,556 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import * as importedHammerhead from 'testcafe-hammerhead';
5
- import { createDeviceCookieProfiles } from './cookieProfiles.js';
6
- import { createBrowserProfilePersistence } from './profilePersistence.js';
7
- import { claimPublicOrigin } from './originLabel.js';
8
- import { browserLabelForOrigin } from './originLabel.js';
9
- import { classifyIp, createBrowserTargetPolicy } from './targetPolicy.js';
10
- import {
11
- hammerheadRebindHeaders,
12
- installHammerheadRebindLocationCompat,
13
- } from './hammerheadRedirectCompat.js';
14
- import {
15
- applySiteVersionHeaders,
16
- normalizeSiteVersion,
17
- siteVersionIdentity,
18
- siteVersionNavigatorScript,
19
- } from './siteVersion.js';
20
-
21
- const defaultHammerhead = importedHammerhead.default || importedHammerhead;
22
- const POOL_IDLE_CLOSE_MS = 1_000;
23
-
24
- function normalizedOrigin(raw) {
25
- const url = new URL(raw);
26
- if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('browser origin must use http or https');
27
- return url.origin;
28
- }
29
-
30
- function normalizedTarget(raw) {
31
- const url = new URL(raw);
32
- if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('browser URL must use http or https');
33
- return url.toString();
34
- }
35
-
36
- function isLoopbackUrl(raw) {
37
- const hostname = new URL(raw).hostname.replace(/^\[|\]$/g, '').toLowerCase();
38
- return hostname === 'localhost' || classifyIp(hostname) === 'loopback';
39
- }
40
-
41
- function bridgeScript(channel, identity) {
42
- const encoded = JSON.stringify(channel);
43
- return `${siteVersionNavigatorScript(identity)}
44
- (() => {
45
- const channel = ${encoded};
46
- const hammerhead = window['%hammerhead%'];
47
- const destinationUrl = (url) => {
48
- try { return hammerhead?.utils?.url?.parseProxyUrl(url)?.destUrl || url; }
49
- catch { return url; }
50
- };
51
- const send = (type, url) => parent.postMessage({ source: 'handmux-browser', channel, type, url: url === undefined ? destinationUrl(location.href) : url, title: document.title }, '*');
52
- let pending = false;
53
- const activity = () => {
54
- if (pending) return;
55
- pending = true;
56
- setTimeout(() => { pending = false; send('activity'); }, 250);
57
- };
58
- for (const name of ['pointerdown', 'keydown', 'input', 'scroll']) addEventListener(name, activity, { capture: true, passive: true });
59
- addEventListener('load', () => send('load'));
60
- addEventListener('popstate', () => send('urlchange'));
61
- addEventListener('hashchange', () => send('urlchange'));
62
- for (const name of ['pushState', 'replaceState']) {
63
- const original = history[name];
64
- history[name] = function (...args) {
65
- const result = original.apply(this, args);
66
- send('urlchange');
67
- return result;
68
- };
69
- }
70
- addEventListener('pagehide', () => send('navigate'));
71
- let lastTitle;
72
- let lastTitleUrl;
73
- const sendTitle = () => {
74
- const title = document.title;
75
- const url = destinationUrl(location.href);
76
- if (title === lastTitle && url === lastTitleUrl) return;
77
- lastTitle = title;
78
- lastTitleUrl = url;
79
- send('title', url);
80
- };
81
- const observeTitle = () => {
82
- if (!document.head) return;
83
- const observer = new MutationObserver(sendTitle);
84
- observer.observe(document.head, { subtree: true, childList: true, characterData: true });
85
- sendTitle();
86
- };
87
- if (document.head) observeTitle();
88
- else addEventListener('DOMContentLoaded', observeTitle, { once: true });
89
- addEventListener('message', (event) => {
90
- if (event.source !== parent || event.data?.source !== 'handmux-browser-parent' || event.data?.channel !== channel) return;
91
- if (event.data.command === 'back') history.back();
92
- else if (event.data.command === 'forward') history.forward();
93
- else if (event.data.command === 'reload') location.reload();
94
- else if (event.data.command === 'stop') window.stop();
95
- });
96
- send('ready');
97
- })();`;
98
- }
99
-
100
- function browserSessionClass(hammerhead) {
101
- return class BrowserSession extends hammerhead.Session {
102
- constructor(channel, identity) {
103
- super([], {
104
- disablePageCaching: true,
105
- allowMultipleWindows: true,
106
- windowId: channel,
107
- requestTimeout: { page: 30_000, ajax: 30_000 },
108
- nativeAutomation: false,
109
- });
110
- this.channel = channel;
111
- this.identity = identity;
112
- }
113
-
114
- async getPayloadScript() { return bridgeScript(this.channel, this.identity); }
115
- async getIframePayloadScript() { return siteVersionNavigatorScript(this.identity); }
116
- getAuthCredentials() { return null; }
117
- handleAttachment() {}
118
- handleFileDownload() {}
119
- handlePageError() {}
120
- };
121
- }
122
-
123
- function applyPublicOrigin(proxy, origin) {
124
- const url = new URL(origin);
125
- const port = Number(url.port || (url.protocol === 'https:' ? 443 : 80));
126
- for (const info of [proxy.server1Info, proxy.server2Info]) {
127
- info.hostname = url.hostname;
128
- info.port = port;
129
- info.crossDomainPort = port;
130
- info.protocol = url.protocol;
131
- info.domain = url.origin;
132
- }
133
- }
134
-
135
- async function waitForListening(server) {
136
- if (!server || server.listening || typeof server.once !== 'function') return;
137
- await new Promise((resolve, reject) => {
138
- const cleanup = () => {
139
- server.removeListener('listening', onListening);
140
- server.removeListener('error', onError);
141
- server.removeListener('close', onClose);
142
- };
143
- const onListening = () => { cleanup(); resolve(); };
144
- const onError = (error) => { cleanup(); reject(error); };
145
- const onClose = () => { cleanup(); reject(new Error('browser manager closing')); };
146
- server.once('listening', onListening);
147
- server.once('error', onError);
148
- server.once('close', onClose);
149
- });
150
- }
151
-
152
- function publicLease(lease) {
153
- if (!lease) return null;
154
- return {
155
- tabId: lease.tabId,
156
- url: lease.url,
157
- originalUrl: lease.originalUrl,
158
- channel: lease.channel,
159
- siteVersion: lease.siteVersion,
160
- };
161
- }
162
-
163
- export async function createBrowserPreviewManager({
164
- hammerhead = defaultHammerhead,
165
- internalPorts = [0, 0],
166
- randomId = () => randomBytes(18).toString('base64url'),
167
- randomChannel = () => randomBytes(18).toString('base64url'),
168
- targetPolicyFactory = createBrowserTargetPolicy,
169
- handmuxOrigin = 'http://127.0.0.1',
170
- previewDomain = null,
171
- browserBootstrap = null,
172
- cookieProfiles: suppliedCookieProfiles,
173
- profilePersistence: suppliedProfilePersistence,
174
- profileDir = path.join(os.homedir(), '.handmux', 'browser-profiles'),
175
- setTimer = setTimeout,
176
- clearTimer = clearTimeout,
177
- leaseTtlMs = 2 * 60 * 60 * 1000,
178
- } = {}) {
179
- installHammerheadRebindLocationCompat();
180
- const ProxyClass = hammerhead.Proxy;
181
- const SessionClass = browserSessionClass(hammerhead);
182
- const profilePersistence = suppliedProfilePersistence || createBrowserProfilePersistence({
183
- dir: profileDir,
184
- keyFile: path.join(profileDir, 'profile.key'),
185
- });
186
- try {
187
- await profilePersistence.pruneExpiredProfiles?.();
188
- } catch (error) {
189
- console.warn(`[handmux] browser profile retention cleanup deferred: ${error?.message || error}`);
190
- }
191
- const cookieProfiles = suppliedCookieProfiles || createDeviceCookieProfiles({
192
- createCookies: () => new SessionClass('').cookies,
193
- persistence: profilePersistence,
194
- setTimer,
195
- clearTimer,
196
- });
197
- const pools = new Map();
198
- const pendingPools = new Map();
199
- const leases = new Map();
200
- const leaseQueues = new Map();
201
- const publicOriginClaims = new Map();
202
- let poolCount = 0;
203
- let closing = false;
204
- let rehomeNavigation = async () => false;
205
-
206
- const leaseKey = (deviceId, tabId) => `${deviceId}\u0000${tabId}`;
207
- const publicOriginFor = (target) => {
208
- if (!previewDomain) return null;
209
- const base = new URL(/^https?:\/\//i.test(previewDomain) ? previewDomain : `https://${previewDomain}`);
210
- base.hostname = `${browserLabelForOrigin(new URL(target).origin)}.${base.hostname}`;
211
- return base.origin;
212
- };
213
- const serializeLease = (key, operation) => {
214
- const previous = leaseQueues.get(key) || Promise.resolve();
215
- const current = previous.then(operation);
216
- const queued = current.catch(() => {});
217
- leaseQueues.set(key, queued);
218
- return current.finally(() => {
219
- if (leaseQueues.get(key) === queued) leaseQueues.delete(key);
220
- });
221
- };
222
- const poolFor = async (origin) => {
223
- if (pools.has(origin)) {
224
- const pool = pools.get(origin);
225
- if (pool.closeTimer != null) clearTimer(pool.closeTimer);
226
- pool.closeTimer = null;
227
- pool.references += 1;
228
- return pool;
229
- }
230
- if (pendingPools.has(origin)) {
231
- const pool = await pendingPools.get(origin);
232
- pool.references += 1;
233
- return pool;
234
- }
235
- const pending = (async () => {
236
- const proxy = new ProxyClass();
237
- const ports = poolCount++ === 0 ? internalPorts : [0, 0];
238
- try {
239
- proxy.start({
240
- hostname: '127.0.0.1',
241
- port1: ports[0],
242
- port2: ports[1],
243
- disableCrossDomain: true,
244
- disableHttp2: true,
245
- });
246
- await Promise.all([waitForListening(proxy.server1), waitForListening(proxy.server2)]);
247
- if (closing) throw new Error('browser manager closing');
248
- applyPublicOrigin(proxy, origin);
249
- const pool = {
250
- origin,
251
- proxy,
252
- references: 0,
253
- closeTimer: null,
254
- ports: [
255
- proxy.server1?.address?.()?.port ?? ports[0],
256
- proxy.server2?.address?.()?.port ?? ports[1],
257
- ],
258
- };
259
- pools.set(origin, pool);
260
- return pool;
261
- } catch (error) {
262
- proxy.close();
263
- throw error;
264
- }
265
- })();
266
- pendingPools.set(origin, pending);
267
- try {
268
- const pool = await pending;
269
- pool.references += 1;
270
- return pool;
271
- } finally {
272
- if (pendingPools.get(origin) === pending) pendingPools.delete(origin);
273
- }
274
- };
275
-
276
- const releasePool = (pool) => {
277
- if (!pool || pool.references <= 0) return;
278
- pool.references -= 1;
279
- if (pool.references !== 0) return;
280
- // A request hook can replace the final lease while the old pool is still writing its redirect.
281
- // Close after a short idle window so that response can drain; a new lease for the same origin reuses
282
- // the pool and cancels this timer. This still releases idle Hammerhead ports without worker shutdown.
283
- pool.closeTimer = setTimer(() => {
284
- pool.closeTimer = null;
285
- if (pool.references !== 0) return;
286
- if (pools.get(pool.origin) === pool) pools.delete(pool.origin);
287
- publicOriginClaims.delete(pool.origin);
288
- pool.proxy.close();
289
- }, POOL_IDLE_CLOSE_MS);
290
- };
291
-
292
- const disposeLease = (lease) => {
293
- if (!lease || lease.disposed) return;
294
- lease.disposed = true;
295
- if (lease.timer != null) clearTimer(lease.timer);
296
- lease.timer = null;
297
- lease.detachCookies();
298
- lease.pool.proxy.closeSession(lease.session);
299
- releasePool(lease.pool);
300
- };
301
-
302
- const setDeviceActive = (deviceId) => cookieProfiles.setActive?.(
303
- deviceId,
304
- [...leases.values()].some((lease) => lease.deviceId === deviceId),
305
- );
306
- const release = (lease) => {
307
- if (!lease || leases.get(lease.key) !== lease) return false;
308
- leases.delete(lease.key);
309
- disposeLease(lease);
310
- setDeviceActive(lease.deviceId);
311
- return true;
312
- };
313
- const touch = (lease) => {
314
- if (lease.timer != null) clearTimer(lease.timer);
315
- lease.timer = setTimer(() => release(lease), leaseTtlMs);
316
- };
317
- const createLease = async ({
318
- tabId, deviceId, url, origin, channel, siteVersion, sourceUserAgent,
319
- }) => {
320
- if (closing) throw new Error('browser manager closing');
321
- const target = normalizedTarget(url);
322
- const publicOrigin = normalizedOrigin(origin);
323
- const targetOrigin = new URL(target).origin;
324
- claimPublicOrigin(publicOriginClaims, publicOrigin, targetOrigin);
325
- const pool = await poolFor(publicOrigin);
326
- let session = null;
327
- let detachCookies = null;
328
- try {
329
- if (closing) throw new Error('browser manager closing');
330
- const identity = siteVersionIdentity(siteVersion, sourceUserAgent);
331
- session = new SessionClass(channel, identity);
332
- session.id = `_browser-${randomId()}-${encodeURIComponent(tabId)}`;
333
- const policy = targetPolicyFactory({ topLevelUrl: target, handmuxOrigin });
334
- const hooks = session.requestHookEventProvider || session;
335
- hooks.addRequestEventListeners(hammerhead.RequestFilterRule.ANY, {
336
- onRequest: async (event) => {
337
- if (await rehomeNavigation(event, session)) return;
338
- const result = await policy.check(event._requestInfo.url);
339
- if (result.allowed) {
340
- applySiteVersionHeaders(event.requestOptions?.headers, identity);
341
- if (result.addresses?.length && event.requestOptions) {
342
- const approved = result.addresses.map(({ address, family }) => ({ address, family }));
343
- event.requestOptions.autoSelectFamily = true;
344
- event.requestOptions.lookup = (_hostname, options, callback) => {
345
- if (options?.all) callback(null, approved);
346
- else callback(null, approved[0].address, approved[0].family);
347
- };
348
- }
349
- return;
350
- }
351
- await event.setMock(new hammerhead.ResponseMock(
352
- JSON.stringify({ error: 'browser target blocked', reason: result.reason }),
353
- 403,
354
- { 'content-type': 'application/json; charset=utf-8' },
355
- ));
356
- },
357
- }, () => {});
358
- detachCookies = cookieProfiles.attach(deviceId, session.cookies);
359
- const publicUrl = pool.proxy.openSession(target, session);
360
- return {
361
- key: leaseKey(deviceId, tabId),
362
- tabId,
363
- deviceId,
364
- originalUrl: target,
365
- url: publicUrl,
366
- publicOrigin,
367
- channel,
368
- siteVersion,
369
- sourceUserAgent,
370
- pool,
371
- session,
372
- policy,
373
- detachCookies,
374
- timer: null,
375
- disposed: false,
376
- };
377
- } catch (error) {
378
- detachCookies?.();
379
- if (session) pool.proxy.closeSession(session);
380
- releasePool(pool);
381
- throw error;
382
- }
383
- };
384
-
385
- const putImpl = async ({
386
- tabId, deviceId, url, origin, siteVersion, sourceUserAgent,
387
- }, requireExisting = false) => {
388
- if (!deviceId || !tabId) throw new Error('browser lease identity required');
389
- const key = leaseKey(deviceId, tabId);
390
- const existing = leases.get(key);
391
- if (requireExisting && !existing) return null;
392
- const target = normalizedTarget(url);
393
- const publicOrigin = normalizedOrigin(origin);
394
- const requestedVersion = normalizeSiteVersion(siteVersion);
395
- if (!requestedVersion) throw new Error('invalid browser site version');
396
- if (existing
397
- && existing.originalUrl === target
398
- && existing.publicOrigin === publicOrigin
399
- && existing.siteVersion === requestedVersion) {
400
- touch(existing);
401
- return publicLease(existing);
402
- }
403
- const next = await createLease({
404
- tabId,
405
- deviceId,
406
- url: target,
407
- origin: publicOrigin,
408
- channel: existing?.channel || randomChannel(),
409
- siteVersion: requestedVersion,
410
- sourceUserAgent,
411
- });
412
- leases.set(key, next);
413
- touch(next);
414
- if (existing) disposeLease(existing);
415
- setDeviceActive(deviceId);
416
- return publicLease(next);
417
- };
418
- const put = ({ tabId, deviceId, ...options }, requireExisting = false) => {
419
- const key = leaseKey(deviceId, tabId);
420
- return serializeLease(key, () => putImpl(
421
- { tabId, deviceId, ...options },
422
- requireExisting,
423
- ));
424
- };
425
-
426
- rehomeNavigation = async (event, session) => {
427
- const info = event?._requestInfo || {};
428
- const headers = info.headers || {};
429
- const destination = String(headers['sec-fetch-dest'] || headers['Sec-Fetch-Dest'] || '').toLowerCase();
430
- const acceptsHtml = String(headers.accept || headers.Accept || '').toLowerCase().includes('text/html');
431
- const topLevelDocument = !info.isAjax && !info.isIframe
432
- && (destination === 'document' || destination === 'iframe' || acceptsHtml);
433
- if (!topLevelDocument) return false;
434
- let target;
435
- try { target = normalizedTarget(info.url); } catch { return false; }
436
- const observed = [...leases.values()].find((lease) => lease.session === session);
437
- if (!observed || new URL(target).origin === new URL(observed.originalUrl).origin) return false;
438
- const origin = publicOriginFor(target);
439
- if (!origin || !browserBootstrap) return false;
440
-
441
- return serializeLease(observed.key, async () => {
442
- const current = leases.get(observed.key);
443
- if (!current || current.session !== session) return false;
444
- const result = await current.policy.check(target);
445
- const loopbackPortChange = isLoopbackUrl(current.originalUrl)
446
- && isLoopbackUrl(target)
447
- && new URL(current.originalUrl).origin !== new URL(target).origin;
448
- if (!result.allowed || loopbackPortChange) {
449
- await event.setMock(new hammerhead.ResponseMock(
450
- JSON.stringify({
451
- error: 'browser target blocked',
452
- reason: loopbackPortChange ? 'loopback-not-authorized' : result.reason,
453
- }),
454
- 403,
455
- { 'content-type': 'application/json; charset=utf-8' },
456
- ));
457
- return true;
458
- }
459
- const next = await createLease({
460
- tabId: current.tabId,
461
- deviceId: current.deviceId,
462
- url: target,
463
- origin,
464
- channel: current.channel,
465
- siteVersion: current.siteVersion,
466
- sourceUserAgent: current.sourceUserAgent,
467
- });
468
- try {
469
- const bootstrapUrl = browserBootstrap.issue({
470
- url: next.url,
471
- origin: next.publicOrigin,
472
- deviceId: next.deviceId,
473
- preserveMethod: true,
474
- redirectStatus: 307,
475
- });
476
- await event.setMock(new hammerhead.ResponseMock(
477
- '',
478
- 307,
479
- hammerheadRebindHeaders(bootstrapUrl),
480
- ));
481
- leases.set(current.key, next);
482
- touch(next);
483
- disposeLease(current);
484
- return true;
485
- } catch (error) {
486
- disposeLease(next);
487
- throw error;
488
- }
489
- });
490
- };
491
-
492
- const sessionIdForPath = (pathname) => {
493
- const descriptor = String(pathname || '').split('/')[1] || '';
494
- const sessionId = descriptor.split(/[!*]/, 1)[0];
495
- return sessionId.startsWith('_browser-') ? sessionId : null;
496
- };
497
-
498
- return {
499
- putLease: (options) => put(options),
500
- navigateLease(tabId, url, deviceId, origin, siteVersion, sourceUserAgent) {
501
- return put({ tabId, url, deviceId, origin, siteVersion, sourceUserAgent }, true);
502
- },
503
- getLease(tabId, deviceId) {
504
- return publicLease(leases.get(leaseKey(deviceId, tabId)));
505
- },
506
- deleteLease(tabId, deviceId) {
507
- return release(leases.get(leaseKey(deviceId, tabId)));
508
- },
509
- hasDevice(deviceId) {
510
- return [...leases.values()].some((lease) => lease.deviceId === deviceId);
511
- },
512
- ownsPublicPath(pathname, deviceId) {
513
- const sessionId = sessionIdForPath(pathname);
514
- return [...leases.values()].some((lease) => (
515
- lease.deviceId === deviceId && lease.session.id === sessionId
516
- ));
517
- },
518
- resolvePublicRequest(pathname, deviceId, rawOrigin) {
519
- let origin;
520
- try { origin = normalizedOrigin(rawOrigin); } catch { return null; }
521
- const sessionId = sessionIdForPath(pathname);
522
- const lease = [...leases.values()].find((item) => (
523
- item.deviceId === deviceId
524
- && item.publicOrigin === origin
525
- && (!sessionId || item.session.id === sessionId)
526
- ));
527
- if (!lease) return null;
528
- touch(lease);
529
- return { port: lease.pool.ports[0], origin: lease.publicOrigin };
530
- },
531
- configureDeviceProfile(deviceId, prefs) {
532
- return cookieProfiles.configure(deviceId, prefs);
533
- },
534
- async clearDeviceProfile(deviceId, { origin } = {}) {
535
- await cookieProfiles.clear(deviceId, {
536
- hostname: origin === null ? undefined : new URL(origin).hostname,
537
- });
538
- await cookieProfiles.flush?.(deviceId);
539
- return { closedTabIds: [] };
540
- },
541
- async close() {
542
- if (closing) return;
543
- closing = true;
544
- for (const lease of [...leases.values()]) release(lease);
545
- await Promise.allSettled([...pendingPools.values(), ...leaseQueues.values()]);
546
- for (const lease of [...leases.values()]) release(lease);
547
- for (const pool of pools.values()) {
548
- if (pool.closeTimer != null) clearTimer(pool.closeTimer);
549
- pool.closeTimer = null;
550
- pool.proxy.close();
551
- }
552
- pools.clear();
553
- await cookieProfiles.close?.();
554
- },
555
- };
556
- }
@@ -1,17 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
-
3
- export function browserLabelForOrigin(rawOrigin) {
4
- const origin = new URL(rawOrigin).origin;
5
- const value = createHash('sha256').update(origin).digest().readBigUInt64BE(0);
6
- return `b-${value.toString(36).padStart(13, '0')}`;
7
- }
8
-
9
- export function claimPublicOrigin(claims, publicOrigin, targetOrigin) {
10
- const normalizedPublic = new URL(publicOrigin).origin;
11
- const normalizedTarget = new URL(targetOrigin).origin;
12
- const existing = claims.get(normalizedPublic);
13
- if (existing && existing !== normalizedTarget) {
14
- throw new Error('browser public origin collision');
15
- }
16
- claims.set(normalizedPublic, normalizedTarget);
17
- }