stageflow 0.32.0 → 0.35.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 +4 -3
- package/dist/agent/adapterSpill.d.ts +14 -0
- package/dist/agent/adapterSpill.js +83 -0
- package/dist/agent/agentBackend.d.ts +2 -2
- package/dist/agent/agentBackend.js +2 -2
- package/dist/agent/claudeAdapter.d.ts +13 -2
- package/dist/agent/claudeAdapter.js +50 -6
- package/dist/agent/cursorProvider.d.ts +3 -0
- package/dist/agent/cursorProvider.js +23 -6
- package/dist/agent/fakeAgent.d.ts +3 -3
- package/dist/agent/fakeAgent.js +0 -6
- package/dist/agent/piAdapter.d.ts +2 -2
- package/dist/agent/piAdapter.js +116 -15
- package/dist/agent/piIsolatedMcp.d.ts +1 -0
- package/dist/agent/piIsolatedMcp.js +19 -1
- package/dist/agent/piIsolatedMcpProbe.d.ts +11 -0
- package/dist/agent/piIsolatedMcpProbe.js +20 -0
- package/dist/agent/port.d.ts +2 -2
- package/dist/agent/providerAuth.d.ts +6 -0
- package/dist/agent/providerAuth.js +86 -17
- package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -6
- package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +13 -9
- package/dist/agent/stageTurnDriver.d.ts +33 -0
- package/dist/agent/stageTurnDriver.js +64 -0
- package/dist/agent/toolOutputBudget.d.ts +46 -0
- package/dist/agent/toolOutputBudget.js +97 -0
- package/dist/agent/toolOutputBudgetExtension.d.ts +22 -0
- package/dist/agent/toolOutputBudgetExtension.js +141 -0
- package/dist/agent/toolOutputSpill.d.ts +34 -0
- package/dist/agent/toolOutputSpill.js +85 -0
- package/dist/browser/agentBrowserLiveViewRelay.d.ts +41 -0
- package/dist/browser/agentBrowserLiveViewRelay.js +801 -0
- package/dist/browser/anchor.d.ts +2 -0
- package/dist/browser/anchor.js +2 -1
- package/dist/browser/auditSink.d.ts +8 -0
- package/dist/browser/browserHost.d.ts +29 -0
- package/dist/browser/browserSweep.d.ts +4 -1
- package/dist/browser/browserSweep.js +7 -0
- package/dist/browser/browserTeardown.d.ts +1 -0
- package/dist/browser/browserTeardown.js +32 -2
- package/dist/browser/containerBrowserEndpoint.d.ts +11 -0
- package/dist/browser/containerBrowserEndpoint.js +57 -0
- package/dist/browser/containerBrowserHost.d.ts +24 -0
- package/dist/browser/containerBrowserHost.js +147 -0
- package/dist/browser/dockerSandboxOrchestrator.d.ts +26 -0
- package/dist/browser/dockerSandboxOrchestrator.js +142 -0
- package/dist/browser/fakeLiveViewRelay.d.ts +15 -0
- package/dist/browser/fakeLiveViewRelay.js +94 -0
- package/dist/browser/fakeLiveViewSource.d.ts +6 -0
- package/dist/browser/fakeLiveViewSource.js +16 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -1
- package/dist/browser/fakeRemoteBrowserHost.js +2 -1
- package/dist/browser/fakeSandboxOrchestrator.d.ts +9 -0
- package/dist/browser/fakeSandboxOrchestrator.js +44 -0
- package/dist/browser/gateHandoff.d.ts +0 -2
- package/dist/browser/gateHandoff.js +5 -11
- package/dist/browser/hostCapabilities.d.ts +23 -0
- package/dist/browser/hostCapabilities.js +23 -0
- package/dist/browser/humanLogin.d.ts +2 -2
- package/dist/browser/humanLogin.js +11 -2
- package/dist/browser/liveViewPersisted.d.ts +11 -0
- package/dist/browser/liveViewPersisted.js +22 -0
- package/dist/browser/liveViewRelay.d.ts +76 -0
- package/dist/browser/liveViewRelay.js +1 -0
- package/dist/browser/liveViewSessions.d.ts +25 -0
- package/dist/browser/liveViewSessions.js +139 -0
- package/dist/browser/liveViewSource.d.ts +25 -0
- package/dist/browser/liveViewSource.js +7 -0
- package/dist/browser/liveViewTickets.d.ts +46 -0
- package/dist/browser/liveViewTickets.js +122 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +53 -7
- package/dist/browser/ownerScope.d.ts +10 -0
- package/dist/browser/ownerScope.js +6 -0
- package/dist/browser/profileStore.d.ts +5 -2
- package/dist/browser/profileStore.js +3 -5
- package/dist/browser/sandboxOrchestrator.d.ts +60 -0
- package/dist/browser/sandboxOrchestrator.js +8 -0
- package/dist/browser/sessionApiSandboxOrchestrator.d.ts +34 -0
- package/dist/browser/sessionApiSandboxOrchestrator.js +108 -0
- package/dist/browser/stageBrowserEnv.d.ts +7 -0
- package/dist/browser/stageBrowserEnv.js +53 -15
- package/dist/browser/stageHandoff.d.ts +47 -0
- package/dist/browser/stageHandoff.js +101 -0
- package/dist/browser/stageProfileLock.d.ts +1 -0
- package/dist/browser/stageProfileLock.js +1 -3
- package/dist/browser/volumeProfileStore.d.ts +7 -0
- package/dist/browser/volumeProfileStore.js +35 -0
- package/dist/cli/browserCommand.d.ts +3 -1
- package/dist/cli/browserCommand.js +25 -20
- package/dist/cli/resolveCliProjectRoot.d.ts +7 -0
- package/dist/cli/resolveCliProjectRoot.js +27 -0
- package/dist/cli/runCommand.d.ts +1 -1
- package/dist/cli/runCommand.js +59 -17
- package/dist/cli/skillsCommand.d.ts +20 -0
- package/dist/cli/skillsCommand.js +74 -31
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +22 -4
- package/dist/cli.js +4 -4
- package/dist/config/browseCatalog.d.ts +2 -0
- package/dist/config/browseCatalog.js +6 -0
- package/dist/config/catalogRelativePath.d.ts +4 -0
- package/dist/config/catalogRelativePath.js +37 -9
- package/dist/config/createStage.d.ts +1 -0
- package/dist/config/createStage.js +11 -2
- package/dist/config/createTask.d.ts +30 -0
- package/dist/config/createTask.js +161 -0
- package/dist/config/createTrigger.d.ts +15 -0
- package/dist/config/createTrigger.js +32 -25
- package/dist/config/draftPackage.d.ts +41 -0
- package/dist/config/draftPackage.js +216 -23
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
- package/dist/config/ensureCatalogScanRoot.js +6 -2
- package/dist/config/findTaskInCatalog.d.ts +33 -0
- package/dist/config/findTaskInCatalog.js +69 -0
- package/dist/config/hostConfig.d.ts +21 -0
- package/dist/config/hostConfig.js +113 -1
- package/dist/config/importTask.d.ts +19 -0
- package/dist/config/importTask.js +133 -0
- package/dist/config/installSkillRequest.d.ts +40 -0
- package/dist/config/installSkillRequest.js +286 -0
- package/dist/config/listConfig.d.ts +0 -16
- package/dist/config/listConfig.js +0 -3
- package/dist/config/loadOutcome.d.ts +11 -6
- package/dist/config/loadPipeline.js +0 -1
- package/dist/config/loadStage.js +2 -0
- package/dist/config/loadStageflowManifest.js +4 -1
- package/dist/config/loadTask.js +2 -0
- package/dist/config/loadTrigger.js +2 -0
- package/dist/config/mergePipelineIncludes.js +3 -0
- package/dist/config/normalizePipelineStageEntry.js +10 -1
- package/dist/config/printTargetYaml.js +0 -1
- package/dist/config/publishDraftPackage.d.ts +5 -1
- package/dist/config/publishDraftPackage.js +4 -1
- package/dist/config/resolveCatalogRoots.d.ts +4 -1
- package/dist/config/resolveCatalogRoots.js +18 -1
- package/dist/config/skillUsageIndex.d.ts +6 -0
- package/dist/config/skillUsageIndex.js +39 -0
- package/dist/config/stageBrowser.js +13 -0
- package/dist/config/triggerAdapterStatus.d.ts +11 -0
- package/dist/config/triggerAdapterStatus.js +90 -0
- package/dist/config/triggerCatalog.d.ts +41 -2
- package/dist/config/triggerCatalog.js +107 -15
- package/dist/config/updateTask.d.ts +30 -0
- package/dist/config/updateTask.js +119 -0
- package/dist/config/updateTrigger.d.ts +38 -0
- package/dist/config/updateTrigger.js +220 -0
- package/dist/config/validateCatalog.d.ts +6 -2
- package/dist/config/validateCatalog.js +48 -7
- package/dist/config/workshopCatalogFilenames.d.ts +37 -0
- package/dist/config/workshopCatalogFilenames.js +125 -0
- package/dist/config/workshopCatalogRepair.d.ts +7 -0
- package/dist/config/workshopCatalogRepair.js +144 -0
- package/dist/config/yamlParsePosition.d.ts +4 -0
- package/dist/config/yamlParsePosition.js +9 -0
- package/dist/hosted/containment.d.ts +3 -0
- package/dist/hosted/containment.js +36 -0
- package/dist/hosted/errors.d.ts +21 -0
- package/dist/hosted/errors.js +30 -0
- package/dist/hosted/packageSkills.d.ts +3 -0
- package/dist/hosted/packageSkills.js +80 -0
- package/dist/hosted/runtime.d.ts +12 -0
- package/dist/hosted/runtime.js +596 -0
- package/dist/hosted/sequencer.d.ts +28 -0
- package/dist/hosted/sequencer.js +129 -0
- package/dist/hosted/translate.d.ts +3 -0
- package/dist/hosted/translate.js +43 -0
- package/dist/hosted/types.d.ts +108 -0
- package/dist/hosted/types.js +10 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +6 -0
- package/dist/mcp/getStartedTool.js +15 -6
- package/dist/mcp/triggerTools.js +2 -8
- package/dist/operatorAgent/agentPort.d.ts +11 -0
- package/dist/operatorAgent/agentPort.js +12 -0
- package/dist/operatorAgent/index.d.ts +1 -0
- package/dist/operatorAgent/index.js +1 -0
- package/dist/operatorAgent/piModel.js +26 -6
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +4 -2
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +103 -16
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/project/ensureProjectRootsFromEnv.d.ts +3 -0
- package/dist/project/ensureProjectRootsFromEnv.js +22 -0
- package/dist/project/globalHome.d.ts +1 -0
- package/dist/project/globalHome.js +18 -0
- package/dist/project/workspaceCatalog.d.ts +10 -0
- package/dist/project/workspaceCatalog.js +91 -0
- package/dist/projection/projectRun.d.ts +0 -1
- package/dist/projection/projectRun.js +0 -1
- package/dist/runstore/backup.js +14 -4
- package/dist/runstore/port.d.ts +11 -1
- package/dist/runstore/restore.js +5 -2
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +29 -9
- package/dist/runstore/sqlite/migrations/012-trigger-fires.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/012-trigger-fires.js +18 -0
- package/dist/runstore/sqlite/migrations/index.js +2 -0
- package/dist/runstore/stageSnapshot.d.ts +3 -1
- package/dist/runstore/stageSnapshot.js +19 -0
- package/dist/runstore/workspaceLayout.js +27 -0
- package/dist/runstore/writeContainedArtifact.d.ts +1 -0
- package/dist/runstore/writeContainedArtifact.js +52 -0
- package/dist/runtime/containerLimits.d.ts +0 -3
- package/dist/runtime/containerLimits.js +0 -5
- package/dist/runtime/credentialBinding.d.ts +3 -0
- package/dist/runtime/credentialBinding.js +23 -6
- package/dist/runtime/emailSource.js +20 -28
- package/dist/runtime/githubPollSource.js +20 -28
- package/dist/runtime/pipelineRunner.d.ts +4 -4
- package/dist/runtime/pipelineScheduler.d.ts +2 -2
- package/dist/runtime/pipelineScheduler.js +3 -0
- package/dist/runtime/resumeReconstruct.d.ts +2 -2
- package/dist/runtime/resumeTimedOut.d.ts +2 -2
- package/dist/runtime/runManager.d.ts +18 -2
- package/dist/runtime/runManager.js +95 -30
- package/dist/runtime/runRetentionSweep.d.ts +0 -2
- package/dist/runtime/runRetentionSweep.js +0 -8
- package/dist/runtime/runRetryCoordinator.d.ts +2 -2
- package/dist/runtime/scheduleSource.js +15 -24
- package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
- package/dist/runtime/stageAttemptBootstrap.js +10 -1
- package/dist/runtime/stageProcessLauncher.js +2 -2
- package/dist/runtime/stageRoots.js +12 -1
- package/dist/runtime/stageRunner.d.ts +2 -2
- package/dist/runtime/stageRunner.js +8 -2
- package/dist/runtime/stageWorker.js +2 -2
- package/dist/runtime/triggerPort.d.ts +5 -3
- package/dist/runtime/triggerRunner.d.ts +1 -0
- package/dist/runtime/triggerRunner.js +27 -20
- package/dist/runtime/webhookAccept.d.ts +25 -0
- package/dist/runtime/webhookAccept.js +56 -0
- package/dist/server/bootstrap.d.ts +7 -2
- package/dist/server/bootstrap.js +15 -1
- package/dist/server/controlToken.d.ts +1 -1
- package/dist/server/controlToken.js +18 -2
- package/dist/server/healthSurfaces.d.ts +12 -0
- package/dist/server/healthSurfaces.js +27 -6
- package/dist/server/http.d.ts +19 -10
- package/dist/server/http.js +605 -85
- package/dist/server/liveViewPath.d.ts +9 -0
- package/dist/server/liveViewPath.js +8 -0
- package/dist/server/liveViewRoutes.d.ts +58 -0
- package/dist/server/liveViewRoutes.js +565 -0
- package/dist/server/mcpHost.d.ts +4 -2
- package/dist/server/mcpHost.js +2 -0
- package/dist/server/readCatalogFile.d.ts +29 -0
- package/dist/server/readCatalogFile.js +215 -0
- package/dist/server/triggerRoutes.d.ts +1 -0
- package/dist/server/triggerRoutes.js +200 -73
- package/dist/tools/readEmail.d.ts +3 -3
- package/dist/tools/writeStageArtifact.d.ts +9 -0
- package/dist/tools/writeStageArtifact.js +2 -47
- package/dist/types/pipeline.d.ts +1 -1
- package/dist/types/stage.d.ts +1 -1
- package/dist/types/stageflowManifest.d.ts +1 -1
- package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/assets/index-BoFPkKFg.js +256 -0
- package/dist/ui/assets/{index-Bv4p1aPK.css → index-CIPbbBUi.css} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/workshop/autosave.d.ts +2 -2
- package/dist/workshop/autosave.js +4 -6
- package/dist/workshop/buildStore.d.ts +2 -0
- package/dist/workshop/chatAttachments.d.ts +27 -0
- package/dist/workshop/chatAttachments.js +98 -0
- package/dist/workshop/chatTurn.d.ts +5 -2
- package/dist/workshop/chatTurn.js +22 -18
- package/dist/workshop/sessionMutations.d.ts +17 -0
- package/dist/workshop/sessionMutations.js +107 -0
- package/dist/workshop/sessionStore.d.ts +10 -0
- package/dist/workshop/sessionStore.js +52 -0
- package/dist/workshop/workshopPin.d.ts +4 -3
- package/dist/workshop/workshopPin.js +26 -6
- package/dist/workshop/workshopTranscriptTurn.d.ts +8 -0
- package/dist/workshop/workshopTranscriptTurn.js +46 -0
- package/examples/README.md +1 -1
- package/examples/archify-on-pr/README.md +1 -1
- package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +0 -1
- package/examples/archify-on-pr/author-diagrams.yaml +1 -0
- package/examples/conditional-fork/branch-a.yaml +1 -1
- package/examples/conditional-fork/decide.yaml +2 -1
- package/examples/feature-loop/{implement.yaml → feature-implement.yaml} +1 -1
- package/examples/feature-loop/feature-loop.pipeline.yaml +10 -10
- package/examples/feature-loop/{plan.yaml → feature-plan.yaml} +1 -1
- package/examples/feature-loop/{review.yaml → feature-review.yaml} +1 -1
- package/examples/feedback-loop/{implement.yaml → feedback-implement.yaml} +1 -1
- package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +9 -9
- package/examples/feedback-loop/feedback-loop.pipeline.yaml +9 -9
- package/examples/feedback-loop/{plan.yaml → feedback-plan.yaml} +1 -1
- package/examples/feedback-loop/{review.yaml → feedback-review.yaml} +1 -1
- package/examples/generic-fan-in/{research.yaml → fan-in-research.yaml} +1 -1
- package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +3 -3
- package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +3 -3
- package/examples/hello-world/{research.yaml → hello-research.yaml} +1 -1
- package/examples/hello-world/hello.pipeline.yaml +2 -2
- package/examples/mcp-hitl-tour/{review.yaml → mcp-hitl-review.yaml} +1 -1
- package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +3 -3
- package/examples/oss-issue-contribution/oss-explain-fix.yaml +1 -0
- package/examples/oss-issue-contribution/oss-explain-issue.yaml +1 -0
- package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +0 -2
- package/examples/stageflow.yaml +6 -0
- package/examples/triggers/sample-github-pr.trigger.yaml +12 -0
- package/examples/triggers/sample-inbox-email.trigger.yaml +14 -0
- package/examples/triggers/sample-manual-hello.trigger.yaml +5 -0
- package/examples/triggers/sample-nightly-hello.trigger.yaml +8 -0
- package/examples/triggers/sample-signed-webhook.trigger.yaml +11 -0
- package/examples/triggers-readiness/README.md +48 -0
- package/examples/triggers-readiness/readiness-emit.yaml +18 -0
- package/examples/triggers-readiness/readiness.pipeline.yaml +4 -0
- package/examples/triggers-readiness/readiness.task.yaml +6 -0
- package/examples/triggers-readiness/stageflow.yaml +13 -0
- package/examples/triggers-readiness/triggers/readiness-dynamic-manual.trigger.yaml +4 -0
- package/examples/triggers-readiness/triggers/readiness-email.trigger.yaml +12 -0
- package/examples/triggers-readiness/triggers/readiness-github-pr.trigger.yaml +12 -0
- package/examples/triggers-readiness/triggers/readiness-manual.trigger.yaml +5 -0
- package/examples/triggers-readiness/triggers/readiness-schedule.trigger.yaml +8 -0
- package/examples/triggers-readiness/triggers/readiness-webhook.trigger.yaml +11 -0
- package/package.json +9 -1
- package/skills/stageflow/references/control-surface.md +1 -1
- package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
- package/skills/stageflow-run/SKILL.md +1 -1
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-setup/SKILL.md +1 -1
- package/dist/ui/assets/index-59cjF0OA.js +0 -133
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
export const DEFAULT_MAX_INPUT_BATCH = 64;
|
|
2
|
+
export const DEFAULT_MAX_INPUT_EVENTS_PER_SECOND = 500;
|
|
3
|
+
const MAX_COORDINATE = 20_000;
|
|
4
|
+
const MAX_DELTA = 100_000;
|
|
5
|
+
const MAX_TEXT_LENGTH = 32;
|
|
6
|
+
const MAX_TOUCH_POINTS = 10;
|
|
7
|
+
const STATUS_TIMEOUT_MS = 10_000;
|
|
8
|
+
const MAX_QUEUED_BATCHES = 16;
|
|
9
|
+
const TAB_LIST_TRIES = 5;
|
|
10
|
+
const RECONNECT_TRIES = 3;
|
|
11
|
+
const RETARGET_ATTEMPTS = 2;
|
|
12
|
+
const DEFAULT_FIRST_FRAME_TIMEOUT_MS = 1500;
|
|
13
|
+
export const DEFAULT_REOPEN_MIN_INTERVAL_MS = 2000;
|
|
14
|
+
const REOPEN_WAIT_TRIES = 50;
|
|
15
|
+
const FORWARDED = new Set(["frame", "status", "tabs", "url"]);
|
|
16
|
+
const DEFAULT_DIALOG_TIMEOUT_MS = 60_000;
|
|
17
|
+
const MAX_DIALOG_TEXT = 2000;
|
|
18
|
+
const MAX_DIALOG_ID = 64;
|
|
19
|
+
const DIALOG_KINDS = ["alert", "confirm", "prompt", "beforeunload"];
|
|
20
|
+
const CDP_NO_DIALOG = -32602;
|
|
21
|
+
const REPLAYED = ["status", "tabs", "url", "frame"];
|
|
22
|
+
export class LiveViewRelayError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
constructor(code, message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.name = "LiveViewRelayError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const WS_OPEN = 1;
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function onlyKeys(value, allowed) {
|
|
35
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
36
|
+
}
|
|
37
|
+
function oneOf(value, options) {
|
|
38
|
+
return typeof value === "string" && options.includes(value);
|
|
39
|
+
}
|
|
40
|
+
function isNumberIn(value, min, max) {
|
|
41
|
+
return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
42
|
+
}
|
|
43
|
+
function isIntIn(value, min, max) {
|
|
44
|
+
return isNumberIn(value, min, max) && Number.isInteger(value);
|
|
45
|
+
}
|
|
46
|
+
function optional(value, check) {
|
|
47
|
+
return value === undefined || check(value);
|
|
48
|
+
}
|
|
49
|
+
function shortString(value) {
|
|
50
|
+
return typeof value === "string" && value.length <= MAX_TEXT_LENGTH;
|
|
51
|
+
}
|
|
52
|
+
function validMouse(e) {
|
|
53
|
+
return (onlyKeys(e, ["type", "eventType", "x", "y", "button", "clickCount", "deltaX", "deltaY", "modifiers"]) &&
|
|
54
|
+
oneOf(e.eventType, ["mousePressed", "mouseReleased", "mouseMoved", "mouseWheel"]) &&
|
|
55
|
+
isNumberIn(e.x, 0, MAX_COORDINATE) &&
|
|
56
|
+
isNumberIn(e.y, 0, MAX_COORDINATE) &&
|
|
57
|
+
optional(e.button, (v) => oneOf(v, ["left", "middle", "right"])) &&
|
|
58
|
+
optional(e.clickCount, (v) => isIntIn(v, 0, 3)) &&
|
|
59
|
+
optional(e.deltaX, (v) => isNumberIn(v, -MAX_DELTA, MAX_DELTA)) &&
|
|
60
|
+
optional(e.deltaY, (v) => isNumberIn(v, -MAX_DELTA, MAX_DELTA)) &&
|
|
61
|
+
optional(e.modifiers, (v) => isIntIn(v, 0, 15)));
|
|
62
|
+
}
|
|
63
|
+
function validKeyboard(e) {
|
|
64
|
+
return (onlyKeys(e, ["type", "eventType", "key", "code", "text", "windowsVirtualKeyCode", "modifiers"]) &&
|
|
65
|
+
oneOf(e.eventType, ["keyDown", "keyUp", "char"]) &&
|
|
66
|
+
optional(e.key, shortString) &&
|
|
67
|
+
optional(e.code, shortString) &&
|
|
68
|
+
optional(e.text, shortString) &&
|
|
69
|
+
optional(e.windowsVirtualKeyCode, (v) => isIntIn(v, 0, 255)) &&
|
|
70
|
+
optional(e.modifiers, (v) => isIntIn(v, 0, 15)));
|
|
71
|
+
}
|
|
72
|
+
function validTouchPoint(p) {
|
|
73
|
+
return (isPlainObject(p) &&
|
|
74
|
+
onlyKeys(p, ["x", "y", "id"]) &&
|
|
75
|
+
isNumberIn(p.x, 0, MAX_COORDINATE) &&
|
|
76
|
+
isNumberIn(p.y, 0, MAX_COORDINATE) &&
|
|
77
|
+
optional(p.id, (v) => isIntIn(v, 0, 1_000_000)));
|
|
78
|
+
}
|
|
79
|
+
function validTouch(e) {
|
|
80
|
+
return (onlyKeys(e, ["type", "eventType", "touchPoints", "modifiers"]) &&
|
|
81
|
+
oneOf(e.eventType, ["touchStart", "touchMove", "touchEnd", "touchCancel"]) &&
|
|
82
|
+
Array.isArray(e.touchPoints) &&
|
|
83
|
+
e.touchPoints.length <= MAX_TOUCH_POINTS &&
|
|
84
|
+
e.touchPoints.every(validTouchPoint) &&
|
|
85
|
+
optional(e.modifiers, (v) => isIntIn(v, 0, 15)));
|
|
86
|
+
}
|
|
87
|
+
export function validEvent(event) {
|
|
88
|
+
if (!isPlainObject(event))
|
|
89
|
+
return false;
|
|
90
|
+
switch (event.type) {
|
|
91
|
+
case "input_mouse":
|
|
92
|
+
return validMouse(event);
|
|
93
|
+
case "input_keyboard":
|
|
94
|
+
return validKeyboard(event);
|
|
95
|
+
case "input_touch":
|
|
96
|
+
return validTouch(event);
|
|
97
|
+
default:
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function readStreamPort(runner, env) {
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await runner(["stream", "status", "--json"], env, { timeoutMs: STATUS_TIMEOUT_MS });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (result.code !== 0)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(result.stdout ?? "");
|
|
113
|
+
const data = isPlainObject(parsed) && isPlainObject(parsed.data) ? parsed.data : undefined;
|
|
114
|
+
const port = data?.port;
|
|
115
|
+
if (data?.enabled === false)
|
|
116
|
+
return undefined;
|
|
117
|
+
return typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536
|
|
118
|
+
? port
|
|
119
|
+
: undefined;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function findStreamPort(runner, env) {
|
|
126
|
+
let port = await readStreamPort(runner, env);
|
|
127
|
+
if (port === undefined) {
|
|
128
|
+
try {
|
|
129
|
+
await runner(["stream", "enable", "--json"], env, { timeoutMs: STATUS_TIMEOUT_MS });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// the follow-up status read decides
|
|
133
|
+
}
|
|
134
|
+
port = await readStreamPort(runner, env);
|
|
135
|
+
}
|
|
136
|
+
if (port === undefined) {
|
|
137
|
+
throw new LiveViewRelayError("stream_port_unavailable", "agent-browser did not report a stream port");
|
|
138
|
+
}
|
|
139
|
+
return port;
|
|
140
|
+
}
|
|
141
|
+
function connect(Impl, url) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
let ws;
|
|
144
|
+
const fail = () => reject(new LiveViewRelayError("stream_connect_failed", "could not connect to the browser stream"));
|
|
145
|
+
try {
|
|
146
|
+
ws = new Impl(url);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
fail();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
ws.onopen = () => resolve(ws);
|
|
153
|
+
ws.onerror = fail;
|
|
154
|
+
ws.onclose = fail;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const streamUrl = (port) => `ws://127.0.0.1:${port}`;
|
|
158
|
+
async function readCdpAddress(runner, request) {
|
|
159
|
+
const known = request.cdpAddress ?? request.env.AGENT_BROWSER_CDP;
|
|
160
|
+
if (known !== undefined)
|
|
161
|
+
return known;
|
|
162
|
+
try {
|
|
163
|
+
const result = await runner(["get", "cdp-url"], request.env, { timeoutMs: STATUS_TIMEOUT_MS });
|
|
164
|
+
const out = (result.stdout ?? "").trim();
|
|
165
|
+
return result.code === 0 && /^wss?:\/\//.test(out) ? out : undefined;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function findTab(stdout, targetId) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(stdout ?? "");
|
|
174
|
+
const tabs = isPlainObject(parsed) && isPlainObject(parsed.data) ? parsed.data.tabs : undefined;
|
|
175
|
+
if (!Array.isArray(tabs))
|
|
176
|
+
return undefined;
|
|
177
|
+
for (const tab of tabs) {
|
|
178
|
+
if (isPlainObject(tab) && tab.targetId === targetId && typeof tab.tabId === "string") {
|
|
179
|
+
return { tabId: tab.tabId, url: typeof tab.url === "string" ? tab.url : "" };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const clip = (value) => typeof value === "string" ? value.slice(0, MAX_DIALOG_TEXT) : "";
|
|
189
|
+
export function createAgentBrowserLiveViewRelay(options) {
|
|
190
|
+
const Impl = options.WebSocketImpl ?? globalThis.WebSocket;
|
|
191
|
+
const maxBatch = options.maxBatch ?? DEFAULT_MAX_INPUT_BATCH;
|
|
192
|
+
const maxPerSecond = options.maxEventsPerSecond ?? DEFAULT_MAX_INPUT_EVENTS_PER_SECOND;
|
|
193
|
+
const now = options.now ?? Date.now;
|
|
194
|
+
const retryDelayMs = options.retryDelayMs ?? 100;
|
|
195
|
+
const reopenMinIntervalMs = options.reopenMinIntervalMs ?? DEFAULT_REOPEN_MIN_INTERVAL_MS;
|
|
196
|
+
const dialogTimeoutMs = options.dialogTimeoutMs ?? DEFAULT_DIALOG_TIMEOUT_MS;
|
|
197
|
+
const firstFrameTimeoutMs = options.firstFrameTimeoutMs ?? DEFAULT_FIRST_FRAME_TIMEOUT_MS;
|
|
198
|
+
const schedule = options.schedule ??
|
|
199
|
+
((fn, ms) => {
|
|
200
|
+
const timer = setTimeout(fn, ms);
|
|
201
|
+
return () => clearTimeout(timer);
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
async open(request) {
|
|
205
|
+
const port = await findStreamPort(options.runner, request.env);
|
|
206
|
+
let ws = await connect(Impl, streamUrl(port));
|
|
207
|
+
const cdpAddress = await readCdpAddress(options.runner, request);
|
|
208
|
+
let cdp;
|
|
209
|
+
if (cdpAddress !== undefined) {
|
|
210
|
+
cdp = await connect(Impl, cdpAddress).catch(() => undefined);
|
|
211
|
+
}
|
|
212
|
+
const subscribers = new Set();
|
|
213
|
+
const latest = new Map();
|
|
214
|
+
const timers = new Set();
|
|
215
|
+
const queued = [];
|
|
216
|
+
const pages = new Map();
|
|
217
|
+
let current;
|
|
218
|
+
let desired;
|
|
219
|
+
let running = false;
|
|
220
|
+
let retargeting = false;
|
|
221
|
+
let closed = false;
|
|
222
|
+
let frameSeen = false;
|
|
223
|
+
let cancelNudge;
|
|
224
|
+
const targetSessions = new Map();
|
|
225
|
+
const sessionTargets = new Map();
|
|
226
|
+
const openDialogs = new Map();
|
|
227
|
+
const cdpPending = new Map();
|
|
228
|
+
let reopening = false;
|
|
229
|
+
let lastReopenAt;
|
|
230
|
+
let dialogSeq = 0;
|
|
231
|
+
let cdpSeq = 0;
|
|
232
|
+
let windowStart = now();
|
|
233
|
+
let windowCount = 0;
|
|
234
|
+
const dropSocket = (socket) => {
|
|
235
|
+
if (socket === undefined)
|
|
236
|
+
return;
|
|
237
|
+
socket.onopen = socket.onmessage = socket.onerror = socket.onclose = null;
|
|
238
|
+
try {
|
|
239
|
+
socket.close();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// already closing
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const finish = (reason) => {
|
|
246
|
+
if (closed)
|
|
247
|
+
return;
|
|
248
|
+
closed = true;
|
|
249
|
+
cancelNudge?.();
|
|
250
|
+
for (const open of openDialogs.values())
|
|
251
|
+
open.cancelTimer?.();
|
|
252
|
+
openDialogs.clear();
|
|
253
|
+
dropSocket(ws);
|
|
254
|
+
dropSocket(cdp);
|
|
255
|
+
ws = cdp = undefined;
|
|
256
|
+
for (const done of [...cdpPending.values()])
|
|
257
|
+
done(undefined);
|
|
258
|
+
cdpPending.clear();
|
|
259
|
+
for (const { timer, done } of [...timers]) {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
done();
|
|
262
|
+
}
|
|
263
|
+
timers.clear();
|
|
264
|
+
for (const batch of queued.splice(0))
|
|
265
|
+
batch.done({ ok: false, reason: "closed" });
|
|
266
|
+
desired = undefined;
|
|
267
|
+
const message = { type: "closed", data: { reason } };
|
|
268
|
+
const targets = [...subscribers];
|
|
269
|
+
subscribers.clear();
|
|
270
|
+
latest.clear();
|
|
271
|
+
for (const subscriber of targets)
|
|
272
|
+
subscriber(message);
|
|
273
|
+
};
|
|
274
|
+
const pause = () => new Promise((resolve) => {
|
|
275
|
+
const entry = {
|
|
276
|
+
timer: setTimeout(() => {
|
|
277
|
+
timers.delete(entry);
|
|
278
|
+
resolve();
|
|
279
|
+
}, retryDelayMs),
|
|
280
|
+
done: resolve,
|
|
281
|
+
};
|
|
282
|
+
timers.add(entry);
|
|
283
|
+
});
|
|
284
|
+
const broadcast = (message) => {
|
|
285
|
+
for (const subscriber of [...subscribers])
|
|
286
|
+
subscriber(message);
|
|
287
|
+
};
|
|
288
|
+
const cdpSend = (method, params, sessionId) => {
|
|
289
|
+
const socket = cdp;
|
|
290
|
+
if (socket === undefined || socket.readyState !== WS_OPEN)
|
|
291
|
+
return undefined;
|
|
292
|
+
const id = ++cdpSeq;
|
|
293
|
+
try {
|
|
294
|
+
socket.send(JSON.stringify({ id, method, params, sessionId }));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
return id;
|
|
300
|
+
};
|
|
301
|
+
const cdpCall = (method, params, sessionId) => new Promise((resolve) => {
|
|
302
|
+
const id = cdpSend(method, params, sessionId);
|
|
303
|
+
if (id === undefined) {
|
|
304
|
+
resolve(undefined);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
cdpPending.set(id, resolve);
|
|
308
|
+
});
|
|
309
|
+
const endDialog = (open, result) => {
|
|
310
|
+
if (openDialogs.get(open.dialog.id) !== open)
|
|
311
|
+
return;
|
|
312
|
+
openDialogs.delete(open.dialog.id);
|
|
313
|
+
open.cancelTimer?.();
|
|
314
|
+
broadcast({ type: "dialog_closed", data: { id: open.dialog.id, result } });
|
|
315
|
+
};
|
|
316
|
+
const endDialogsWhere = (match, result) => {
|
|
317
|
+
for (const open of [...openDialogs.values()])
|
|
318
|
+
if (match(open))
|
|
319
|
+
endDialog(open, result);
|
|
320
|
+
};
|
|
321
|
+
const expireDialog = async (open) => {
|
|
322
|
+
open.cancelTimer = undefined;
|
|
323
|
+
if (open.answered || openDialogs.get(open.dialog.id) !== open)
|
|
324
|
+
return;
|
|
325
|
+
open.answered = true;
|
|
326
|
+
const reply = await cdpCall("Page.handleJavaScriptDialog", { accept: false }, open.sessionId);
|
|
327
|
+
if (reply === undefined)
|
|
328
|
+
return;
|
|
329
|
+
if (reply.error === undefined || reply.error.code === CDP_NO_DIALOG)
|
|
330
|
+
endDialog(open, "timeout");
|
|
331
|
+
};
|
|
332
|
+
const onDialogOpening = (sessionId, params) => {
|
|
333
|
+
const targetId = sessionTargets.get(sessionId);
|
|
334
|
+
if (targetId === undefined || typeof params.type !== "string" || !DIALOG_KINDS.includes(params.type))
|
|
335
|
+
return;
|
|
336
|
+
endDialogsWhere((open) => open.sessionId === sessionId, "closed_by_page");
|
|
337
|
+
const kind = params.type;
|
|
338
|
+
const answerable = kind === "confirm" || kind === "prompt";
|
|
339
|
+
const dialog = {
|
|
340
|
+
id: `d${++dialogSeq}`,
|
|
341
|
+
kind,
|
|
342
|
+
message: clip(params.message),
|
|
343
|
+
defaultPrompt: clip(params.defaultPrompt),
|
|
344
|
+
targetId,
|
|
345
|
+
answerable,
|
|
346
|
+
};
|
|
347
|
+
const open = { dialog, sessionId, answered: false };
|
|
348
|
+
openDialogs.set(dialog.id, open);
|
|
349
|
+
if (answerable)
|
|
350
|
+
open.cancelTimer = schedule(() => void expireDialog(open), dialogTimeoutMs);
|
|
351
|
+
broadcast({ type: "dialog", data: dialog });
|
|
352
|
+
};
|
|
353
|
+
const onDialogClosed = (sessionId, params) => {
|
|
354
|
+
endDialogsWhere((open) => open.sessionId === sessionId, params.result === true ? "accepted" : "dismissed");
|
|
355
|
+
};
|
|
356
|
+
const onAttached = (params) => {
|
|
357
|
+
const sessionId = params.sessionId;
|
|
358
|
+
if (typeof sessionId !== "string")
|
|
359
|
+
return;
|
|
360
|
+
const info = params.targetInfo;
|
|
361
|
+
if (isPlainObject(info) && info.type === "page" && typeof info.targetId === "string") {
|
|
362
|
+
targetSessions.set(info.targetId, sessionId);
|
|
363
|
+
sessionTargets.set(sessionId, info.targetId);
|
|
364
|
+
cdpSend("Page.enable", {}, sessionId);
|
|
365
|
+
}
|
|
366
|
+
if (params.waitingForDebugger === true)
|
|
367
|
+
cdpSend("Runtime.runIfWaitingForDebugger", {}, sessionId);
|
|
368
|
+
};
|
|
369
|
+
const onDetached = (params) => {
|
|
370
|
+
const sessionId = params.sessionId;
|
|
371
|
+
if (typeof sessionId !== "string")
|
|
372
|
+
return;
|
|
373
|
+
const targetId = sessionTargets.get(sessionId);
|
|
374
|
+
sessionTargets.delete(sessionId);
|
|
375
|
+
if (targetId !== undefined && targetSessions.get(targetId) === sessionId)
|
|
376
|
+
targetSessions.delete(targetId);
|
|
377
|
+
endDialogsWhere((open) => open.sessionId === sessionId, "closed_by_page");
|
|
378
|
+
};
|
|
379
|
+
const attachStream = (socket) => {
|
|
380
|
+
socket.onmessage = (event) => {
|
|
381
|
+
if (closed || typeof event.data !== "string")
|
|
382
|
+
return;
|
|
383
|
+
let parsed;
|
|
384
|
+
try {
|
|
385
|
+
parsed = JSON.parse(event.data);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!isPlainObject(parsed) || typeof parsed.type !== "string")
|
|
391
|
+
return;
|
|
392
|
+
if (!FORWARDED.has(parsed.type))
|
|
393
|
+
return;
|
|
394
|
+
if (parsed.type === "frame")
|
|
395
|
+
frameSeen = true;
|
|
396
|
+
const { type, ...data } = parsed;
|
|
397
|
+
const message = { type, data };
|
|
398
|
+
latest.set(type, message);
|
|
399
|
+
for (const subscriber of [...subscribers])
|
|
400
|
+
subscriber(message);
|
|
401
|
+
};
|
|
402
|
+
socket.onclose = () => finish("upstream_closed");
|
|
403
|
+
};
|
|
404
|
+
attachStream(ws);
|
|
405
|
+
const run = async (args) => {
|
|
406
|
+
try {
|
|
407
|
+
return await options.runner(args, request.env, { timeoutMs: STATUS_TIMEOUT_MS });
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const resolveTab = async (target) => {
|
|
414
|
+
for (let i = 0; i < TAB_LIST_TRIES && !closed; i++) {
|
|
415
|
+
const listed = await run(["tab", "--json"]);
|
|
416
|
+
if (listed === undefined || listed.code !== 0)
|
|
417
|
+
return undefined;
|
|
418
|
+
const tab = findTab(listed.stdout, target);
|
|
419
|
+
if (tab !== undefined)
|
|
420
|
+
return tab;
|
|
421
|
+
if (i < TAB_LIST_TRIES - 1)
|
|
422
|
+
await pause();
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
};
|
|
426
|
+
const reconnect = async () => {
|
|
427
|
+
for (let i = 0; i < RECONNECT_TRIES && !closed; i++) {
|
|
428
|
+
try {
|
|
429
|
+
return await connect(Impl, streamUrl(port));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
if (i < RECONNECT_TRIES - 1)
|
|
433
|
+
await pause();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return undefined;
|
|
437
|
+
};
|
|
438
|
+
const restartStream = async () => {
|
|
439
|
+
dropSocket(ws);
|
|
440
|
+
ws = undefined;
|
|
441
|
+
if ((await run(["stream", "disable"]))?.code !== 0 || closed)
|
|
442
|
+
return false;
|
|
443
|
+
if ((await run(["stream", "enable", "--port", String(port)]))?.code !== 0 || closed)
|
|
444
|
+
return false;
|
|
445
|
+
const fresh = await reconnect();
|
|
446
|
+
if (fresh === undefined)
|
|
447
|
+
return false;
|
|
448
|
+
if (closed) {
|
|
449
|
+
dropSocket(fresh);
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
ws = fresh;
|
|
453
|
+
attachStream(fresh);
|
|
454
|
+
frameSeen = false;
|
|
455
|
+
return true;
|
|
456
|
+
};
|
|
457
|
+
const armNudge = () => {
|
|
458
|
+
cancelNudge?.();
|
|
459
|
+
cancelNudge = schedule(() => {
|
|
460
|
+
cancelNudge = undefined;
|
|
461
|
+
if (!closed && !frameSeen && !running)
|
|
462
|
+
void nudge();
|
|
463
|
+
}, firstFrameTimeoutMs);
|
|
464
|
+
};
|
|
465
|
+
const nudge = async () => {
|
|
466
|
+
running = true;
|
|
467
|
+
retargeting = true;
|
|
468
|
+
const ok = await restartStream();
|
|
469
|
+
running = false;
|
|
470
|
+
retargeting = false;
|
|
471
|
+
if (closed)
|
|
472
|
+
return;
|
|
473
|
+
if (!ok) {
|
|
474
|
+
finish("nudge_failed");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
flushQueued();
|
|
478
|
+
if (desired !== undefined)
|
|
479
|
+
void retargetLoop();
|
|
480
|
+
};
|
|
481
|
+
const attemptRetarget = async (target, reason) => {
|
|
482
|
+
const tab = await resolveTab(target);
|
|
483
|
+
if (tab === undefined || closed)
|
|
484
|
+
return false;
|
|
485
|
+
if ((await run(["tab", tab.tabId]))?.code !== 0 || closed)
|
|
486
|
+
return false;
|
|
487
|
+
if (!(await restartStream()))
|
|
488
|
+
return false;
|
|
489
|
+
current = target;
|
|
490
|
+
latest.delete("frame");
|
|
491
|
+
armNudge();
|
|
492
|
+
const message = { type: "retarget", data: { tab: tab.tabId, url: tab.url, reason } };
|
|
493
|
+
for (const subscriber of [...subscribers])
|
|
494
|
+
subscriber(message);
|
|
495
|
+
const fresh = pages.get(target)?.url ?? "";
|
|
496
|
+
if (fresh !== "") {
|
|
497
|
+
const urlMessage = { type: "url", data: { url: fresh } };
|
|
498
|
+
latest.set("url", urlMessage);
|
|
499
|
+
if (fresh !== tab.url)
|
|
500
|
+
for (const subscriber of [...subscribers])
|
|
501
|
+
subscriber(urlMessage);
|
|
502
|
+
}
|
|
503
|
+
return true;
|
|
504
|
+
};
|
|
505
|
+
const flushQueued = () => {
|
|
506
|
+
for (const batch of queued.splice(0)) {
|
|
507
|
+
if (ws === undefined || ws.readyState !== WS_OPEN) {
|
|
508
|
+
batch.done({ ok: false, reason: "upstream_unavailable" });
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
for (const event of batch.events)
|
|
512
|
+
ws.send(JSON.stringify(event));
|
|
513
|
+
batch.done({ ok: true, accepted: batch.events.length });
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const retargetLoop = async () => {
|
|
517
|
+
running = true;
|
|
518
|
+
retargeting = true;
|
|
519
|
+
cancelNudge?.();
|
|
520
|
+
cancelNudge = undefined;
|
|
521
|
+
while (desired !== undefined && !closed) {
|
|
522
|
+
const { target, reason } = desired;
|
|
523
|
+
desired = undefined;
|
|
524
|
+
if (target === current || !pages.has(target))
|
|
525
|
+
continue;
|
|
526
|
+
let ok = false;
|
|
527
|
+
for (let attempt = 0; attempt < RETARGET_ATTEMPTS && !ok && !closed; attempt++) {
|
|
528
|
+
ok = await attemptRetarget(target, reason);
|
|
529
|
+
}
|
|
530
|
+
if (closed)
|
|
531
|
+
break;
|
|
532
|
+
if (!ok) {
|
|
533
|
+
finish("retarget_failed");
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
running = false;
|
|
538
|
+
retargeting = false;
|
|
539
|
+
if (!closed)
|
|
540
|
+
flushQueued();
|
|
541
|
+
};
|
|
542
|
+
const want = (target, reason) => {
|
|
543
|
+
desired = { target, reason };
|
|
544
|
+
if (!running)
|
|
545
|
+
void retargetLoop();
|
|
546
|
+
};
|
|
547
|
+
const publishUrl = (url) => {
|
|
548
|
+
const known = latest.get("url")?.data;
|
|
549
|
+
if (url === "" || known?.url === url)
|
|
550
|
+
return;
|
|
551
|
+
const message = { type: "url", data: { url } };
|
|
552
|
+
latest.set("url", message);
|
|
553
|
+
for (const subscriber of [...subscribers])
|
|
554
|
+
subscriber(message);
|
|
555
|
+
};
|
|
556
|
+
const onTarget = (method, params) => {
|
|
557
|
+
if (closed)
|
|
558
|
+
return;
|
|
559
|
+
if (method === "Target.targetDestroyed") {
|
|
560
|
+
const id = params.targetId;
|
|
561
|
+
if (typeof id !== "string")
|
|
562
|
+
return;
|
|
563
|
+
const gone = pages.get(id);
|
|
564
|
+
pages.delete(id);
|
|
565
|
+
const session = targetSessions.get(id);
|
|
566
|
+
targetSessions.delete(id);
|
|
567
|
+
if (session !== undefined) {
|
|
568
|
+
sessionTargets.delete(session);
|
|
569
|
+
endDialogsWhere((open) => open.sessionId === session, "closed_by_page");
|
|
570
|
+
}
|
|
571
|
+
if (desired?.target === id)
|
|
572
|
+
desired = undefined;
|
|
573
|
+
if (gone !== undefined && id === current) {
|
|
574
|
+
const back = gone.opener !== undefined && pages.has(gone.opener) ? gone.opener : [...pages.keys()].at(-1);
|
|
575
|
+
if (back !== undefined)
|
|
576
|
+
want(back, "tab_closed");
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const info = params.targetInfo;
|
|
581
|
+
if (!isPlainObject(info) || info.type !== "page" || typeof info.targetId !== "string")
|
|
582
|
+
return;
|
|
583
|
+
const id = info.targetId;
|
|
584
|
+
const url = typeof info.url === "string" ? info.url : "";
|
|
585
|
+
const opener = typeof info.openerId === "string" ? info.openerId : undefined;
|
|
586
|
+
const isNew = !pages.has(id);
|
|
587
|
+
pages.set(id, { opener: opener ?? pages.get(id)?.opener, url });
|
|
588
|
+
if (current === undefined && /^https?:/.test(url))
|
|
589
|
+
current = id;
|
|
590
|
+
if (id === current)
|
|
591
|
+
publishUrl(url);
|
|
592
|
+
if (isNew && opener !== undefined)
|
|
593
|
+
want(id, "popup_opened");
|
|
594
|
+
};
|
|
595
|
+
if (cdp !== undefined) {
|
|
596
|
+
cdp.onmessage = (event) => {
|
|
597
|
+
if (typeof event.data !== "string")
|
|
598
|
+
return;
|
|
599
|
+
try {
|
|
600
|
+
const parsed = JSON.parse(event.data);
|
|
601
|
+
if (!isPlainObject(parsed))
|
|
602
|
+
return;
|
|
603
|
+
if (typeof parsed.id === "number") {
|
|
604
|
+
const done = cdpPending.get(parsed.id);
|
|
605
|
+
if (done === undefined)
|
|
606
|
+
return;
|
|
607
|
+
cdpPending.delete(parsed.id);
|
|
608
|
+
const error = isPlainObject(parsed.error) ? parsed.error : undefined;
|
|
609
|
+
done(error === undefined ? { result: parsed.result } : { error: { code: typeof error.code === "number" ? error.code : undefined } });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (typeof parsed.method !== "string" || !isPlainObject(parsed.params))
|
|
613
|
+
return;
|
|
614
|
+
const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : undefined;
|
|
615
|
+
switch (parsed.method) {
|
|
616
|
+
case "Target.targetCreated":
|
|
617
|
+
case "Target.targetInfoChanged":
|
|
618
|
+
case "Target.targetDestroyed":
|
|
619
|
+
onTarget(parsed.method, parsed.params);
|
|
620
|
+
break;
|
|
621
|
+
case "Target.attachedToTarget":
|
|
622
|
+
if (!closed)
|
|
623
|
+
onAttached(parsed.params);
|
|
624
|
+
break;
|
|
625
|
+
case "Target.detachedFromTarget":
|
|
626
|
+
if (!closed)
|
|
627
|
+
onDetached(parsed.params);
|
|
628
|
+
break;
|
|
629
|
+
case "Page.javascriptDialogOpening":
|
|
630
|
+
if (!closed && sessionId !== undefined)
|
|
631
|
+
onDialogOpening(sessionId, parsed.params);
|
|
632
|
+
break;
|
|
633
|
+
case "Page.javascriptDialogClosed":
|
|
634
|
+
if (!closed && sessionId !== undefined)
|
|
635
|
+
onDialogClosed(sessionId, parsed.params);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const watcher = cdp;
|
|
644
|
+
watcher.onclose = () => {
|
|
645
|
+
watcher.onmessage = watcher.onclose = null;
|
|
646
|
+
if (cdp !== watcher)
|
|
647
|
+
return;
|
|
648
|
+
cdp = undefined;
|
|
649
|
+
for (const done of [...cdpPending.values()])
|
|
650
|
+
done(undefined);
|
|
651
|
+
cdpPending.clear();
|
|
652
|
+
targetSessions.clear();
|
|
653
|
+
sessionTargets.clear();
|
|
654
|
+
endDialogsWhere(() => true, "closed_by_page");
|
|
655
|
+
};
|
|
656
|
+
cdpSend("Target.setDiscoverTargets", { discover: true });
|
|
657
|
+
cdpSend("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
|
|
658
|
+
}
|
|
659
|
+
armNudge();
|
|
660
|
+
const waitFor = async (ready) => {
|
|
661
|
+
for (let i = 0; i < REOPEN_WAIT_TRIES && !closed; i++) {
|
|
662
|
+
if (ready())
|
|
663
|
+
return true;
|
|
664
|
+
await pause();
|
|
665
|
+
}
|
|
666
|
+
return !closed && ready();
|
|
667
|
+
};
|
|
668
|
+
const reopenTab = async () => {
|
|
669
|
+
if (closed)
|
|
670
|
+
return { ok: false, reason: "closed" };
|
|
671
|
+
if (cdp === undefined || cdp.readyState !== WS_OPEN)
|
|
672
|
+
return { ok: false, reason: "upstream_unavailable" };
|
|
673
|
+
const old = current;
|
|
674
|
+
const url = old === undefined ? "" : (pages.get(old)?.url ?? "");
|
|
675
|
+
if (old === undefined || url === "")
|
|
676
|
+
return { ok: false, reason: "no_tab" };
|
|
677
|
+
const at = now();
|
|
678
|
+
if (reopening || (lastReopenAt !== undefined && at >= lastReopenAt && at - lastReopenAt < reopenMinIntervalMs)) {
|
|
679
|
+
return { ok: false, reason: "rate_limited" };
|
|
680
|
+
}
|
|
681
|
+
reopening = true;
|
|
682
|
+
lastReopenAt = at;
|
|
683
|
+
try {
|
|
684
|
+
const created = await cdpCall("Target.createTarget", { url });
|
|
685
|
+
const fresh = isPlainObject(created?.result) ? created.result.targetId : undefined;
|
|
686
|
+
if (typeof fresh !== "string")
|
|
687
|
+
return { ok: false, reason: closed ? "closed" : "failed" };
|
|
688
|
+
const closeTarget = (targetId) => void cdpSend("Target.closeTarget", { targetId });
|
|
689
|
+
if (!(await waitFor(() => pages.has(fresh)))) {
|
|
690
|
+
closeTarget(fresh);
|
|
691
|
+
return { ok: false, reason: closed ? "closed" : "failed" };
|
|
692
|
+
}
|
|
693
|
+
want(fresh, "reopened");
|
|
694
|
+
const streaming = await waitFor(() => current === fresh && !retargeting && frameSeen);
|
|
695
|
+
if (closed)
|
|
696
|
+
return { ok: false, reason: "closed" };
|
|
697
|
+
if (!streaming) {
|
|
698
|
+
closeTarget(fresh);
|
|
699
|
+
return { ok: false, reason: "failed" };
|
|
700
|
+
}
|
|
701
|
+
closeTarget(old);
|
|
702
|
+
return { ok: true };
|
|
703
|
+
}
|
|
704
|
+
finally {
|
|
705
|
+
reopening = false;
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
const reject = (reason) => ({ ok: false, reason });
|
|
709
|
+
return {
|
|
710
|
+
subscribe(subscriber) {
|
|
711
|
+
if (closed) {
|
|
712
|
+
subscriber({ type: "closed", data: { reason: "closed" } });
|
|
713
|
+
return () => { };
|
|
714
|
+
}
|
|
715
|
+
for (const type of REPLAYED) {
|
|
716
|
+
if (type === "frame") {
|
|
717
|
+
for (const { dialog } of openDialogs.values())
|
|
718
|
+
subscriber({ type: "dialog", data: dialog });
|
|
719
|
+
}
|
|
720
|
+
const message = latest.get(type);
|
|
721
|
+
if (message !== undefined)
|
|
722
|
+
subscriber(message);
|
|
723
|
+
}
|
|
724
|
+
subscribers.add(subscriber);
|
|
725
|
+
return () => {
|
|
726
|
+
subscribers.delete(subscriber);
|
|
727
|
+
};
|
|
728
|
+
},
|
|
729
|
+
async sendInput(events) {
|
|
730
|
+
if (closed)
|
|
731
|
+
return reject("closed");
|
|
732
|
+
if (!Array.isArray(events))
|
|
733
|
+
return reject("not_array");
|
|
734
|
+
if (events.length > maxBatch)
|
|
735
|
+
return reject("batch_too_large");
|
|
736
|
+
if (!events.every(validEvent))
|
|
737
|
+
return reject("invalid_event");
|
|
738
|
+
const at = now();
|
|
739
|
+
if (at - windowStart >= 1000 || at < windowStart) {
|
|
740
|
+
windowStart = at;
|
|
741
|
+
windowCount = 0;
|
|
742
|
+
}
|
|
743
|
+
if (windowCount + events.length > maxPerSecond)
|
|
744
|
+
return reject("rate_limited");
|
|
745
|
+
if (retargeting) {
|
|
746
|
+
if (queued.length >= MAX_QUEUED_BATCHES)
|
|
747
|
+
return reject("upstream_unavailable");
|
|
748
|
+
windowCount += events.length;
|
|
749
|
+
return new Promise((done) => queued.push({ events, done }));
|
|
750
|
+
}
|
|
751
|
+
if (ws === undefined || ws.readyState !== WS_OPEN)
|
|
752
|
+
return reject("upstream_unavailable");
|
|
753
|
+
windowCount += events.length;
|
|
754
|
+
for (const event of events)
|
|
755
|
+
ws.send(JSON.stringify(event));
|
|
756
|
+
return { ok: true, accepted: events.length };
|
|
757
|
+
},
|
|
758
|
+
async answerDialog(answer) {
|
|
759
|
+
if (closed)
|
|
760
|
+
return { ok: false, reason: "closed" };
|
|
761
|
+
if (!isPlainObject(answer) ||
|
|
762
|
+
typeof answer.id !== "string" ||
|
|
763
|
+
answer.id.length === 0 ||
|
|
764
|
+
answer.id.length > MAX_DIALOG_ID ||
|
|
765
|
+
typeof answer.accept !== "boolean" ||
|
|
766
|
+
(answer.promptText !== undefined &&
|
|
767
|
+
(typeof answer.promptText !== "string" || answer.promptText.length > MAX_DIALOG_TEXT))) {
|
|
768
|
+
return { ok: false, reason: "invalid" };
|
|
769
|
+
}
|
|
770
|
+
const open = openDialogs.get(answer.id);
|
|
771
|
+
if (open === undefined || open.answered)
|
|
772
|
+
return { ok: false, reason: "no_dialog" };
|
|
773
|
+
if (!open.dialog.answerable)
|
|
774
|
+
return { ok: false, reason: "not_answerable" };
|
|
775
|
+
open.answered = true;
|
|
776
|
+
const params = { accept: answer.accept };
|
|
777
|
+
if (answer.accept && open.dialog.kind === "prompt") {
|
|
778
|
+
params.promptText = answer.promptText ?? open.dialog.defaultPrompt;
|
|
779
|
+
}
|
|
780
|
+
const reply = await cdpCall("Page.handleJavaScriptDialog", params, open.sessionId);
|
|
781
|
+
if (closed)
|
|
782
|
+
return { ok: false, reason: "closed" };
|
|
783
|
+
if (reply?.error?.code === CDP_NO_DIALOG)
|
|
784
|
+
return { ok: false, reason: "no_dialog" };
|
|
785
|
+
if (reply === undefined || reply.error !== undefined) {
|
|
786
|
+
open.answered = false;
|
|
787
|
+
return { ok: false, reason: "closed" };
|
|
788
|
+
}
|
|
789
|
+
return { ok: true };
|
|
790
|
+
},
|
|
791
|
+
reopenTab,
|
|
792
|
+
clearFrame() {
|
|
793
|
+
latest.delete("frame");
|
|
794
|
+
},
|
|
795
|
+
async close() {
|
|
796
|
+
finish("closed");
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
}
|