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