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