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,16 +0,0 @@
1
- import { createHmac } from 'node:crypto';
2
-
3
- // Build a fully-signed iFlytek IAT v2 WebSocket URL. The secret (apiSecret) is used only here on the
4
- // server — the browser receives only the resulting URL (HMAC output, not the key). `host`/`date` are
5
- // injectable so this is deterministic and unit-testable; production passes host='iat-api.xfyun.cn'
6
- // and date=new Date().toUTCString() (RFC1123 GMT, iFlytek allows ±300s skew).
7
- export function buildIatSignedUrl({ appId, apiKey, apiSecret, host = 'iat-api.xfyun.cn', date }) {
8
- const requestLine = 'GET /v2/iat HTTP/1.1';
9
- const signatureOrigin = `host: ${host}\ndate: ${date}\n${requestLine}`;
10
- const signature = createHmac('sha256', apiSecret).update(signatureOrigin).digest('base64');
11
- const authorizationOrigin =
12
- `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
13
- const authorization = Buffer.from(authorizationOrigin, 'utf8').toString('base64');
14
- const qs = new URLSearchParams({ authorization, date, host });
15
- return { url: `wss://${host}/v2/iat?${qs.toString()}`, appId };
16
- }
package/src/auth.js DELETED
@@ -1,30 +0,0 @@
1
- import crypto from 'node:crypto';
2
-
3
- export function loadToken(env = process.env) {
4
- let token = env.HANDMUX_TOKEN;
5
- if (!token) {
6
- token = crypto.randomBytes(24).toString('base64url');
7
- console.log(`[handmux] no HANDMUX_TOKEN set; generated token: ${token}`);
8
- }
9
- return token;
10
- }
11
-
12
- const sha = (s) => crypto.createHash('sha256').update(String(s)).digest();
13
-
14
- export function tokenEquals(a, b) {
15
- return crypto.timingSafeEqual(sha(a), sha(b));
16
- }
17
-
18
- export function bearerFrom(header) {
19
- if (!header) return null;
20
- const m = /^Bearer (.+)$/.exec(header);
21
- return m ? m[1] : null;
22
- }
23
-
24
- export function expressAuth(token) {
25
- return (req, res, next) => {
26
- const provided = bearerFrom(req.get('authorization'));
27
- if (provided && tokenEquals(provided, token)) return next();
28
- res.status(401).json({ error: 'unauthorized' });
29
- };
30
- }
@@ -1,68 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
-
3
- const BOOTSTRAP_PREFIX = '/_browser-bootstrap/';
4
-
5
- function normalizedOrigin(raw) {
6
- const url = new URL(raw);
7
- if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('browser bootstrap origin must use http or https');
8
- return url.origin;
9
- }
10
-
11
- export function isBrowserBootstrapPath(pathname) {
12
- return String(pathname || '').startsWith(BOOTSTRAP_PREFIX);
13
- }
14
-
15
- export function createBrowserBootstrapStore({
16
- randomToken = () => randomBytes(24).toString('base64url'),
17
- now = Date.now,
18
- ttlMs = 60_000,
19
- } = {}) {
20
- const tickets = new Map();
21
-
22
- const prune = () => {
23
- const current = now();
24
- for (const [token, entry] of tickets) if (entry.expiresAt <= current) tickets.delete(token);
25
- };
26
-
27
- return {
28
- issue({ url, origin, deviceId, preserveMethod = false, redirectStatus = 302 }) {
29
- prune();
30
- const expectedOrigin = normalizedOrigin(origin);
31
- const target = new URL(url);
32
- if (target.origin !== expectedOrigin || !target.pathname.split('/')[1]?.startsWith('_browser-')) {
33
- throw new Error('browser bootstrap target must be a session on its public origin');
34
- }
35
- const token = randomToken();
36
- tickets.set(token, {
37
- url: target.toString(),
38
- origin: expectedOrigin,
39
- deviceId,
40
- preserveMethod,
41
- redirectStatus,
42
- expiresAt: now() + ttlMs,
43
- });
44
- return new URL(`${BOOTSTRAP_PREFIX}${encodeURIComponent(token)}`, expectedOrigin).toString();
45
- },
46
-
47
- consume(pathname, origin) {
48
- prune();
49
- if (!isBrowserBootstrapPath(pathname)) return null;
50
- let token;
51
- try { token = decodeURIComponent(String(pathname).slice(BOOTSTRAP_PREFIX.length)); } catch { return null; }
52
- const entry = tickets.get(token);
53
- let requestedOrigin;
54
- try { requestedOrigin = normalizedOrigin(origin); } catch { return null; }
55
- if (!entry || entry.origin !== requestedOrigin) return null;
56
- tickets.delete(token);
57
- const consumed = {
58
- url: entry.url,
59
- deviceId: entry.deviceId,
60
- };
61
- if (entry.preserveMethod) {
62
- consumed.preserveMethod = true;
63
- consumed.redirectStatus = entry.redirectStatus;
64
- }
65
- return consumed;
66
- },
67
- };
68
- }
@@ -1,403 +0,0 @@
1
- const MUTATION_METHODS = ['setByServer', 'setByClient', 'setCookies', 'deleteCookies'];
2
- const DAY = 24 * 60 * 60 * 1000;
3
- const MAX_TIMER_DELAY = 2_147_483_647;
4
-
5
- const noPersistence = {
6
- async read() { return null; },
7
- async write() {},
8
- async remove() {},
9
- async readMetadata() { return null; },
10
- async writeMetadata() {},
11
- async removeMetadata() {},
12
- async close() {},
13
- };
14
-
15
- export function createDeviceCookieProfiles({
16
- createCookies,
17
- onMutation = () => {},
18
- persistence = noPersistence,
19
- now = Date.now,
20
- setTimer = setTimeout,
21
- clearTimer = clearTimeout,
22
- }) {
23
- const profiles = new Map();
24
- const pending = new Set();
25
- let closing = false;
26
-
27
- const track = (operation) => {
28
- pending.add(operation);
29
- operation.then(() => pending.delete(operation), () => pending.delete(operation));
30
- return operation;
31
- };
32
-
33
- const profileFor = (deviceId) => {
34
- let profile = profiles.get(deviceId);
35
- if (!profile) {
36
- profile = {
37
- cookies: createCookies(),
38
- attached: new Set(),
39
- persist: false,
40
- retentionDays: 30,
41
- loaded: false,
42
- used: false,
43
- active: false,
44
- idleSince: null,
45
- retentionTimer: null,
46
- flushTimer: null,
47
- dirty: false,
48
- operationPromise: Promise.resolve(),
49
- configurePromise: Promise.resolve(),
50
- useVersion: 0,
51
- retentionEpoch: 0,
52
- };
53
- profiles.set(deviceId, profile);
54
- }
55
- return profile;
56
- };
57
-
58
- const installCookies = (profile, cookies) => {
59
- profile.cookies = cookies;
60
- for (const attached of profile.attached) {
61
- attached._cookieJar = cookies._cookieJar;
62
- attached._pendingSyncCookies = [];
63
- }
64
- };
65
-
66
- const replaceJar = (profile, serialized) => {
67
- profile.cookies.setJar(serialized);
68
- for (const cookies of profile.attached) {
69
- cookies._cookieJar = profile.cookies._cookieJar;
70
- cookies._pendingSyncCookies = [];
71
- }
72
- };
73
-
74
- const queueOperation = (profile, operation) => {
75
- const result = profile.operationPromise.then(operation);
76
- profile.operationPromise = result.catch(() => {});
77
- return track(result);
78
- };
79
-
80
- const queueWrite = (deviceId, profile, serialized = profile.cookies.serializeJar()) => (
81
- queueOperation(profile, () => persistence.write(deviceId, serialized))
82
- );
83
- const queueMetadata = (deviceId, profile) => queueOperation(
84
- profile,
85
- () => persistence.writeMetadata?.(deviceId, {
86
- persist: profile.persist,
87
- retentionDays: profile.retentionDays,
88
- noLeaseSince: profile.idleSince,
89
- }),
90
- );
91
-
92
- const flush = async (deviceId) => {
93
- const profile = profiles.get(deviceId);
94
- if (!profile || !profile.persist) return;
95
- if (!profile.dirty) {
96
- await profile.operationPromise;
97
- return;
98
- }
99
- if (profile.flushTimer !== null) {
100
- clearTimer(profile.flushTimer);
101
- profile.flushTimer = null;
102
- }
103
- profile.dirty = false;
104
- try {
105
- await queueWrite(deviceId, profile);
106
- } catch (error) {
107
- profile.dirty = true;
108
- throw error;
109
- }
110
- };
111
-
112
- const markDirty = (deviceId) => {
113
- const profile = profileFor(deviceId);
114
- profile.used = true;
115
- profile.useVersion += 1;
116
- onMutation(deviceId);
117
- if (!profile.persist) return;
118
- profile.dirty = true;
119
- if (closing || profile.flushTimer !== null) return;
120
- profile.flushTimer = setTimer(() => {
121
- profile.flushTimer = null;
122
- void flush(deviceId).catch(() => {});
123
- }, 500);
124
- };
125
-
126
- const clearRetentionTimer = (profile) => {
127
- if (profile.retentionTimer === null) return;
128
- clearTimer(profile.retentionTimer);
129
- profile.retentionTimer = null;
130
- };
131
-
132
- const clearExpired = async (deviceId, profile, expectedEpoch, expectedIdleSince) => {
133
- clearRetentionTimer(profile);
134
- if (profile.dirty) await flush(deviceId);
135
- else await profile.operationPromise;
136
- if (profile.active
137
- || profile.retentionEpoch !== expectedEpoch
138
- || profile.idleSince !== expectedIdleSince) return;
139
- installCookies(profile, createCookies());
140
- profile.used = false;
141
- profile.useVersion += 1;
142
- profile.idleSince = null;
143
- profile.dirty = false;
144
- await queueOperation(profile, () => persistence.remove(deviceId));
145
- };
146
-
147
- const scheduleRetention = (deviceId, profile) => {
148
- clearRetentionTimer(profile);
149
- if (closing || profile.active || profile.idleSince === null || profile.retentionDays === null) return null;
150
- const remaining = profile.idleSince + profile.retentionDays * DAY - now();
151
- if (remaining <= 0) {
152
- return track(clearExpired(
153
- deviceId,
154
- profile,
155
- profile.retentionEpoch,
156
- profile.idleSince,
157
- ));
158
- }
159
- profile.retentionTimer = setTimer(() => {
160
- profile.retentionTimer = null;
161
- const operation = scheduleRetention(deviceId, profile);
162
- if (operation) void operation.catch(() => {});
163
- }, Math.min(remaining, MAX_TIMER_DELAY));
164
- return null;
165
- };
166
-
167
- const saveCurrent = async (deviceId, profile) => {
168
- if (profile.flushTimer !== null) {
169
- clearTimer(profile.flushTimer);
170
- profile.flushTimer = null;
171
- }
172
- profile.dirty = false;
173
- await queueWrite(deviceId, profile);
174
- };
175
-
176
- const configureImpl = async (deviceId, prefs) => {
177
- if (typeof prefs?.persist !== 'boolean' || ![1, 7, 30, null].includes(prefs?.retentionDays)) {
178
- throw new Error('invalid browser profile preferences');
179
- }
180
- const profile = profileFor(deviceId);
181
- const wasPersisting = profile.persist;
182
- profile.retentionEpoch += 1;
183
- let warning = null;
184
-
185
- if (prefs.persist && !wasPersisting) {
186
- if (!profile.loaded) {
187
- const metadata = await persistence.readMetadata?.(deviceId);
188
- if (metadata?.persist) profile.idleSince = metadata.noLeaseSince;
189
- }
190
- profile.persist = true;
191
- profile.retentionDays = prefs.retentionDays;
192
- if (!profile.loaded && !profile.used) {
193
- profile.loaded = true;
194
- const useVersion = profile.useVersion;
195
- try {
196
- const serialized = await persistence.read(deviceId);
197
- if (profile.used || profile.useVersion !== useVersion) {
198
- await saveCurrent(deviceId, profile);
199
- } else if (serialized !== null) {
200
- replaceJar(profile, serialized);
201
- }
202
- } catch {
203
- warning = 'profile-recovery-failed';
204
- await queueOperation(profile, () => persistence.remove(deviceId));
205
- if (profile.used || profile.useVersion !== useVersion) {
206
- await saveCurrent(deviceId, profile);
207
- } else {
208
- installCookies(profile, createCookies());
209
- profile.used = false;
210
- profile.useVersion += 1;
211
- }
212
- }
213
- } else {
214
- profile.loaded = true;
215
- await saveCurrent(deviceId, profile);
216
- }
217
- } else if (!prefs.persist && wasPersisting) {
218
- const wasDirty = profile.dirty;
219
- if (profile.flushTimer !== null) {
220
- clearTimer(profile.flushTimer);
221
- profile.flushTimer = null;
222
- }
223
- profile.dirty = false;
224
- try {
225
- await queueOperation(profile, () => persistence.remove(deviceId));
226
- } catch (error) {
227
- profile.dirty ||= wasDirty;
228
- const retention = scheduleRetention(deviceId, profile);
229
- if (retention) await retention;
230
- throw error;
231
- }
232
- if (profile.flushTimer !== null) {
233
- clearTimer(profile.flushTimer);
234
- profile.flushTimer = null;
235
- }
236
- profile.dirty = false;
237
- profile.persist = false;
238
- profile.retentionDays = prefs.retentionDays;
239
- await queueOperation(profile, () => persistence.removeMetadata?.(deviceId));
240
- } else {
241
- profile.persist = prefs.persist;
242
- profile.retentionDays = prefs.retentionDays;
243
- }
244
-
245
- if (profile.persist) await queueMetadata(deviceId, profile);
246
- const retention = scheduleRetention(deviceId, profile);
247
- if (retention) await retention;
248
- return { persist: profile.persist, retentionDays: profile.retentionDays, warning };
249
- };
250
-
251
- const configure = (deviceId, prefs) => {
252
- const profile = profileFor(deviceId);
253
- // Preference changes span reads, jar restoration, disk removal, and metadata writes. Serialize that
254
- // whole transaction per device; serializing only the individual file operations lets an older enable
255
- // finish after a newer disable and silently turn persistence back on.
256
- const result = profile.configurePromise.catch(() => {}).then(() => configureImpl(deviceId, prefs));
257
- profile.configurePromise = result.catch(() => {});
258
- return track(result);
259
- };
260
-
261
- const setActive = (deviceId, active) => {
262
- const profile = profiles.get(deviceId);
263
- if (!profile) return;
264
- profile.retentionEpoch += 1;
265
- if (active) {
266
- profile.active = true;
267
- profile.idleSince = null;
268
- clearRetentionTimer(profile);
269
- if (profile.persist) void queueMetadata(deviceId, profile).catch(() => {});
270
- return;
271
- }
272
- if (profile.active || profile.idleSince === null) profile.idleSince = now();
273
- profile.active = false;
274
- if (profile.persist) void queueMetadata(deviceId, profile).catch(() => {});
275
- const retention = scheduleRetention(deviceId, profile);
276
- if (retention) void retention.catch(() => {});
277
- };
278
-
279
- const attach = (deviceId, sessionCookies) => {
280
- const profile = profileFor(deviceId);
281
- if (!profile.cookies?._cookieJar || !sessionCookies?._cookieJar) {
282
- throw new Error('browser cookie profile unsupported');
283
- }
284
- profile.used = true;
285
- profile.useVersion += 1;
286
- sessionCookies._cookieJar = profile.cookies._cookieJar;
287
- sessionCookies._pendingSyncCookies = [];
288
- profile.attached.add(sessionCookies);
289
-
290
- const originals = new Map();
291
- for (const name of MUTATION_METHODS) {
292
- const original = sessionCookies[name];
293
- originals.set(name, {
294
- hadOwn: Object.prototype.hasOwnProperty.call(sessionCookies, name),
295
- value: original,
296
- });
297
- sessionCookies[name] = (...args) => {
298
- const result = original.apply(sessionCookies, args);
299
- markDirty(deviceId);
300
- return result;
301
- };
302
- }
303
-
304
- let attached = true;
305
- return () => {
306
- if (!attached) return;
307
- attached = false;
308
- profile.attached.delete(sessionCookies);
309
- for (const [name, original] of originals) {
310
- if (original.hadOwn) sessionCookies[name] = original.value;
311
- else delete sessionCookies[name];
312
- }
313
- };
314
- };
315
-
316
- const serialize = (deviceId) => {
317
- const profile = profiles.get(deviceId);
318
- if (!profile) return null;
319
- profile.used = true;
320
- profile.useVersion += 1;
321
- return profile.cookies.serializeJar();
322
- };
323
-
324
- const clear = (deviceId, { url, hostname } = {}) => {
325
- const fullClear = !hostname && !url;
326
- const profile = profiles.get(deviceId) || (fullClear ? profileFor(deviceId) : null);
327
- if (!profile) return { cleared: false };
328
- profile.used = true;
329
- profile.useVersion += 1;
330
- let targetHostname = hostname;
331
- if (!targetHostname && url) {
332
- try { targetHostname = new URL(url).hostname; } catch { return { cleared: false }; }
333
- }
334
- if (!targetHostname) {
335
- replaceJar(profile, null);
336
- onMutation(deviceId);
337
- profile.dirty = false;
338
- if (profile.flushTimer !== null) {
339
- clearTimer(profile.flushTimer);
340
- profile.flushTimer = null;
341
- }
342
- return queueOperation(profile, () => persistence.remove(deviceId))
343
- .then(() => ({ cleared: true }));
344
- }
345
-
346
- const normalizedHostname = targetHostname.toLowerCase();
347
- const internalCookies = profile.cookies._getAllCookiesSync().filter((cookie) => {
348
- const domain = cookie.domain?.replace(/^\./, '').toLowerCase();
349
- if (!domain) return false;
350
- if (cookie.hostOnly) return domain === normalizedHostname;
351
- return normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`);
352
- });
353
- if (!internalCookies.length) return { cleared: false };
354
- const matches = profile.cookies._convertToExternalCookies(internalCookies);
355
- for (let index = 0; index < matches.length; index++) {
356
- matches[index].expires = internalCookies[index].expires;
357
- matches[index].maxAge = internalCookies[index].maxAge;
358
- matches[index].sameSite = internalCookies[index].sameSite;
359
- }
360
- profile.cookies.deleteCookies(matches);
361
- markDirty(deviceId);
362
- return { cleared: true };
363
- };
364
-
365
- const remove = (deviceId) => {
366
- const profile = profiles.get(deviceId);
367
- if (!profile) return false;
368
- clearRetentionTimer(profile);
369
- if (profile.flushTimer !== null) clearTimer(profile.flushTimer);
370
- profiles.delete(deviceId);
371
- if (profile.persist) {
372
- queueOperation(profile, () => persistence.remove(deviceId));
373
- queueOperation(profile, () => persistence.removeMetadata?.(deviceId));
374
- }
375
- return true;
376
- };
377
- const has = (deviceId) => profiles.has(deviceId);
378
-
379
- const close = async () => {
380
- closing = true;
381
- for (const profile of profiles.values()) clearRetentionTimer(profile);
382
- while (true) {
383
- const flushes = [];
384
- for (const [deviceId, profile] of profiles) {
385
- if (profile.dirty) flushes.push(flush(deviceId));
386
- else if (profile.flushTimer !== null) {
387
- clearTimer(profile.flushTimer);
388
- profile.flushTimer = null;
389
- }
390
- }
391
- await Promise.all(flushes);
392
- const operations = [...pending];
393
- if (!operations.length) break;
394
- await Promise.all(operations);
395
- }
396
- for (const profile of profiles.values()) clearRetentionTimer(profile);
397
- await persistence.close();
398
- };
399
-
400
- return {
401
- attach, serialize, clear, remove, has, configure, setActive, flush, close,
402
- };
403
- }
@@ -1,47 +0,0 @@
1
- const DEVICE_ID = /^[A-Za-z0-9_-]{32,128}$/;
2
-
3
- function jsonBody(response) {
4
- if (!response?.body?.length) return null;
5
- try { return JSON.parse(response.body.toString('utf8')); } catch { return null; }
6
- }
7
-
8
- function sendProxy(res, response, generation, stampGeneration) {
9
- if (!response) return res.status(503).json({ error: 'browser unavailable' });
10
- for (const [name, value] of Object.entries(response.headers || {})) {
11
- if (value != null && !['connection', 'content-length', 'transfer-encoding'].includes(name.toLowerCase())) {
12
- res.setHeader(name, value);
13
- }
14
- }
15
- if (stampGeneration && response.status >= 200 && response.status < 300) {
16
- const payload = jsonBody(response);
17
- if (payload) return res.status(response.status).json({ ...payload, generation });
18
- }
19
- return res.status(response.status).send(response.body);
20
- }
21
-
22
- export function createBrowserCoordinator({
23
- proxyRequest,
24
- getStatus = () => ({ ready: false, generation: 0 }),
25
- } = {}) {
26
- const handler = async (req, res) => {
27
- if (req.method === 'GET' && req.path === '/status') {
28
- return res.json(getStatus());
29
- }
30
- const deviceId = req.get('x-handmux-browser-device');
31
- if (!DEVICE_ID.test(deviceId || '')) {
32
- return res.status(400).json({ error: 'browser device id required' });
33
- }
34
- const path = `/api/browser-proxy${req.path}`;
35
- const response = await proxyRequest({
36
- req,
37
- method: req.method,
38
- path,
39
- body: ['PUT', 'POST', 'PATCH'].includes(req.method) ? req.body : undefined,
40
- });
41
- const stampGeneration = /^\/leases\/[^/]+(?:\/navigate)?$/.test(req.path)
42
- && ['PUT', 'POST'].includes(req.method);
43
- return sendProxy(res, response, getStatus().generation, stampGeneration);
44
- };
45
- handler.close = () => {};
46
- return handler;
47
- }
@@ -1,86 +0,0 @@
1
- import { createRequire } from 'node:module';
2
-
3
- const require = createRequire(import.meta.url);
4
- const PATCHED = Symbol.for('handmux.hammerhead.websocket-upgrade-socket');
5
- const RESPONDER_PATCHED = Symbol.for('handmux.hammerhead.websocket-mock-response');
6
- const REQUEST_INFO_PATCHED = Symbol.for('handmux.hammerhead.request-info-frame-nature');
7
-
8
- export function patchHammerheadDestinationRequest(DestinationRequest) {
9
- const prototype = DestinationRequest?.prototype;
10
- const original = prototype?._onUpgrade;
11
- if (typeof original !== 'function' || original.length !== 3) {
12
- throw new Error('unsupported Hammerhead DestinationRequest._onUpgrade signature');
13
- }
14
- if (original[PATCHED]) return false;
15
-
16
- function onUpgrade(response, socket, head) {
17
- if (response) response.socket ??= socket;
18
- return original.call(this, response, socket, head);
19
- }
20
- Object.defineProperty(onUpgrade, PATCHED, { value: true });
21
- prototype._onUpgrade = onUpgrade;
22
- return true;
23
- }
24
-
25
- export function patchHammerheadWebSocketResponder(websocket, headerTransforms) {
26
- const original = websocket?.respondOnWebSocket;
27
- if (typeof original !== 'function') {
28
- throw new Error('unsupported Hammerhead WebSocket responder');
29
- }
30
- if (original[RESPONDER_PATCHED]) return false;
31
-
32
- function respondOnWebSocket(ctx) {
33
- if (ctx?.destRes?.socket) return original(ctx);
34
- const response = ctx?.res;
35
- const destination = ctx?.destRes;
36
- if (!destination || typeof response?.end !== 'function') return original(ctx);
37
-
38
- const headers = headerTransforms.forResponse(ctx);
39
- const missingUpgradeSocket = destination.statusCode === 101;
40
- const statusCode = missingUpgradeSocket ? 502 : destination.statusCode || 502;
41
- const statusMessage = missingUpgradeSocket ? 'Bad Gateway' : destination.statusMessage || 'Bad Gateway';
42
- const lines = [`HTTP/${destination.httpVersion || '1.1'} ${statusCode} ${statusMessage}`];
43
- for (const [name, value] of Object.entries(headers)) {
44
- if (Array.isArray(value)) value.forEach((item) => lines.push(`${name}: ${item}`));
45
- else if (value != null) lines.push(`${name}: ${value}`);
46
- }
47
- if (!Object.keys(headers).some((name) => name.toLowerCase() === 'connection')) {
48
- lines.push('connection: close');
49
- }
50
- response.end(`${lines.join('\r\n')}\r\n\r\n`);
51
- return undefined;
52
- }
53
- Object.defineProperty(respondOnWebSocket, RESPONDER_PATCHED, { value: true });
54
- websocket.respondOnWebSocket = respondOnWebSocket;
55
- return true;
56
- }
57
-
58
- export function patchHammerheadRequestInfo(RequestInfo) {
59
- const original = RequestInfo?.from;
60
- if (typeof original !== 'function') {
61
- throw new Error('unsupported Hammerhead RequestInfo.from');
62
- }
63
- if (original[REQUEST_INFO_PATCHED]) return false;
64
-
65
- function from(ctx) {
66
- const info = original.call(this, ctx);
67
- info.isIframe = !!ctx?.isIframe;
68
- return info;
69
- }
70
- Object.defineProperty(from, REQUEST_INFO_PATCHED, { value: true });
71
- RequestInfo.from = from;
72
- return true;
73
- }
74
-
75
- export function installHammerheadWebSocketUpgradeCompat() {
76
- const DestinationRequest = require('testcafe-hammerhead/lib/request-pipeline/destination-request');
77
- const websocket = require('testcafe-hammerhead/lib/request-pipeline/websocket');
78
- const headerTransforms = require('testcafe-hammerhead/lib/request-pipeline/header-transforms');
79
- const { RequestInfo } = require(
80
- 'testcafe-hammerhead/lib/request-pipeline/request-hooks/events/info',
81
- );
82
- const destinationPatched = patchHammerheadDestinationRequest(DestinationRequest);
83
- const responderPatched = patchHammerheadWebSocketResponder(websocket, headerTransforms);
84
- const requestInfoPatched = patchHammerheadRequestInfo(RequestInfo);
85
- return destinationPatched || responderPatched || requestInfoPatched;
86
- }
@@ -1,38 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { randomBytes } from 'node:crypto';
3
-
4
- const require = createRequire(import.meta.url);
5
- const MARKER = 'x-handmux-browser-rebind';
6
- const MARKER_SECRET = randomBytes(32).toString('base64url');
7
- const PATCHED = Symbol.for('handmux.hammerhead.raw-rebind-location');
8
-
9
- export function hammerheadRebindHeaders(location) {
10
- return { location, [MARKER]: MARKER_SECRET };
11
- }
12
-
13
- export function patchHammerheadRebindLocation(
14
- responseTransforms,
15
- markerSecret = MARKER_SECRET,
16
- ) {
17
- const original = responseTransforms?.location;
18
- if (typeof original !== 'function') throw new Error('unsupported Hammerhead location transform');
19
- if (original[PATCHED]) return false;
20
-
21
- function location(src, ctx) {
22
- if (ctx?.destRes?.headers?.[MARKER] === markerSecret) return src;
23
- return original(src, ctx);
24
- }
25
- Object.defineProperty(location, PATCHED, { value: true });
26
- responseTransforms.location = location;
27
- responseTransforms[MARKER] = () => undefined;
28
- return true;
29
- }
30
-
31
- export function installHammerheadRebindLocationCompat() {
32
- const { responseTransforms } = require(
33
- 'testcafe-hammerhead/lib/request-pipeline/header-transforms/transforms',
34
- );
35
- return patchHammerheadRebindLocation(responseTransforms, MARKER_SECRET);
36
- }
37
-
38
- export { MARKER as HAMMERHEAD_REBIND_HEADER };