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,241 +0,0 @@
1
- import {
2
- createCipheriv,
3
- createDecipheriv,
4
- createHash,
5
- randomBytes as nodeRandomBytes,
6
- } from 'node:crypto';
7
- import defaultFs from 'node:fs/promises';
8
- import path from 'node:path';
9
-
10
- const AUTHENTICATION_ERROR = 'browser profile authentication failed';
11
-
12
- function profileHash(deviceId) {
13
- return createHash('sha256').update(deviceId).digest('hex');
14
- }
15
-
16
- function authenticationError() {
17
- return new Error(AUTHENTICATION_ERROR);
18
- }
19
-
20
- export function createBrowserProfilePersistence({
21
- dir,
22
- keyFile,
23
- fs = defaultFs,
24
- randomBytes = nodeRandomBytes,
25
- }) {
26
- let keyPromise = null;
27
- const pending = new Set();
28
-
29
- const ensureDir = async () => {
30
- await fs.mkdir(dir, { recursive: true, mode: 0o700 });
31
- await fs.chmod(dir, 0o700);
32
- };
33
-
34
- const loadKey = async () => {
35
- await ensureDir();
36
- try {
37
- await fs.chmod(keyFile, 0o600);
38
- const key = await fs.readFile(keyFile);
39
- if (key.length !== 32) throw authenticationError();
40
- return key;
41
- } catch (error) {
42
- if (error?.code !== 'ENOENT') throw error;
43
- }
44
-
45
- const key = randomBytes(32);
46
- let handle;
47
- try {
48
- handle = await fs.open(keyFile, 'wx', 0o600);
49
- await handle.writeFile(key);
50
- await handle.sync();
51
- await handle.close();
52
- handle = null;
53
- return key;
54
- } catch (error) {
55
- await handle?.close().catch(() => {});
56
- if (error?.code === 'EEXIST') {
57
- await fs.chmod(keyFile, 0o600);
58
- const existing = await fs.readFile(keyFile);
59
- if (existing.length !== 32) throw authenticationError();
60
- return existing;
61
- }
62
- await fs.unlink(keyFile).catch(() => {});
63
- throw error;
64
- }
65
- };
66
-
67
- const key = () => {
68
- if (!keyPromise) keyPromise = loadKey();
69
- return keyPromise;
70
- };
71
- const profilePath = (deviceId) => path.join(dir, `${profileHash(deviceId)}.profile`);
72
- const metadataPath = (deviceId) => path.join(dir, `${profileHash(deviceId)}.meta`);
73
-
74
- const track = (operation) => {
75
- pending.add(operation);
76
- operation.then(
77
- () => pending.delete(operation),
78
- () => pending.delete(operation),
79
- );
80
- return operation;
81
- };
82
-
83
- const read = async (deviceId) => {
84
- let raw;
85
- const target = profilePath(deviceId);
86
- await ensureDir();
87
- try {
88
- await fs.chmod(target, 0o600);
89
- raw = await fs.readFile(target, 'utf8');
90
- } catch (error) {
91
- if (error?.code === 'ENOENT') return null;
92
- throw error;
93
- }
94
-
95
- try {
96
- const deviceHash = profileHash(deviceId);
97
- const envelope = JSON.parse(raw);
98
- if (envelope?.v !== 1
99
- || typeof envelope.iv !== 'string'
100
- || typeof envelope.tag !== 'string'
101
- || typeof envelope.data !== 'string') {
102
- throw authenticationError();
103
- }
104
- const iv = Buffer.from(envelope.iv, 'base64');
105
- const tag = Buffer.from(envelope.tag, 'base64');
106
- if (iv.length !== 12 || tag.length !== 16) throw authenticationError();
107
- const decipher = createDecipheriv('aes-256-gcm', await key(), iv);
108
- decipher.setAAD(Buffer.from(`handmux-browser-profile:v1:${deviceHash}`));
109
- decipher.setAuthTag(tag);
110
- return Buffer.concat([
111
- decipher.update(Buffer.from(envelope.data, 'base64')),
112
- decipher.final(),
113
- ]).toString('utf8');
114
- } catch {
115
- throw authenticationError();
116
- }
117
- };
118
-
119
- const write = (deviceId, serializedJar) => track((async () => {
120
- const deviceHash = profileHash(deviceId);
121
- const iv = randomBytes(12);
122
- const cipher = createCipheriv('aes-256-gcm', await key(), iv);
123
- cipher.setAAD(Buffer.from(`handmux-browser-profile:v1:${deviceHash}`));
124
- const encrypted = Buffer.concat([
125
- cipher.update(serializedJar, 'utf8'),
126
- cipher.final(),
127
- ]);
128
- const envelope = {
129
- v: 1,
130
- iv: iv.toString('base64'),
131
- tag: cipher.getAuthTag().toString('base64'),
132
- data: encrypted.toString('base64'),
133
- };
134
- const target = profilePath(deviceId);
135
- const temp = `${target}.tmp-${process.pid}-${randomBytes(12).toString('hex')}`;
136
- let handle;
137
- try {
138
- handle = await fs.open(temp, 'wx', 0o600);
139
- await handle.writeFile(JSON.stringify(envelope), 'utf8');
140
- await handle.sync();
141
- await handle.close();
142
- handle = null;
143
- await fs.rename(temp, target);
144
- } catch (error) {
145
- await handle?.close().catch(() => {});
146
- await fs.unlink(temp).catch(() => {});
147
- throw error;
148
- }
149
- })());
150
-
151
- const remove = (deviceId) => track(
152
- fs.unlink(profilePath(deviceId)).catch((error) => {
153
- if (error?.code !== 'ENOENT') throw error;
154
- }),
155
- );
156
-
157
- const readMetadata = async (deviceId) => {
158
- const target = metadataPath(deviceId);
159
- await ensureDir();
160
- try {
161
- await fs.chmod(target, 0o600);
162
- const value = JSON.parse(await fs.readFile(target, 'utf8'));
163
- if (typeof value?.persist !== 'boolean'
164
- || ![1, 7, 30, null].includes(value?.retentionDays)
165
- || (value?.noLeaseSince !== null && !Number.isFinite(value?.noLeaseSince))) {
166
- throw new Error('invalid browser profile metadata');
167
- }
168
- return value;
169
- } catch (error) {
170
- if (error?.code === 'ENOENT') return null;
171
- throw error;
172
- }
173
- };
174
-
175
- const writeMetadata = (deviceId, metadata) => track((async () => {
176
- await ensureDir();
177
- const target = metadataPath(deviceId);
178
- const temp = `${target}.tmp-${process.pid}-${randomBytes(12).toString('hex')}`;
179
- let handle;
180
- try {
181
- handle = await fs.open(temp, 'wx', 0o600);
182
- await handle.writeFile(JSON.stringify(metadata), 'utf8');
183
- await handle.sync();
184
- await handle.close();
185
- handle = null;
186
- await fs.rename(temp, target);
187
- } catch (error) {
188
- await handle?.close().catch(() => {});
189
- await fs.unlink(temp).catch(() => {});
190
- throw error;
191
- }
192
- })());
193
-
194
- const removeMetadata = (deviceId) => track(
195
- fs.unlink(metadataPath(deviceId)).catch((error) => {
196
- if (error?.code !== 'ENOENT') throw error;
197
- }),
198
- );
199
-
200
- const pruneExpiredProfiles = (currentTime = Date.now()) => track((async () => {
201
- await ensureDir();
202
- const names = await fs.readdir(dir);
203
- let removed = 0;
204
- for (const name of names) {
205
- const match = name.match(/^([0-9a-f]{64})\.meta$/);
206
- if (!match) continue;
207
- const metadataFile = path.join(dir, name);
208
- let metadata;
209
- try {
210
- metadata = JSON.parse(await fs.readFile(metadataFile, 'utf8'));
211
- } catch {
212
- continue;
213
- }
214
- if (!metadata?.persist
215
- || metadata.retentionDays === null
216
- || ![1, 7, 30].includes(metadata.retentionDays)
217
- || !Number.isFinite(metadata.noLeaseSince)) continue;
218
- if (metadata.noLeaseSince + metadata.retentionDays * 24 * 60 * 60 * 1000 > currentTime) continue;
219
- await Promise.all([
220
- fs.unlink(path.join(dir, `${match[1]}.profile`)).catch((error) => {
221
- if (error?.code !== 'ENOENT') throw error;
222
- }),
223
- fs.unlink(metadataFile).catch((error) => {
224
- if (error?.code !== 'ENOENT') throw error;
225
- }),
226
- ]);
227
- removed += 1;
228
- }
229
- return removed;
230
- })());
231
-
232
- const close = async () => {
233
- const results = await Promise.allSettled([...pending]);
234
- const failed = results.find((result) => result.status === 'rejected');
235
- if (failed) throw failed.reason;
236
- };
237
-
238
- return {
239
- read, write, remove, readMetadata, writeMetadata, removeMetadata, pruneExpiredProfiles, close,
240
- };
241
- }
@@ -1,175 +0,0 @@
1
- import http from 'node:http';
2
- import net from 'node:net';
3
- import { isBrowserBootstrapPath } from './bootstrap.js';
4
- import { classifyIp } from './targetPolicy.js';
5
-
6
- const SERVICE_PATHS = new Set([
7
- '/hammerhead.js',
8
- '/task.js',
9
- '/iframe-task.js',
10
- '/messaging',
11
- '/transport-worker.js',
12
- '/worker-hammerhead.js',
13
- ]);
14
- const DEVICE_COOKIE = 'tw_browser_device';
15
-
16
- function sessionChannel(pathname) {
17
- const descriptor = String(pathname || '').split('/')[1] || '';
18
- return descriptor.match(/\*([A-Za-z0-9_-]{1,128})$/)?.[1] || null;
19
- }
20
-
21
- function sessionUnavailableHtml(pathname) {
22
- const channel = JSON.stringify(sessionChannel(pathname));
23
- return `<!doctype html><meta charset="utf-8">
24
- <script>parent.postMessage({source:'handmux-browser',channel:${channel},type:'session-unavailable'},'*')</script>
25
- <pre>{"error":"browser session unavailable"}</pre>`;
26
- }
27
-
28
- export function isBrowserServicePath(pathname) {
29
- return SERVICE_PATHS.has(String(pathname || '').split('?')[0]);
30
- }
31
-
32
- function cookieValue(raw, name) {
33
- for (const part of String(raw || '').split(';')) {
34
- const [key, ...rest] = part.trim().split('=');
35
- if (key === name) return rest.join('=');
36
- }
37
- return null;
38
- }
39
-
40
- function browserTarget(browser, req, deviceId) {
41
- const pathname = String(req.url || '').split('?')[0];
42
- if (!claimedBrowserRequest(req)) return null;
43
- if (typeof browser.resolvePublicRequest === 'function') {
44
- return browser.resolvePublicRequest(pathname, deviceId, browserRequestOrigin(req));
45
- }
46
- const allowed = isBrowserServicePath(pathname) ? browser.hasDevice(deviceId) : browser.ownsPublicPath(pathname, deviceId);
47
- return allowed ? { port: browser.internalPorts[0] } : null;
48
- }
49
-
50
- function isLoopback(address) {
51
- return classifyIp(address) === 'loopback';
52
- }
53
-
54
- export function browserRequestOrigin(req) {
55
- const forwardedProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim();
56
- const protocol = isLoopback(req.socket?.remoteAddress) && (forwardedProto === 'http' || forwardedProto === 'https')
57
- ? forwardedProto
58
- : (req.socket?.encrypted ? 'https' : 'http');
59
- const host = req.headers.host;
60
- if (!host) return null;
61
- try { return new URL(`${protocol}://${host}`).origin; } catch { return null; }
62
- }
63
-
64
- export function claimedBrowserRequest(req) {
65
- const pathname = String(req.url || '').split('?')[0];
66
- return isBrowserBootstrapPath(pathname)
67
- || isBrowserServicePath(pathname)
68
- || String(pathname).split('/')[1]?.startsWith('_browser-');
69
- }
70
-
71
- function filteredCookie(raw) {
72
- const values = String(raw || '')
73
- .split(';')
74
- .map((value) => value.trim())
75
- .filter(Boolean)
76
- .filter((value) => !value.startsWith('tw_preview=') && !value.startsWith(`${DEVICE_COOKIE}=`));
77
- return values.join('; ');
78
- }
79
-
80
- function expectsDocument(req) {
81
- const destination = String(req.headers['sec-fetch-dest'] || '').toLowerCase();
82
- const accept = String(req.headers.accept || '').toLowerCase();
83
- return destination === 'document' || destination === 'iframe' || accept.includes('text/html');
84
- }
85
-
86
- function upstreamHeaders(headers, port, token) {
87
- const out = { ...headers, host: `127.0.0.1:${port}` };
88
- if (token && out.authorization === `Bearer ${token}`) delete out.authorization;
89
- delete out['proxy-authorization'];
90
- if (out.cookie) {
91
- out.cookie = filteredCookie(out.cookie);
92
- if (!out.cookie) delete out.cookie;
93
- }
94
- return out;
95
- }
96
-
97
- export function createBrowserPublicProxy({
98
- browser,
99
- browserBootstrap,
100
- token,
101
- request = http.request,
102
- connect = net.connect,
103
- } = {}) {
104
- const handler = (req, res, next) => {
105
- if (!browser) return next();
106
- const pathname = String(req.url || '').split('?')[0];
107
- if (isBrowserBootstrapPath(pathname)) {
108
- const origin = browserRequestOrigin(req);
109
- const bootstrap = browserBootstrap?.consume(pathname, origin);
110
- if (!bootstrap) return res.status(403).json({ error: 'browser bootstrap unavailable' });
111
- const secure = origin?.startsWith('https://') ? '; Secure' : '';
112
- res.setHeader('Cache-Control', 'no-store');
113
- res.setHeader('Set-Cookie', `${DEVICE_COOKIE}=${bootstrap.deviceId}; Path=/; HttpOnly; SameSite=Strict${secure}`);
114
- return res.redirect(
115
- bootstrap.preserveMethod ? (bootstrap.redirectStatus || 307) : 302,
116
- bootstrap.url,
117
- );
118
- }
119
- const deviceId = cookieValue(req.headers.cookie, DEVICE_COOKIE);
120
- const target = browserTarget(browser, req, deviceId);
121
- if (!target) {
122
- if (claimedBrowserRequest(req)) {
123
- res.setHeader('Cache-Control', 'no-store');
124
- if (expectsDocument(req)) return res.status(403).type('html').send(sessionUnavailableHtml(pathname));
125
- return res.status(403).json({ error: 'browser session unavailable' });
126
- }
127
- return next();
128
- }
129
- const { port } = target;
130
- const upstream = request({
131
- hostname: '127.0.0.1',
132
- port,
133
- method: req.method,
134
- path: req.originalUrl || req.url,
135
- headers: upstreamHeaders(req.headers, port, token),
136
- }, (incoming) => {
137
- res.writeHead(incoming.statusCode || 502, incoming.headers);
138
- incoming.pipe(res);
139
- res.once('close', () => { if (!res.writableEnded) incoming.destroy(); });
140
- });
141
- const abort = () => upstream.destroy();
142
- req.once('aborted', abort);
143
- res.once('close', () => { if (!res.writableEnded) abort(); });
144
- upstream.on('error', () => {
145
- if (res.destroyed) return;
146
- if (!res.headersSent) res.status(502).json({ error: 'browser proxy unavailable' });
147
- else res.destroy();
148
- });
149
- req.pipe(upstream);
150
- };
151
-
152
- const onUpgrade = (req, socket, head) => {
153
- const deviceId = cookieValue(req.headers.cookie, DEVICE_COOKIE);
154
- const target = browser && browserTarget(browser, req, deviceId);
155
- if (!target) return false;
156
- const { port } = target;
157
- const upstream = connect({ host: '127.0.0.1', port });
158
- upstream.once('connect', () => {
159
- const headers = upstreamHeaders(req.headers, port, token);
160
- const lines = [`${req.method || 'GET'} ${req.url} HTTP/${req.httpVersion || '1.1'}`];
161
- for (const [name, value] of Object.entries(headers)) {
162
- if (Array.isArray(value)) for (const item of value) lines.push(`${name}: ${item}`);
163
- else if (value != null) lines.push(`${name}: ${value}`);
164
- }
165
- upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
166
- if (head?.length) upstream.write(head);
167
- socket.pipe(upstream).pipe(socket);
168
- });
169
- upstream.once('error', () => socket.destroy());
170
- socket.once('error', () => upstream.destroy());
171
- return true;
172
- };
173
-
174
- return { handler, onUpgrade };
175
- }
@@ -1,154 +0,0 @@
1
- import express from 'express';
2
- import { browserLabelForOrigin } from './originLabel.js';
3
- import { browserRequestOrigin } from './publicProxy.js';
4
- import { normalizeSiteVersion } from './siteVersion.js';
5
-
6
- const RETENTION_DAYS = new Set([1, 7, 30, null]);
7
- const DEVICE_ID = /^[A-Za-z0-9_-]{32,128}$/;
8
- const TAB_ID = /^[A-Za-z0-9_-]{1,128}$/;
9
- const DEVICE_COOKIE = 'tw_browser_device';
10
-
11
- function previewBase(raw) {
12
- if (!raw) return null;
13
- const value = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
14
- return new URL(value);
15
- }
16
-
17
- function wildcardOrigin(base, targetOrigin) {
18
- const url = new URL(base.origin);
19
- url.hostname = `${browserLabelForOrigin(targetOrigin)}.${base.hostname}`;
20
- return url.origin;
21
- }
22
-
23
- function normalizedTarget(value) {
24
- if (typeof value !== 'string' || !value) return null;
25
- try {
26
- const url = new URL(value);
27
- return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : null;
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- function normalizedHttpOrigin(value) {
34
- try {
35
- const url = new URL(value);
36
- return url.protocol === 'http:' || url.protocol === 'https:' ? url.origin : null;
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
- export function browserRoutes({
43
- browser,
44
- previewDomain = null,
45
- browserBootstrap = null,
46
- }) {
47
- const router = express.Router();
48
- const publicBase = previewBase(previewDomain);
49
-
50
- router.use((req, res, next) => {
51
- const deviceId = req.get('x-handmux-browser-device');
52
- if (!DEVICE_ID.test(deviceId || '')) return res.status(400).json({ error: 'browser device id required' });
53
- req.browserDeviceId = deviceId;
54
- const origin = browserRequestOrigin(req);
55
- const secure = origin?.startsWith('https://') ? '; Secure' : '';
56
- res.append('Set-Cookie', `${DEVICE_COOKIE}=${deviceId}; Path=/; HttpOnly; SameSite=Strict${secure}`);
57
- next();
58
- });
59
-
60
- const responseLease = (lease, deviceId) => {
61
- if (!lease) return null;
62
- const url = browserBootstrap.issue({
63
- url: lease.url,
64
- origin: new URL(lease.url).origin,
65
- deviceId,
66
- });
67
- return { ...lease, url };
68
- };
69
-
70
- router.put('/leases/:tabId', async (req, res, next) => {
71
- if (!browser || !publicBase) return res.status(503).json({ error: 'browser proxy unavailable' });
72
- if (!TAB_ID.test(req.params.tabId)) return res.status(400).json({ error: 'bad browser tab id' });
73
- const url = normalizedTarget(req.body?.url);
74
- if (!url) return res.status(400).json({ error: 'browser URL must use http or https' });
75
- const siteVersion = normalizeSiteVersion(req.body?.siteVersion);
76
- if (!siteVersion) return res.status(400).json({ error: 'bad browser site version' });
77
- try {
78
- const origin = wildcardOrigin(publicBase, new URL(url).origin);
79
- const lease = await browser.putLease({
80
- tabId: req.params.tabId,
81
- url: req.body.url,
82
- origin,
83
- deviceId: req.browserDeviceId,
84
- siteVersion,
85
- sourceUserAgent: req.get('user-agent') || '',
86
- });
87
- return res.json(responseLease(lease, req.browserDeviceId));
88
- } catch (error) {
89
- return next(error);
90
- }
91
- });
92
-
93
- router.post('/leases/:tabId/navigate', async (req, res, next) => {
94
- if (!browser || !publicBase) return res.status(503).json({ error: 'browser proxy unavailable' });
95
- if (!TAB_ID.test(req.params.tabId)) return res.status(400).json({ error: 'bad browser tab id' });
96
- const url = normalizedTarget(req.body?.url);
97
- if (!url) return res.status(400).json({ error: 'browser URL must use http or https' });
98
- const siteVersion = normalizeSiteVersion(req.body?.siteVersion);
99
- if (!siteVersion) return res.status(400).json({ error: 'bad browser site version' });
100
- try {
101
- const origin = wildcardOrigin(publicBase, new URL(url).origin);
102
- const lease = await browser.navigateLease(
103
- req.params.tabId,
104
- req.body.url,
105
- req.browserDeviceId,
106
- origin,
107
- siteVersion,
108
- req.get('user-agent') || '',
109
- );
110
- if (!lease) return res.status(404).json({ error: 'browser proxy lease not found' });
111
- return res.json(responseLease(lease, req.browserDeviceId));
112
- } catch (error) {
113
- return next(error);
114
- }
115
- });
116
-
117
- router.delete('/leases/:tabId', (req, res) => {
118
- if (!browser) return res.status(503).json({ error: 'browser proxy unavailable' });
119
- if (!browser.deleteLease(req.params.tabId, req.browserDeviceId)) {
120
- return res.status(404).json({ error: 'browser proxy lease not found' });
121
- }
122
- return res.status(204).end();
123
- });
124
-
125
- router.put('/profile', async (req, res, next) => {
126
- const { persist, retentionDays } = req.body || {};
127
- if (typeof persist !== 'boolean' || !RETENTION_DAYS.has(retentionDays)) {
128
- return res.status(400).json({ error: 'bad browser profile preferences' });
129
- }
130
- try {
131
- return res.json(await browser.configureDeviceProfile(
132
- req.browserDeviceId,
133
- { persist, retentionDays },
134
- ));
135
- } catch (error) {
136
- return next(error);
137
- }
138
- });
139
-
140
- router.post('/profile/clear', async (req, res, next) => {
141
- const rawOrigin = req.body?.origin;
142
- const origin = rawOrigin === null ? null : normalizedHttpOrigin(rawOrigin);
143
- if (origin === null && rawOrigin !== null) {
144
- return res.status(400).json({ error: 'bad browser profile clear request' });
145
- }
146
- try {
147
- return res.json(await browser.clearDeviceProfile(req.browserDeviceId, { origin }));
148
- } catch (error) {
149
- return next(error);
150
- }
151
- });
152
-
153
- return router;
154
- }
@@ -1,78 +0,0 @@
1
- export function createBrowserSessionStore({
2
- now = Date.now,
3
- setTimer = setTimeout,
4
- clearTimer = clearTimeout,
5
- onExpire = () => {},
6
- } = {}) {
7
- const tabs = new Map();
8
-
9
- const cancel = (tab) => {
10
- if (tab.timer != null) clearTimer(tab.timer);
11
- tab.timer = null;
12
- tab.hiddenAt = null;
13
- tab.expiresAt = null;
14
- };
15
-
16
- const remove = (id) => {
17
- const tab = tabs.get(id);
18
- if (!tab) return null;
19
- if (tab.timer != null) clearTimer(tab.timer);
20
- tabs.delete(id);
21
- const { timer: _timer, ...out } = tab;
22
- return out;
23
- };
24
-
25
- const view = (tab) => {
26
- if (!tab) return null;
27
- const { timer: _timer, ...out } = tab;
28
- return out;
29
- };
30
-
31
- return {
32
- add(input) {
33
- if (tabs.has(input.id)) throw new Error(`browser tab already exists: ${input.id}`);
34
- const mode = input.mode === 'direct' ? 'direct' : 'proxy';
35
- const tab = { ...input, mode, visible: true, hiddenAt: null, expiresAt: null, timer: null };
36
- tabs.set(tab.id, tab);
37
- return view(tab);
38
- },
39
-
40
- get(id) { return view(tabs.get(id)); },
41
-
42
- list() { return [...tabs.values()].map(view); },
43
-
44
- update(id, patch) {
45
- const tab = tabs.get(id);
46
- if (!tab) return null;
47
- Object.assign(tab, patch);
48
- return view(tab);
49
- },
50
-
51
- setVisible(id, visible, closeAfterMinutes) {
52
- const tab = tabs.get(id);
53
- if (!tab) return null;
54
- cancel(tab);
55
- tab.visible = !!visible;
56
- if (!tab.visible) {
57
- tab.hiddenAt = now();
58
- if (closeAfterMinutes != null) {
59
- const delay = closeAfterMinutes * 60_000;
60
- tab.expiresAt = tab.hiddenAt + delay;
61
- tab.timer = setTimer(() => {
62
- const expired = remove(id);
63
- if (expired) onExpire(expired, 'expired');
64
- }, delay);
65
- }
66
- }
67
- return view(tab);
68
- },
69
-
70
- remove,
71
-
72
- close() {
73
- const removed = [];
74
- for (const id of [...tabs.keys()]) removed.push(remove(id));
75
- return removed;
76
- },
77
- };
78
- }