@xyagent/cli 0.0.1
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 +860 -0
- package/bin/agentlink +8585 -0
- package/bin/agentlink-agent +7 -0
- package/bin/agentlink-mcp-stdio +173 -0
- package/docs/LOGGING.md +159 -0
- package/package.json +38 -0
- package/src/core.mjs +320 -0
- package/src/tunnel.mjs +291 -0
- package/src/tunnel_proxy.mjs +913 -0
- package/src/tunnel_service.mjs +699 -0
- package/src/tunnel_state.mjs +301 -0
- package/src/unified_producer.mjs +259 -0
- package/src-ext/README.md +75 -0
- package/src-ext/bin.mjs +128 -0
- package/src-ext/commands/agent.mjs +66 -0
- package/src-ext/commands/help.mjs +31 -0
- package/src-ext/commands/pair.mjs +455 -0
- package/src-ext/commands/reload.mjs +78 -0
- package/src-ext/commands/service.mjs +64 -0
- package/src-ext/core/accountResolver.mjs +137 -0
- package/src-ext/core/activeRuns.mjs +76 -0
- package/src-ext/core/agentlinkToolsBootstrap.mjs +75 -0
- package/src-ext/core/agentlinkToolsRegistry.mjs +323 -0
- package/src-ext/core/agentlinkToolsServer.mjs +109 -0
- package/src-ext/core/approvalGateway.mjs +132 -0
- package/src-ext/core/attachAuditLog.mjs +79 -0
- package/src-ext/core/attachDenylist.mjs +128 -0
- package/src-ext/core/autoTunnelDetector.mjs +98 -0
- package/src-ext/core/autoTunnelGateway.mjs +393 -0
- package/src-ext/core/autoTunnelWiring.mjs +184 -0
- package/src-ext/core/boundedMap.mjs +98 -0
- package/src-ext/core/bridgeAttachAutoScanner.mjs +237 -0
- package/src-ext/core/bridgeHooks.mjs +168 -0
- package/src-ext/core/bridgeSelfUninstall.mjs +133 -0
- package/src-ext/core/claudeTrust.mjs +74 -0
- package/src-ext/core/codexTrust.mjs +77 -0
- package/src-ext/core/cosUploadClient.mjs +326 -0
- package/src-ext/core/createWorkspace.mjs +170 -0
- package/src-ext/core/daemonRestart.mjs +49 -0
- package/src-ext/core/defaultWorkspace.mjs +65 -0
- package/src-ext/core/ensureGitRepo.mjs +57 -0
- package/src-ext/core/hermesProfile.mjs +75 -0
- package/src-ext/core/interruptGateway.mjs +119 -0
- package/src-ext/core/interruptRunListener.mjs +106 -0
- package/src-ext/core/mcpRuntimeFanout.mjs +236 -0
- package/src-ext/core/mcpStdioClient.mjs +316 -0
- package/src-ext/core/openclawGatewayClient.mjs +230 -0
- package/src-ext/core/pairCodeClient.mjs +331 -0
- package/src-ext/core/pairInventory.mjs +59 -0
- package/src-ext/core/pathExpand.mjs +34 -0
- package/src-ext/core/preprocessAttachments.mjs +113 -0
- package/src-ext/core/relayWorker.mjs +284 -0
- package/src-ext/core/relayWorkerHttp.mjs +58 -0
- package/src-ext/core/scanWorkspaces.mjs +842 -0
- package/src-ext/core/sessionSlot.mjs +70 -0
- package/src-ext/core/stateMachine.mjs +85 -0
- package/src-ext/core/toolsServerLifecycle.mjs +169 -0
- package/src-ext/core/unifiedDispatchHandler.mjs +439 -0
- package/src-ext/core/usageReporter.mjs +207 -0
- package/src-ext/openclaw-plugin/envelope-builder.cjs +619 -0
- package/src-ext/openclaw-plugin/index.cjs +1919 -0
- package/src-ext/openclaw-plugin/openclaw.plugin.json +20 -0
- package/src-ext/openclaw-plugin/package.json +12 -0
- package/src-ext/openclaw-plugin/relay-defaults.json +3 -0
- package/src-ext/openclaw-plugin/todoTranslatorUtils.cjs +101 -0
- package/src-ext/runtime/_shared/cliExec.mjs +138 -0
- package/src-ext/runtime/_shared/cosTextRefs.mjs +120 -0
- package/src-ext/runtime/_shared/fetchAndBase64Encode.mjs +52 -0
- package/src-ext/runtime/_shared/relayObjectToBlock.mjs +307 -0
- package/src-ext/runtime/_shared/sessionPicker.mjs +418 -0
- package/src-ext/runtime/_shared/slashCommandRouter.mjs +1346 -0
- package/src-ext/runtime/_shared/todoTranslatorUtils.mjs +94 -0
- package/src-ext/runtime/claude/askUserQuestionTranslator.mjs +241 -0
- package/src-ext/runtime/claude/customCommandsCollector.mjs +132 -0
- package/src-ext/runtime/claude/handleRequest.mjs +1418 -0
- package/src-ext/runtime/claude/index.mjs +160 -0
- package/src-ext/runtime/claude/internalCommandNames.mjs +58 -0
- package/src-ext/runtime/claude/launcher.mjs +313 -0
- package/src-ext/runtime/claude/mcpConfigAdapter.mjs +174 -0
- package/src-ext/runtime/claude/mcpConfigWriter.mjs +57 -0
- package/src-ext/runtime/claude/modelContextWindow.mjs +30 -0
- package/src-ext/runtime/claude/modelScanner.mjs +257 -0
- package/src-ext/runtime/claude/permissionInterruptBlock.mjs +86 -0
- package/src-ext/runtime/claude/permissionPromptServer.mjs +170 -0
- package/src-ext/runtime/claude/permissionPromptServerWorker.mjs +214 -0
- package/src-ext/runtime/claude/preflight.mjs +62 -0
- package/src-ext/runtime/claude/stdoutParser.mjs +365 -0
- package/src-ext/runtime/claude/todoTranslator.mjs +66 -0
- package/src-ext/runtime/claude/usageProbe.mjs +576 -0
- package/src-ext/runtime/claude/usageProbeStrip.mjs +14 -0
- package/src-ext/runtime/codex/codexCustomCommandsCollector.mjs +87 -0
- package/src-ext/runtime/codex/elicitationHandler.mjs +91 -0
- package/src-ext/runtime/codex/eventMapper.mjs +154 -0
- package/src-ext/runtime/codex/handleRequest.mjs +1152 -0
- package/src-ext/runtime/codex/index.mjs +142 -0
- package/src-ext/runtime/codex/mcpConfigAdapter.mjs +155 -0
- package/src-ext/runtime/codex/modelScanner.mjs +305 -0
- package/src-ext/runtime/codex/planNotificationHandler.mjs +61 -0
- package/src-ext/runtime/codex/preflight.mjs +67 -0
- package/src-ext/runtime/hermes/commandBackend.mjs +117 -0
- package/src-ext/runtime/hermes/envSetup.mjs +229 -0
- package/src-ext/runtime/hermes/gatewayManager.mjs +625 -0
- package/src-ext/runtime/hermes/handleRequest.mjs +1257 -0
- package/src-ext/runtime/hermes/httpBackend.mjs +219 -0
- package/src-ext/runtime/hermes/index.mjs +196 -0
- package/src-ext/runtime/hermes/mcpConfigAdapter.mjs +189 -0
- package/src-ext/runtime/hermes/preflight.mjs +359 -0
- package/src-ext/runtime/hermes/responsesBackend.mjs +276 -0
- package/src-ext/runtime/hermes/sessionStore.mjs +78 -0
- package/src-ext/runtime/hermes/todoTranslator.mjs +90 -0
- package/src-ext/runtime/hermes/workspaceRpc.mjs +124 -0
- package/src-ext/runtime/openclaw/buildOpenclawDaemonInput.mjs +143 -0
- package/src-ext/runtime/openclaw/mcpConfigAdapter.mjs +91 -0
- package/src-ext/runtime/openclaw/openclawConfig.mjs +145 -0
- package/src-ext/runtime/picoclaw/constants.mjs +39 -0
- package/src-ext/runtime/picoclaw/handleRequest.mjs +289 -0
- package/src-ext/runtime/picoclaw/index.mjs +314 -0
- package/src-ext/runtime/picoclaw/pairFlow.mjs +224 -0
- package/src-ext/runtime/picoclaw/state.mjs +78 -0
- package/src-ext/runtime/picoclaw/todoTranslator.mjs +67 -0
- package/src-ext/runtime/picoclaw/translator.mjs +272 -0
- package/src-ext/runtime/picoclaw/wsClient.mjs +129 -0
- package/src-ext/service/serviceManager.mjs +521 -0
- package/src-shared/README.md +44 -0
- package/src-shared/chat_push_normalizer.mjs +113 -0
- package/src-shared/correlation.mjs +25 -0
- package/src-shared/envelope_builder.mjs +612 -0
- package/src-shared/logger.mjs +167 -0
- package/src-shared/relay-defaults.json +3 -0
- package/src-shared/relay_revoke.mjs +46 -0
- package/src-shared/vendor/README.md +60 -0
- package/src-shared/vendor/smol-toml/LICENSE +24 -0
- package/src-shared/vendor/smol-toml/README.md +236 -0
- package/src-shared/vendor/smol-toml/VENDORED.md +37 -0
- package/src-shared/vendor/smol-toml/dist/date.d.ts +41 -0
- package/src-shared/vendor/smol-toml/dist/date.js +127 -0
- package/src-shared/vendor/smol-toml/dist/error.d.ts +38 -0
- package/src-shared/vendor/smol-toml/dist/error.js +63 -0
- package/src-shared/vendor/smol-toml/dist/extract.d.ts +30 -0
- package/src-shared/vendor/smol-toml/dist/extract.js +109 -0
- package/src-shared/vendor/smol-toml/dist/index.cjs +900 -0
- package/src-shared/vendor/smol-toml/dist/index.d.ts +43 -0
- package/src-shared/vendor/smol-toml/dist/index.js +33 -0
- package/src-shared/vendor/smol-toml/dist/parse.d.ts +37 -0
- package/src-shared/vendor/smol-toml/dist/parse.js +148 -0
- package/src-shared/vendor/smol-toml/dist/primitive.d.ts +31 -0
- package/src-shared/vendor/smol-toml/dist/primitive.js +178 -0
- package/src-shared/vendor/smol-toml/dist/stringify.d.ts +31 -0
- package/src-shared/vendor/smol-toml/dist/stringify.js +163 -0
- package/src-shared/vendor/smol-toml/dist/struct.d.ts +32 -0
- package/src-shared/vendor/smol-toml/dist/struct.js +194 -0
- package/src-shared/vendor/smol-toml/dist/util.d.ts +42 -0
- package/src-shared/vendor/smol-toml/dist/util.js +100 -0
- package/src-shared/vendor/smol-toml/package.json +54 -0
- package/src-shared/vendor/yaml/LICENSE +13 -0
- package/src-shared/vendor/yaml/README.md +155 -0
- package/src-shared/vendor/yaml/VENDORED.md +29 -0
- package/src-shared/vendor/yaml/bin.mjs +11 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-collection.js +88 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-doc.js +42 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-node.js +92 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-scalar.js +80 -0
- package/src-shared/vendor/yaml/browser/dist/compose/composer.js +217 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-map.js +113 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-seq.js +47 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-end.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-collection.js +203 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-scalar.js +223 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-props.js +148 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-contains-newline.js +34 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-empty-scalar-position.js +27 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-map-includes.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/doc/Document.js +334 -0
- package/src-shared/vendor/yaml/browser/dist/doc/anchors.js +72 -0
- package/src-shared/vendor/yaml/browser/dist/doc/applyReviver.js +55 -0
- package/src-shared/vendor/yaml/browser/dist/doc/createNode.js +89 -0
- package/src-shared/vendor/yaml/browser/dist/doc/directives.js +176 -0
- package/src-shared/vendor/yaml/browser/dist/errors.js +57 -0
- package/src-shared/vendor/yaml/browser/dist/index.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/log.js +14 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Alias.js +101 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Collection.js +147 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Node.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Pair.js +36 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Scalar.js +24 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/YAMLMap.js +144 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/addPairToJSMap.js +104 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/identity.js +36 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/toJS.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-scalar.js +214 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-stringify.js +61 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-visit.js +97 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst.js +98 -0
- package/src-shared/vendor/yaml/browser/dist/parse/lexer.js +717 -0
- package/src-shared/vendor/yaml/browser/dist/parse/line-counter.js +39 -0
- package/src-shared/vendor/yaml/browser/dist/parse/parser.js +954 -0
- package/src-shared/vendor/yaml/browser/dist/public-api.js +99 -0
- package/src-shared/vendor/yaml/browser/dist/schema/Schema.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/map.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/null.js +15 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/seq.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/string.js +14 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/bool.js +19 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/float.js +43 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/int.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/schema.js +23 -0
- package/src-shared/vendor/yaml/browser/dist/schema/json/schema.js +62 -0
- package/src-shared/vendor/yaml/browser/dist/schema/tags.js +83 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/binary.js +66 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/schema.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringify.js +124 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyCollection.js +143 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyComment.js +20 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyNumber.js +24 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyPair.js +150 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyString.js +328 -0
- package/src-shared/vendor/yaml/browser/dist/util.js +11 -0
- package/src-shared/vendor/yaml/browser/dist/visit.js +233 -0
- package/src-shared/vendor/yaml/browser/index.js +5 -0
- package/src-shared/vendor/yaml/browser/package.json +3 -0
- package/src-shared/vendor/yaml/dist/cli.d.ts +8 -0
- package/src-shared/vendor/yaml/dist/cli.mjs +199 -0
- package/src-shared/vendor/yaml/dist/compose/compose-collection.d.ts +11 -0
- package/src-shared/vendor/yaml/dist/compose/compose-collection.js +90 -0
- package/src-shared/vendor/yaml/dist/compose/compose-doc.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/compose/compose-doc.js +44 -0
- package/src-shared/vendor/yaml/dist/compose/compose-node.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/compose/compose-node.js +95 -0
- package/src-shared/vendor/yaml/dist/compose/compose-scalar.d.ts +5 -0
- package/src-shared/vendor/yaml/dist/compose/compose-scalar.js +82 -0
- package/src-shared/vendor/yaml/dist/compose/composer.d.ts +62 -0
- package/src-shared/vendor/yaml/dist/compose/composer.js +221 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-map.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.js +200 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-end.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-end.js +39 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.js +205 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-props.d.ts +23 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-props.js +150 -0
- package/src-shared/vendor/yaml/dist/compose/util-contains-newline.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/compose/util-contains-newline.js +36 -0
- package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.js +29 -0
- package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.js +17 -0
- package/src-shared/vendor/yaml/dist/compose/util-map-includes.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/compose/util-map-includes.js +19 -0
- package/src-shared/vendor/yaml/dist/doc/Document.d.ts +141 -0
- package/src-shared/vendor/yaml/dist/doc/Document.js +336 -0
- package/src-shared/vendor/yaml/dist/doc/anchors.d.ts +24 -0
- package/src-shared/vendor/yaml/dist/doc/anchors.js +77 -0
- package/src-shared/vendor/yaml/dist/doc/applyReviver.d.ts +9 -0
- package/src-shared/vendor/yaml/dist/doc/applyReviver.js +57 -0
- package/src-shared/vendor/yaml/dist/doc/createNode.d.ts +17 -0
- package/src-shared/vendor/yaml/dist/doc/createNode.js +91 -0
- package/src-shared/vendor/yaml/dist/doc/directives.d.ts +49 -0
- package/src-shared/vendor/yaml/dist/doc/directives.js +178 -0
- package/src-shared/vendor/yaml/dist/errors.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/errors.js +62 -0
- package/src-shared/vendor/yaml/dist/index.d.ts +22 -0
- package/src-shared/vendor/yaml/dist/index.js +50 -0
- package/src-shared/vendor/yaml/dist/log.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/log.js +17 -0
- package/src-shared/vendor/yaml/dist/nodes/Alias.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/nodes/Alias.js +103 -0
- package/src-shared/vendor/yaml/dist/nodes/Collection.d.ts +73 -0
- package/src-shared/vendor/yaml/dist/nodes/Collection.js +151 -0
- package/src-shared/vendor/yaml/dist/nodes/Node.d.ts +47 -0
- package/src-shared/vendor/yaml/dist/nodes/Node.js +40 -0
- package/src-shared/vendor/yaml/dist/nodes/Pair.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/nodes/Pair.js +39 -0
- package/src-shared/vendor/yaml/dist/nodes/Scalar.d.ts +42 -0
- package/src-shared/vendor/yaml/dist/nodes/Scalar.js +27 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLMap.d.ts +53 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLMap.js +147 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.js +115 -0
- package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.js +106 -0
- package/src-shared/vendor/yaml/dist/nodes/identity.d.ts +23 -0
- package/src-shared/vendor/yaml/dist/nodes/identity.js +53 -0
- package/src-shared/vendor/yaml/dist/nodes/toJS.d.ts +27 -0
- package/src-shared/vendor/yaml/dist/nodes/toJS.js +39 -0
- package/src-shared/vendor/yaml/dist/options.d.ts +338 -0
- package/src-shared/vendor/yaml/dist/parse/cst-scalar.d.ts +64 -0
- package/src-shared/vendor/yaml/dist/parse/cst-scalar.js +218 -0
- package/src-shared/vendor/yaml/dist/parse/cst-stringify.d.ts +8 -0
- package/src-shared/vendor/yaml/dist/parse/cst-stringify.js +63 -0
- package/src-shared/vendor/yaml/dist/parse/cst-visit.d.ts +39 -0
- package/src-shared/vendor/yaml/dist/parse/cst-visit.js +99 -0
- package/src-shared/vendor/yaml/dist/parse/cst.d.ts +108 -0
- package/src-shared/vendor/yaml/dist/parse/cst.js +112 -0
- package/src-shared/vendor/yaml/dist/parse/lexer.d.ts +87 -0
- package/src-shared/vendor/yaml/dist/parse/lexer.js +719 -0
- package/src-shared/vendor/yaml/dist/parse/line-counter.d.ts +22 -0
- package/src-shared/vendor/yaml/dist/parse/line-counter.js +41 -0
- package/src-shared/vendor/yaml/dist/parse/parser.d.ts +84 -0
- package/src-shared/vendor/yaml/dist/parse/parser.js +958 -0
- package/src-shared/vendor/yaml/dist/public-api.d.ts +43 -0
- package/src-shared/vendor/yaml/dist/public-api.js +104 -0
- package/src-shared/vendor/yaml/dist/schema/Schema.d.ts +18 -0
- package/src-shared/vendor/yaml/dist/schema/Schema.js +40 -0
- package/src-shared/vendor/yaml/dist/schema/common/map.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/map.js +19 -0
- package/src-shared/vendor/yaml/dist/schema/common/null.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/common/null.js +17 -0
- package/src-shared/vendor/yaml/dist/schema/common/seq.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/seq.js +19 -0
- package/src-shared/vendor/yaml/dist/schema/common/string.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/string.js +16 -0
- package/src-shared/vendor/yaml/dist/schema/core/bool.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/bool.js +21 -0
- package/src-shared/vendor/yaml/dist/schema/core/float.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/float.js +47 -0
- package/src-shared/vendor/yaml/dist/schema/core/int.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/int.js +42 -0
- package/src-shared/vendor/yaml/dist/schema/core/schema.d.ts +1 -0
- package/src-shared/vendor/yaml/dist/schema/core/schema.js +25 -0
- package/src-shared/vendor/yaml/dist/schema/json/schema.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/json/schema.js +64 -0
- package/src-shared/vendor/yaml/dist/schema/json-schema.d.ts +69 -0
- package/src-shared/vendor/yaml/dist/schema/tags.d.ts +40 -0
- package/src-shared/vendor/yaml/dist/schema/tags.js +86 -0
- package/src-shared/vendor/yaml/dist/schema/types.d.ts +90 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.js +68 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.js +29 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.js +50 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.js +76 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.js +77 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.js +96 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
- package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
- package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.js +151 -0
- package/src-shared/vendor/yaml/dist/stringify/stringify.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/stringify/stringify.js +127 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.js +145 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyComment.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyComment.js +24 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.js +87 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.js +26 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyPair.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyPair.js +152 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyString.d.ts +9 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyString.js +330 -0
- package/src-shared/vendor/yaml/dist/test-events.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/test-events.js +134 -0
- package/src-shared/vendor/yaml/dist/util.d.ts +12 -0
- package/src-shared/vendor/yaml/dist/util.js +28 -0
- package/src-shared/vendor/yaml/dist/visit.d.ts +102 -0
- package/src-shared/vendor/yaml/dist/visit.js +236 -0
- package/src-shared/vendor/yaml/package.json +96 -0
- package/src-shared/vendor/yaml/util.js +2 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
// tunnel.mjs 拆分:tunnel-agent 长轮询 + HTTP/WS/TCP 代理。
|
|
2
|
+
//
|
|
3
|
+
// 这层只做"作为 agent 在 relay 和本地服务之间转发流量",不知道 CLI、不知道
|
|
4
|
+
// state 文件,只接受参数(relayUrl/tunnelID/tunnelToken/targetBaseUrl/protocol)
|
|
5
|
+
// 然后跑 long-poll 循环 / 心跳 / 会话维护。
|
|
6
|
+
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
|
|
9
|
+
import { trimSlash } from "./core.mjs";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
12
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
13
|
+
DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
14
|
+
DEFAULT_POLL_WAIT_SECONDS,
|
|
15
|
+
DEFAULT_STREAM_WAIT_TIMEOUT_MS,
|
|
16
|
+
TUNNEL_PROTOCOL_HTTP,
|
|
17
|
+
TUNNEL_PROTOCOL_WS,
|
|
18
|
+
TUNNEL_PROTOCOL_TCP,
|
|
19
|
+
TUNNEL_IDLE_LOG_EVERY,
|
|
20
|
+
TUNNEL_HEARTBEAT_ERROR_LOG_EVERY,
|
|
21
|
+
TUNNEL_STREAM_IDLE_LOG_EVERY,
|
|
22
|
+
fetchJson,
|
|
23
|
+
withServerToken,
|
|
24
|
+
extractJsonError,
|
|
25
|
+
isHopByHopHeader,
|
|
26
|
+
joinTunnelTargetUrl,
|
|
27
|
+
parseTunnelTCPAddress,
|
|
28
|
+
normalizeWebSocketPayload,
|
|
29
|
+
summarizeTunnelRequest,
|
|
30
|
+
decodeBase64Length,
|
|
31
|
+
sleep,
|
|
32
|
+
normalizeTunnelProtocol,
|
|
33
|
+
} from "./tunnel_state.mjs";
|
|
34
|
+
|
|
35
|
+
// ---- 远端 tunnel CRUD ----
|
|
36
|
+
|
|
37
|
+
export async function createTunnel({ relayUrl, localPort, protocol, targetBaseUrl, serverToken, fail }) {
|
|
38
|
+
const response = await fetchJson(`${relayUrl}/v1/tunnels`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: withServerToken(serverToken),
|
|
41
|
+
body: JSON.stringify({ protocol, localPort, targetBaseUrl }),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
fail(extractJsonError(response.body, `create tunnel failed (HTTP ${response.status})`));
|
|
45
|
+
}
|
|
46
|
+
const tunnel = response.body?.tunnel;
|
|
47
|
+
if (!tunnel?.id || !tunnel?.agentToken || !tunnel?.publicUrl) {
|
|
48
|
+
fail("create tunnel returned incomplete payload");
|
|
49
|
+
}
|
|
50
|
+
tunnel.protocol = normalizeTunnelProtocol(tunnel.protocol || protocol, fail);
|
|
51
|
+
return tunnel;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function safeDeleteRemoteTunnel(state) {
|
|
55
|
+
if (!state?.id || !state?.token || !state?.relayUrl) return;
|
|
56
|
+
try {
|
|
57
|
+
await fetchJson(`${trimSlash(state.relayUrl)}/v1/tunnels/${encodeURIComponent(state.id)}`, {
|
|
58
|
+
method: "DELETE",
|
|
59
|
+
headers: { Authorization: `Bearer ${state.token}` },
|
|
60
|
+
});
|
|
61
|
+
} catch { /* best effort */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- agent 长轮询 ----
|
|
65
|
+
|
|
66
|
+
export async function readNextTunnelRequest({ relayUrl, tunnelID, tunnelToken }) {
|
|
67
|
+
const response = await fetchJson(
|
|
68
|
+
`${relayUrl}/v1/tunnels/${encodeURIComponent(tunnelID)}/requests/next`,
|
|
69
|
+
{ method: "GET", headers: { Authorization: `Bearer ${tunnelToken}` } },
|
|
70
|
+
DEFAULT_REQUEST_TIMEOUT_MS + DEFAULT_POLL_WAIT_SECONDS * 1000,
|
|
71
|
+
);
|
|
72
|
+
if (response.status === 204) return null;
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(extractJsonError(response.body, `read tunnel request failed (HTTP ${response.status})`));
|
|
75
|
+
}
|
|
76
|
+
return response.body?.request ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function publishTunnelResponse({ relayUrl, tunnelID, tunnelToken, requestID, response }) {
|
|
80
|
+
const result = await fetchJson(
|
|
81
|
+
`${relayUrl}/v1/tunnels/${encodeURIComponent(tunnelID)}/responses/${encodeURIComponent(requestID)}/publish`,
|
|
82
|
+
{
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { Authorization: `Bearer ${tunnelToken}` },
|
|
85
|
+
body: JSON.stringify(response),
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
throw new Error(extractJsonError(result.body, `publish tunnel response failed (HTTP ${result.status})`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function readNextTunnelStreamEvent({ relayUrl, tunnelID, tunnelToken }) {
|
|
94
|
+
const response = await fetchJson(
|
|
95
|
+
`${relayUrl}/v1/tunnels/${encodeURIComponent(tunnelID)}/streams/next`,
|
|
96
|
+
{ method: "GET", headers: { Authorization: `Bearer ${tunnelToken}` } },
|
|
97
|
+
DEFAULT_STREAM_WAIT_TIMEOUT_MS + DEFAULT_POLL_WAIT_SECONDS * 1000,
|
|
98
|
+
);
|
|
99
|
+
if (response.status === 204) return null;
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(extractJsonError(response.body, `read tunnel stream event failed (HTTP ${response.status})`));
|
|
102
|
+
}
|
|
103
|
+
return response.body?.event ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function publishTunnelSessionEvent({ relayUrl, tunnelID, tunnelToken, sessionID, event }) {
|
|
107
|
+
const result = await fetchJson(
|
|
108
|
+
`${relayUrl}/v1/tunnels/${encodeURIComponent(tunnelID)}/sessions/${encodeURIComponent(sessionID)}/publish`,
|
|
109
|
+
{
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { Authorization: `Bearer ${tunnelToken}` },
|
|
112
|
+
body: JSON.stringify(event),
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
throw new Error(extractJsonError(result.body, `publish tunnel session event failed (HTTP ${result.status})`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- agent 心跳 ----
|
|
121
|
+
|
|
122
|
+
async function sendTunnelHeartbeat({ relayUrl, tunnelID, tunnelToken }) {
|
|
123
|
+
const result = await fetchJson(
|
|
124
|
+
`${relayUrl}/v1/tunnels/${encodeURIComponent(tunnelID)}/heartbeat`,
|
|
125
|
+
{ method: "POST", headers: { Authorization: `Bearer ${tunnelToken}` } },
|
|
126
|
+
DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
127
|
+
);
|
|
128
|
+
if (!result.ok) {
|
|
129
|
+
throw new Error(extractJsonError(result.body, `heartbeat failed (HTTP ${result.status})`));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function startTunnelHeartbeat({ relayUrl, tunnelID, tunnelToken, logger }) {
|
|
134
|
+
let stopped = false;
|
|
135
|
+
let failureCount = 0;
|
|
136
|
+
const tick = async (phase) => {
|
|
137
|
+
if (stopped) return;
|
|
138
|
+
try {
|
|
139
|
+
await sendTunnelHeartbeat({ relayUrl, tunnelID, tunnelToken });
|
|
140
|
+
if (failureCount > 0) {
|
|
141
|
+
logger.info("heartbeat recovered", { failures: failureCount, phase });
|
|
142
|
+
}
|
|
143
|
+
failureCount = 0;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
failureCount += 1;
|
|
146
|
+
if (failureCount === 1 || failureCount % TUNNEL_HEARTBEAT_ERROR_LOG_EVERY === 0) {
|
|
147
|
+
logger.error("heartbeat failed", {
|
|
148
|
+
phase, failures: failureCount,
|
|
149
|
+
error: String(error instanceof Error ? error.message : error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const timer = setInterval(() => { tick("interval").catch(() => {}); }, DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
156
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
157
|
+
return {
|
|
158
|
+
sendNow(phase = "manual") { return tick(phase); },
|
|
159
|
+
stop() { stopped = true; clearInterval(timer); },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- HTTP 代理 ----
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Tunnel path rewriting & runtime shim injection
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// 被代理应用 (Next.js / Vite / 任何带前端构建产物的 SPA) 不知道自己挂在
|
|
169
|
+
// `/t/<slug>/` 下。它产的 absolute path 资源 (`/_next/...`)、Location 头、
|
|
170
|
+
// CSS url()、JS 里 `fetch("/api/...")`、SPA router 写 history 时调 pushState、
|
|
171
|
+
// 等等,都会落到 origin 根上,绕过 tunnel 路由直接 404。
|
|
172
|
+
//
|
|
173
|
+
// 两层补丁:
|
|
174
|
+
// 1. 静态层: 扫文本改写 HTML 属性 / CSS url() / Link / Location 等所有能
|
|
175
|
+
// 用 regex 安全匹配的 absolute path。这层零运行时副作用。
|
|
176
|
+
// 2. 运行时层: 往 HTML 注一段 inline shim,hook fetch / XHR / WebSocket /
|
|
177
|
+
// EventSource / sendBeacon / history.pushState/replaceState /
|
|
178
|
+
// Location.pathname/href getter,以及 MutationObserver 跟踪后续 DOM。
|
|
179
|
+
// shim 保证 JS 在运行时拼出来的 absolute path / SPA 路由也带前缀。
|
|
180
|
+
|
|
181
|
+
// 静态属性扫描覆盖的 URL 属性集合 —— 引导 HTML rewriter 一次性命中所有
|
|
182
|
+
// 单值 URL 属性。`srcset` / `imagesrcset` 是逗号分隔的多值,走单独的 regex。
|
|
183
|
+
const SINGLE_URL_ATTRS = [
|
|
184
|
+
"href", "src", "action", "formaction",
|
|
185
|
+
"data-href", "data-src", "poster",
|
|
186
|
+
"cite", "data", "manifest", "longdesc", "background",
|
|
187
|
+
];
|
|
188
|
+
const URL_ATTR_RE = new RegExp(
|
|
189
|
+
`\\b(${SINGLE_URL_ATTRS.join("|")})=(['"])\\/(?!\\/)`,
|
|
190
|
+
"gi",
|
|
191
|
+
);
|
|
192
|
+
// srcset / imagesrcset 形如 `"/a.jpg 1x, /b.jpg 2x, /c.jpg"`,
|
|
193
|
+
// 抓整个属性值后逐项处理。
|
|
194
|
+
const SRCSET_ATTR_RE = /\b(srcset|imagesrcset)=(['"])([^'"]*)\2/gi;
|
|
195
|
+
// `<meta http-equiv="refresh" content="0;url=/foo">` —— content 里嵌的 URL。
|
|
196
|
+
const META_REFRESH_RE =
|
|
197
|
+
/(<meta\b[^>]*\bhttp-equiv\s*=\s*['"]?refresh['"]?[^>]*\bcontent\s*=\s*['"][^'"]*\burl\s*=\s*)\/(?!\/)/gi;
|
|
198
|
+
// CSS url(/foo) / url('/foo') / url("/foo) —— `(?!\/)` 排除 protocol-relative。
|
|
199
|
+
const CSS_URL_RE = /\burl\(\s*(['"]?)\/(?!\/)/gi;
|
|
200
|
+
// CSS @import "/foo" (不带 url() 的裸引号形式)。
|
|
201
|
+
const CSS_IMPORT_RE = /(@import\s+)(['"])\/(?!\/)/gi;
|
|
202
|
+
|
|
203
|
+
function trimTrailingSlash(s) {
|
|
204
|
+
return String(s || "").replace(/\/+$/, "");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// rewriteSrcsetValue 改写 srcset / imagesrcset 属性值。逗号分隔多 URL,
|
|
208
|
+
// 每段允许追加 descriptor (`1x` / `300w`),descriptor 后再带逗号或结束。
|
|
209
|
+
function rewriteSrcsetValue(value, prefix) {
|
|
210
|
+
return value
|
|
211
|
+
.split(",")
|
|
212
|
+
.map((part) => {
|
|
213
|
+
const trimmed = part.trim();
|
|
214
|
+
if (!trimmed) return part;
|
|
215
|
+
const m = trimmed.match(/^(\S+)(\s.*)?$/);
|
|
216
|
+
if (!m) return part;
|
|
217
|
+
const url = m[1];
|
|
218
|
+
const rest = m[2] || "";
|
|
219
|
+
const leading = part.slice(0, part.length - trimmed.length);
|
|
220
|
+
if (url.startsWith("/") && !url.startsWith("//") &&
|
|
221
|
+
url !== prefix && !url.startsWith(prefix + "/")
|
|
222
|
+
) {
|
|
223
|
+
return `${leading}${prefix}${url}${rest}`;
|
|
224
|
+
}
|
|
225
|
+
return part;
|
|
226
|
+
})
|
|
227
|
+
.join(",");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function rewriteHtmlAbsolutePaths(html, prefix) {
|
|
231
|
+
const prefixSafe = trimTrailingSlash(prefix);
|
|
232
|
+
if (!prefixSafe) return html;
|
|
233
|
+
return html
|
|
234
|
+
.replace(URL_ATTR_RE, (_full, attr, quote) =>
|
|
235
|
+
`${attr}=${quote}${prefixSafe}/`,
|
|
236
|
+
)
|
|
237
|
+
.replace(SRCSET_ATTR_RE, (_full, attr, quote, val) =>
|
|
238
|
+
`${attr}=${quote}${rewriteSrcsetValue(val, prefixSafe)}${quote}`,
|
|
239
|
+
)
|
|
240
|
+
.replace(META_REFRESH_RE, (_full, head) => `${head}${prefixSafe}/`)
|
|
241
|
+
.replace(CSS_URL_RE, (_match, quote) => `url(${quote}${prefixSafe}/`)
|
|
242
|
+
.replace(CSS_IMPORT_RE, (_full, head, quote) => `${head}${quote}${prefixSafe}/`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// CSS 文件 (`text/css` 响应) 的改写。HTML rewriter 只扫 HTML 里的 inline
|
|
246
|
+
// `<style>` 和 url() —— 外链 `.css` 文件里写的 url() 和 @import 不在它的覆盖范围
|
|
247
|
+
// (响应不是 text/html),所以这里单独跑一遍。
|
|
248
|
+
export function rewriteCssAbsolutePaths(css, prefix) {
|
|
249
|
+
const prefixSafe = trimTrailingSlash(prefix);
|
|
250
|
+
if (!prefixSafe) return css;
|
|
251
|
+
return css
|
|
252
|
+
.replace(CSS_URL_RE, (_match, quote) => `url(${quote}${prefixSafe}/`)
|
|
253
|
+
.replace(CSS_IMPORT_RE, (_full, head, quote) => `${head}${quote}${prefixSafe}/`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 3xx 响应的 Location 头。`/login` → `/t/<slug>/login`;跨域 / protocol-relative
|
|
257
|
+
// 不动 (浏览器自己跳走,不归 tunnel 管)。
|
|
258
|
+
export function rewriteLocationHeader(value, prefix) {
|
|
259
|
+
const prefixSafe = trimTrailingSlash(prefix);
|
|
260
|
+
if (!prefixSafe) return value;
|
|
261
|
+
const v = String(value || "");
|
|
262
|
+
if (!v.startsWith("/") || v.startsWith("//")) return v;
|
|
263
|
+
if (v === prefixSafe || v.startsWith(prefixSafe + "/")) return v;
|
|
264
|
+
return prefixSafe + v;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 运行时 shim —— 注入到 HTML 顶端,patch 所有运行时 URL 入口。
|
|
268
|
+
// 占位 `__HTTP_PREFIX__` / `__WS_PREFIX__` 由 buildShimScript 替换。
|
|
269
|
+
const TUNNEL_SHIM_TEMPLATE = `
|
|
270
|
+
(function(){
|
|
271
|
+
if (window.__BUBBO_TUNNEL_SHIM__) return;
|
|
272
|
+
window.__BUBBO_TUNNEL_SHIM__ = true;
|
|
273
|
+
|
|
274
|
+
var HTTP_PREFIX = "__HTTP_PREFIX__";
|
|
275
|
+
var WS_PREFIX = "__WS_PREFIX__";
|
|
276
|
+
|
|
277
|
+
function isAbsPath(u){
|
|
278
|
+
return typeof u === "string" && u.length > 0 && u.charAt(0) === "/" && u.charAt(1) !== "/";
|
|
279
|
+
}
|
|
280
|
+
function hasHttpPrefix(u){
|
|
281
|
+
return u === HTTP_PREFIX || u.indexOf(HTTP_PREFIX + "/") === 0;
|
|
282
|
+
}
|
|
283
|
+
function fixHttp(u){
|
|
284
|
+
if (!isAbsPath(u)) return u;
|
|
285
|
+
if (hasHttpPrefix(u)) return u;
|
|
286
|
+
return HTTP_PREFIX + u;
|
|
287
|
+
}
|
|
288
|
+
function stripHttp(p){
|
|
289
|
+
if (typeof p !== "string") return p;
|
|
290
|
+
if (p === HTTP_PREFIX) return "/";
|
|
291
|
+
if (p.indexOf(HTTP_PREFIX + "/") === 0) return p.slice(HTTP_PREFIX.length);
|
|
292
|
+
return p;
|
|
293
|
+
}
|
|
294
|
+
function sameOrigin(u){
|
|
295
|
+
try { return u.host === location.host; } catch(e){ return false; }
|
|
296
|
+
}
|
|
297
|
+
function fixHttpUrl(s){
|
|
298
|
+
if (typeof s !== "string") return s;
|
|
299
|
+
if (isAbsPath(s)) return fixHttp(s);
|
|
300
|
+
try {
|
|
301
|
+
var u = new URL(s, location.href);
|
|
302
|
+
if (sameOrigin(u) && !hasHttpPrefix(u.pathname)) {
|
|
303
|
+
u.pathname = HTTP_PREFIX + (u.pathname === "" ? "/" : u.pathname);
|
|
304
|
+
return u.toString();
|
|
305
|
+
}
|
|
306
|
+
} catch(e){}
|
|
307
|
+
return s;
|
|
308
|
+
}
|
|
309
|
+
function fixWs(u){
|
|
310
|
+
if (typeof u !== "string") return u;
|
|
311
|
+
if (isAbsPath(u)) {
|
|
312
|
+
if (u === WS_PREFIX || u.indexOf(WS_PREFIX + "/") === 0) return u;
|
|
313
|
+
return WS_PREFIX + u;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
var parsed = new URL(u, location.href);
|
|
317
|
+
if ((parsed.protocol === "ws:" || parsed.protocol === "wss:") && sameOrigin(parsed)) {
|
|
318
|
+
if (parsed.pathname !== WS_PREFIX && parsed.pathname.indexOf(WS_PREFIX + "/") !== 0) {
|
|
319
|
+
parsed.pathname = WS_PREFIX + (parsed.pathname === "" ? "/" : parsed.pathname);
|
|
320
|
+
return parsed.toString();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch(e){}
|
|
324
|
+
return u;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 1) fetch
|
|
328
|
+
if (typeof window.fetch === "function") {
|
|
329
|
+
var origFetch = window.fetch.bind(window);
|
|
330
|
+
window.fetch = function(input, init){
|
|
331
|
+
try {
|
|
332
|
+
if (typeof input === "string") {
|
|
333
|
+
return origFetch(fixHttpUrl(input), init);
|
|
334
|
+
}
|
|
335
|
+
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
336
|
+
var url = new URL(input.url);
|
|
337
|
+
if (sameOrigin(url) && !hasHttpPrefix(url.pathname)) {
|
|
338
|
+
url.pathname = HTTP_PREFIX + url.pathname;
|
|
339
|
+
return origFetch(new Request(url.toString(), input), init);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch(e){}
|
|
343
|
+
return origFetch(input, init);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 2) XMLHttpRequest
|
|
348
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
349
|
+
var xhrOpen = XMLHttpRequest.prototype.open;
|
|
350
|
+
XMLHttpRequest.prototype.open = function(method, url){
|
|
351
|
+
try { arguments[1] = fixHttpUrl(url); } catch(e){}
|
|
352
|
+
return xhrOpen.apply(this, arguments);
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 3) WebSocket
|
|
357
|
+
if (typeof window.WebSocket === "function") {
|
|
358
|
+
var OrigWS = window.WebSocket;
|
|
359
|
+
function PatchedWS(url, protocols){
|
|
360
|
+
return arguments.length >= 2 ? new OrigWS(fixWs(url), protocols) : new OrigWS(fixWs(url));
|
|
361
|
+
}
|
|
362
|
+
PatchedWS.prototype = OrigWS.prototype;
|
|
363
|
+
PatchedWS.CONNECTING = OrigWS.CONNECTING;
|
|
364
|
+
PatchedWS.OPEN = OrigWS.OPEN;
|
|
365
|
+
PatchedWS.CLOSING = OrigWS.CLOSING;
|
|
366
|
+
PatchedWS.CLOSED = OrigWS.CLOSED;
|
|
367
|
+
window.WebSocket = PatchedWS;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 4) EventSource (SSE)
|
|
371
|
+
if (typeof window.EventSource === "function") {
|
|
372
|
+
var OrigES = window.EventSource;
|
|
373
|
+
function PatchedES(url, init){
|
|
374
|
+
return arguments.length >= 2 ? new OrigES(fixHttpUrl(url), init) : new OrigES(fixHttpUrl(url));
|
|
375
|
+
}
|
|
376
|
+
PatchedES.prototype = OrigES.prototype;
|
|
377
|
+
window.EventSource = PatchedES;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 5) sendBeacon
|
|
381
|
+
if (navigator && typeof navigator.sendBeacon === "function") {
|
|
382
|
+
var origBeacon = navigator.sendBeacon.bind(navigator);
|
|
383
|
+
navigator.sendBeacon = function(url, data){
|
|
384
|
+
return origBeacon(fixHttpUrl(url), data);
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 6) history.pushState / replaceState
|
|
389
|
+
["pushState","replaceState"].forEach(function(name){
|
|
390
|
+
var orig = history[name];
|
|
391
|
+
if (typeof orig !== "function") return;
|
|
392
|
+
history[name] = function(state, title, url){
|
|
393
|
+
if (typeof url === "string") url = fixHttpUrl(url);
|
|
394
|
+
return orig.call(this, state, title, url);
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// 7) Location.pathname / href getter — 让 SPA router 看到剥掉前缀的 path,
|
|
399
|
+
// 否则 router 把 /t/<slug>/foo 当成路由的一部分,根本匹配不到 /foo。
|
|
400
|
+
// setter 同样兜底:app 写 location.pathname = "/foo" 时自动补前缀。
|
|
401
|
+
try {
|
|
402
|
+
var Loc = Location.prototype;
|
|
403
|
+
var pathDesc = Object.getOwnPropertyDescriptor(Loc, "pathname");
|
|
404
|
+
if (pathDesc && pathDesc.configurable && pathDesc.get) {
|
|
405
|
+
var origPathGet = pathDesc.get, origPathSet = pathDesc.set;
|
|
406
|
+
Object.defineProperty(Loc, "pathname", {
|
|
407
|
+
configurable: true,
|
|
408
|
+
enumerable: pathDesc.enumerable,
|
|
409
|
+
get: function(){ return stripHttp(origPathGet.call(this)); },
|
|
410
|
+
set: function(v){ return origPathSet.call(this, fixHttp(v)); },
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
var hrefDesc = Object.getOwnPropertyDescriptor(Loc, "href");
|
|
414
|
+
if (hrefDesc && hrefDesc.configurable && hrefDesc.get) {
|
|
415
|
+
var origHrefGet = hrefDesc.get, origHrefSet = hrefDesc.set;
|
|
416
|
+
Object.defineProperty(Loc, "href", {
|
|
417
|
+
configurable: true,
|
|
418
|
+
enumerable: hrefDesc.enumerable,
|
|
419
|
+
get: function(){
|
|
420
|
+
var real = origHrefGet.call(this);
|
|
421
|
+
try {
|
|
422
|
+
var u = new URL(real);
|
|
423
|
+
if (u.pathname === HTTP_PREFIX) { u.pathname = "/"; return u.toString(); }
|
|
424
|
+
if (u.pathname.indexOf(HTTP_PREFIX + "/") === 0) {
|
|
425
|
+
u.pathname = u.pathname.slice(HTTP_PREFIX.length);
|
|
426
|
+
return u.toString();
|
|
427
|
+
}
|
|
428
|
+
} catch(e){}
|
|
429
|
+
return real;
|
|
430
|
+
},
|
|
431
|
+
set: function(v){ return origHrefSet.call(this, fixHttpUrl(v)); },
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
// toString() 等价 href getter
|
|
435
|
+
if (typeof Loc.toString === "function") {
|
|
436
|
+
var origToString = Loc.toString;
|
|
437
|
+
Loc.toString = function(){ return this.href; };
|
|
438
|
+
// 保留旧 origToString 引用避免被 GC,但用 hrefGet 输出。
|
|
439
|
+
void origToString;
|
|
440
|
+
}
|
|
441
|
+
} catch(e){ /* Safari / 古浏览器 patch 失败,降级:SPA 路由可能不工作 */ }
|
|
442
|
+
|
|
443
|
+
// 8) MutationObserver —— 跟踪 JS 后续 inject 的 <a>/<img>/<link>/<script> 等
|
|
444
|
+
// 与已有元素的 attribute 动态变化。
|
|
445
|
+
var DYN_ATTRS = ["href","src","action","formaction","poster","data","cite","manifest","longdesc","background"];
|
|
446
|
+
function rewriteAttrs(el){
|
|
447
|
+
if (!el || el.nodeType !== 1) return;
|
|
448
|
+
for (var i = 0; i < DYN_ATTRS.length; i++) {
|
|
449
|
+
var a = DYN_ATTRS[i];
|
|
450
|
+
if (!el.hasAttribute || !el.hasAttribute(a)) continue;
|
|
451
|
+
var v = el.getAttribute(a);
|
|
452
|
+
if (isAbsPath(v) && !hasHttpPrefix(v)) el.setAttribute(a, HTTP_PREFIX + v);
|
|
453
|
+
}
|
|
454
|
+
if (el.hasAttribute && el.hasAttribute("srcset")) {
|
|
455
|
+
el.setAttribute("srcset", rewriteSrcsetAttr(el.getAttribute("srcset")));
|
|
456
|
+
}
|
|
457
|
+
if (el.hasAttribute && el.hasAttribute("imagesrcset")) {
|
|
458
|
+
el.setAttribute("imagesrcset", rewriteSrcsetAttr(el.getAttribute("imagesrcset")));
|
|
459
|
+
}
|
|
460
|
+
if (el.children) {
|
|
461
|
+
for (var j = 0; j < el.children.length; j++) rewriteAttrs(el.children[j]);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function rewriteSrcsetAttr(v){
|
|
465
|
+
if (!v) return v;
|
|
466
|
+
return v.split(",").map(function(part){
|
|
467
|
+
var trimmed = part.trim();
|
|
468
|
+
if (!trimmed) return part;
|
|
469
|
+
var m = trimmed.match(/^(\\S+)(\\s.*)?$/);
|
|
470
|
+
if (!m) return part;
|
|
471
|
+
var url = m[1], rest = m[2] || "";
|
|
472
|
+
var leading = part.slice(0, part.length - trimmed.length);
|
|
473
|
+
if (isAbsPath(url) && !hasHttpPrefix(url)) return leading + HTTP_PREFIX + url + rest;
|
|
474
|
+
return part;
|
|
475
|
+
}).join(",");
|
|
476
|
+
}
|
|
477
|
+
function startObserver(){
|
|
478
|
+
if (!document.body) {
|
|
479
|
+
var pre = new MutationObserver(function(){ if (document.body) { pre.disconnect(); startObserver(); } });
|
|
480
|
+
pre.observe(document.documentElement, { childList: true });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
new MutationObserver(function(records){
|
|
484
|
+
for (var i = 0; i < records.length; i++) {
|
|
485
|
+
var r = records[i];
|
|
486
|
+
if (r.type === "childList") {
|
|
487
|
+
for (var k = 0; k < r.addedNodes.length; k++) rewriteAttrs(r.addedNodes[k]);
|
|
488
|
+
} else if (r.type === "attributes") {
|
|
489
|
+
rewriteAttrs(r.target);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}).observe(document.documentElement, {
|
|
493
|
+
childList: true, subtree: true, attributes: true,
|
|
494
|
+
attributeFilter: ["href","src","srcset","imagesrcset","action","formaction","poster","data","cite","manifest","longdesc","background"],
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (document.readyState === "loading") {
|
|
498
|
+
document.addEventListener("DOMContentLoaded", startObserver, { once: true });
|
|
499
|
+
} else {
|
|
500
|
+
startObserver();
|
|
501
|
+
}
|
|
502
|
+
})();
|
|
503
|
+
`.trim();
|
|
504
|
+
|
|
505
|
+
export function buildShimScript(httpPrefix, wsPrefix) {
|
|
506
|
+
const safeHttp = trimTrailingSlash(httpPrefix);
|
|
507
|
+
const safeWs = trimTrailingSlash(wsPrefix || "");
|
|
508
|
+
// 占位符替换 —— 直接字符串替换,不参与 JS 语义,前缀里只允许 URL-safe 字符
|
|
509
|
+
// (本仓库 slug 形如 `p<port>-<hex>`,符合 `[A-Za-z0-9_-]`)。
|
|
510
|
+
return TUNNEL_SHIM_TEMPLATE
|
|
511
|
+
.replace("__HTTP_PREFIX__", safeHttp)
|
|
512
|
+
.replace("__WS_PREFIX__", safeWs || safeHttp);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// injectHtmlShim 把 <script>shim</script> 塞到 HTML 最前面。优先紧跟 `<head>`
|
|
516
|
+
// 开标签插入(保证在被代理 app 任何 JS 之前执行);若没有 `<head>`,退而求其次
|
|
517
|
+
// 紧跟 `<html>`;两者都没有则直接 prepend。
|
|
518
|
+
export function injectHtmlShim(html, httpPrefix, wsPrefix) {
|
|
519
|
+
const script = buildShimScript(httpPrefix, wsPrefix);
|
|
520
|
+
const tag = `<script>${script}</script>`;
|
|
521
|
+
const headMatch = html.match(/<head\b[^>]*>/i);
|
|
522
|
+
if (headMatch) {
|
|
523
|
+
const idx = headMatch.index + headMatch[0].length;
|
|
524
|
+
return html.slice(0, idx) + tag + html.slice(idx);
|
|
525
|
+
}
|
|
526
|
+
const htmlMatch = html.match(/<html\b[^>]*>/i);
|
|
527
|
+
if (htmlMatch) {
|
|
528
|
+
const idx = htmlMatch.index + htmlMatch[0].length;
|
|
529
|
+
return html.slice(0, idx) + tag + html.slice(idx);
|
|
530
|
+
}
|
|
531
|
+
return tag + html;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export async function proxyTunnelRequest({ request, targetBaseUrl, publicPathPrefix = "", publicWsPathPrefix = "" }) {
|
|
535
|
+
const pathName = typeof request.path === "string" && request.path.trim() ? request.path : "/";
|
|
536
|
+
const url = new URL(trimSlash(targetBaseUrl) + pathName);
|
|
537
|
+
if (typeof request.rawQuery === "string" && request.rawQuery) {
|
|
538
|
+
url.search = request.rawQuery;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const headers = new Headers();
|
|
542
|
+
for (const [name, values] of Object.entries(request.headers ?? {})) {
|
|
543
|
+
if (isHopByHopHeader(name) || name.toLowerCase() === "host") continue;
|
|
544
|
+
// 跳过浏览器的 Accept-Encoding —— 它会让 origin 返回 gzip 压缩 body,
|
|
545
|
+
// 但我们要做 HTML 路径改写必须拿到 plain text。如果改写 gzip 字节,
|
|
546
|
+
// 浏览器 gzip 解码会得到 ERR_CONTENT_DECODING_FAILED。强制 identity
|
|
547
|
+
// 让 origin 返回未压缩 body。
|
|
548
|
+
if (publicPathPrefix && name.toLowerCase() === "accept-encoding") continue;
|
|
549
|
+
for (const value of Array.isArray(values) ? values : [values]) {
|
|
550
|
+
headers.append(name, String(value));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (publicPathPrefix) {
|
|
554
|
+
headers.set("accept-encoding", "identity");
|
|
555
|
+
}
|
|
556
|
+
if (request.originalHost) {
|
|
557
|
+
headers.set("x-forwarded-host", String(request.originalHost));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
let body;
|
|
561
|
+
if (typeof request.bodyBase64 === "string" && request.bodyBase64) {
|
|
562
|
+
body = Buffer.from(request.bodyBase64, "base64");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let response;
|
|
566
|
+
try {
|
|
567
|
+
const controller = new AbortController();
|
|
568
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_REQUEST_TIMEOUT_MS);
|
|
569
|
+
try {
|
|
570
|
+
response = await fetch(url, {
|
|
571
|
+
method: request.method || "GET",
|
|
572
|
+
headers, body,
|
|
573
|
+
signal: controller.signal,
|
|
574
|
+
redirect: "manual",
|
|
575
|
+
});
|
|
576
|
+
} finally {
|
|
577
|
+
clearTimeout(timeout);
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return {
|
|
581
|
+
statusCode: 502,
|
|
582
|
+
headers: { "content-type": ["text/plain; charset=utf-8"] },
|
|
583
|
+
bodyBase64: Buffer.from(String(error instanceof Error ? error.message : error), "utf8").toString("base64"),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let payload = Buffer.from(await response.arrayBuffer());
|
|
588
|
+
const contentType = response.headers.get("content-type") || "";
|
|
589
|
+
|
|
590
|
+
// text/html: 改写 absolute-path 属性 + 注入运行时 shim。两步合在一起做,
|
|
591
|
+
// 避免对同一份 body 反复 toString。
|
|
592
|
+
if (publicPathPrefix && /^text\/html\b/i.test(contentType)) {
|
|
593
|
+
try {
|
|
594
|
+
const html = payload.toString("utf8");
|
|
595
|
+
const rewritten = rewriteHtmlAbsolutePaths(html, publicPathPrefix);
|
|
596
|
+
const injected = injectHtmlShim(rewritten, publicPathPrefix, publicWsPathPrefix);
|
|
597
|
+
if (injected !== html) {
|
|
598
|
+
payload = Buffer.from(injected, "utf8");
|
|
599
|
+
}
|
|
600
|
+
} catch { /* 改写失败回退到原 payload */ }
|
|
601
|
+
}
|
|
602
|
+
// text/css: 外链 CSS 文件里写的 url()/@import 走单独一遍扫描;HTML rewriter
|
|
603
|
+
// 不看这些响应。
|
|
604
|
+
if (publicPathPrefix && /^text\/css\b/i.test(contentType)) {
|
|
605
|
+
try {
|
|
606
|
+
const css = payload.toString("utf8");
|
|
607
|
+
const rewritten = rewriteCssAbsolutePaths(css, publicPathPrefix);
|
|
608
|
+
if (rewritten !== css) {
|
|
609
|
+
payload = Buffer.from(rewritten, "utf8");
|
|
610
|
+
}
|
|
611
|
+
} catch { /* same */ }
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const outHeaders = responseHeadersToObject(response.headers);
|
|
615
|
+
|
|
616
|
+
if (publicPathPrefix) {
|
|
617
|
+
// 1) Link 头 preload hints (Next.js / Nuxt / etc.) —— 已有逻辑。
|
|
618
|
+
if (Array.isArray(outHeaders["link"])) {
|
|
619
|
+
outHeaders["link"] = outHeaders["link"].map((v) =>
|
|
620
|
+
rewriteLinkHeaderValue(v, publicPathPrefix),
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
// 2) 3xx Location 头 —— `/login` → `/t/<slug>/login`,绝对 URL 不动。
|
|
624
|
+
if (response.status >= 300 && response.status < 400 && Array.isArray(outHeaders["location"])) {
|
|
625
|
+
outHeaders["location"] = outHeaders["location"].map((v) =>
|
|
626
|
+
rewriteLocationHeader(v, publicPathPrefix),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
// 3) Content-Encoding —— Node fetch 已自动解压,客户端看到这个 header 会
|
|
630
|
+
// 去 decode 已解压的 body 必然失败,删掉。
|
|
631
|
+
delete outHeaders["content-encoding"];
|
|
632
|
+
// 4) CSP —— inline shim 在 strict CSP (`script-src 'self'`) 下会被拒。
|
|
633
|
+
// tunnel 自身是受信代理,直接把 CSP 头干掉,让 shim 能跑。
|
|
634
|
+
// Report-Only 同样删除,避免产生误导性违规日志。
|
|
635
|
+
delete outHeaders["content-security-policy"];
|
|
636
|
+
delete outHeaders["content-security-policy-report-only"];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
statusCode: response.status,
|
|
641
|
+
headers: outHeaders,
|
|
642
|
+
bodyBase64: payload.toString("base64"),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// 改写 Link header 里的 absolute path。Link header 形如:
|
|
647
|
+
// </foo.css>; rel=preload; as=style, </bar.woff2>; rel=preload; as=font
|
|
648
|
+
// 里面的 `<URL>` 部分若以 `/` 开头(非 protocol-relative `//`)则加前缀。
|
|
649
|
+
export function rewriteLinkHeaderValue(value, prefix) {
|
|
650
|
+
const prefixSafe = String(prefix || "").replace(/\/+$/, "");
|
|
651
|
+
if (!prefixSafe) return value;
|
|
652
|
+
return String(value).replace(/<\s*(\/(?!\/)[^>]*)>/g, (_match, urlPath) => `<${prefixSafe}${urlPath}>`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function responseHeadersToObject(headers) {
|
|
656
|
+
const out = {};
|
|
657
|
+
for (const [name, value] of headers.entries()) {
|
|
658
|
+
if (isHopByHopHeader(name) || name.toLowerCase() === "content-length") continue;
|
|
659
|
+
if (!out[name]) out[name] = [];
|
|
660
|
+
out[name].push(value);
|
|
661
|
+
}
|
|
662
|
+
if (typeof headers.getSetCookie === "function") {
|
|
663
|
+
const cookies = headers.getSetCookie();
|
|
664
|
+
if (cookies.length > 0) out["set-cookie"] = cookies;
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ---- WebSocket / TCP session pool ----
|
|
670
|
+
|
|
671
|
+
const tunnelStreamSessions = new Map();
|
|
672
|
+
|
|
673
|
+
export async function handleTunnelStreamEvent({
|
|
674
|
+
event, protocol, targetBaseUrl, relayUrl, tunnelID, tunnelToken, logger,
|
|
675
|
+
}) {
|
|
676
|
+
const sessionID = String(event?.sessionId ?? "").trim();
|
|
677
|
+
if (!sessionID) throw new Error("missing session id");
|
|
678
|
+
switch (event.kind) {
|
|
679
|
+
case "open":
|
|
680
|
+
if (protocol === TUNNEL_PROTOCOL_WS) {
|
|
681
|
+
await openLocalWebSocketSession({ event, targetBaseUrl, relayUrl, tunnelID, tunnelToken, logger });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (protocol === TUNNEL_PROTOCOL_TCP) {
|
|
685
|
+
await openLocalTCPSession({ event, targetBaseUrl, relayUrl, tunnelID, tunnelToken, logger });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
throw new Error(`unsupported stream protocol: ${protocol}`);
|
|
689
|
+
case "client_data":
|
|
690
|
+
await writeTunnelStreamSession({ sessionID, event });
|
|
691
|
+
return;
|
|
692
|
+
case "client_close":
|
|
693
|
+
await closeTunnelStreamSession(sessionID);
|
|
694
|
+
return;
|
|
695
|
+
default:
|
|
696
|
+
logger.info("stream event ignored", { sessionId: sessionID, kind: event.kind ?? "-" });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function openLocalWebSocketSession({ event, targetBaseUrl, relayUrl, tunnelID, tunnelToken, logger }) {
|
|
701
|
+
const sessionID = event.sessionId;
|
|
702
|
+
const targetUrl = joinTunnelTargetUrl(targetBaseUrl, event.path, event.rawQuery);
|
|
703
|
+
const ws = new WebSocket(targetUrl);
|
|
704
|
+
ws.binaryType = "arraybuffer";
|
|
705
|
+
await new Promise((resolve, reject) => {
|
|
706
|
+
const timer = setTimeout(() => reject(new Error("websocket open timeout")), DEFAULT_REQUEST_TIMEOUT_MS);
|
|
707
|
+
ws.addEventListener("open", () => { clearTimeout(timer); resolve(true); }, { once: true });
|
|
708
|
+
ws.addEventListener("error", (error) => {
|
|
709
|
+
clearTimeout(timer);
|
|
710
|
+
reject(error?.error ?? new Error("websocket open failed"));
|
|
711
|
+
}, { once: true });
|
|
712
|
+
});
|
|
713
|
+
tunnelStreamSessions.set(sessionID, {
|
|
714
|
+
protocol: TUNNEL_PROTOCOL_WS,
|
|
715
|
+
send(payload, eventType) {
|
|
716
|
+
ws.send(eventType === "binary" ? Buffer.from(payload) : payload.toString("utf8"));
|
|
717
|
+
},
|
|
718
|
+
close() {
|
|
719
|
+
try { ws.close(); } catch { /* noop */ }
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
logger.info("stream open", { sessionId: sessionID, protocol: TUNNEL_PROTOCOL_WS, target: targetUrl });
|
|
723
|
+
|
|
724
|
+
ws.addEventListener("message", async (message) => {
|
|
725
|
+
const payload = await normalizeWebSocketPayload(message.data);
|
|
726
|
+
await publishTunnelSessionEvent({
|
|
727
|
+
relayUrl, tunnelID, tunnelToken, sessionID,
|
|
728
|
+
event: {
|
|
729
|
+
sessionId: sessionID, protocol: TUNNEL_PROTOCOL_WS,
|
|
730
|
+
kind: "server_data",
|
|
731
|
+
messageType: typeof message.data === "string" ? "text" : "binary",
|
|
732
|
+
payloadBase64: payload.toString("base64"),
|
|
733
|
+
createdAt: new Date().toISOString(),
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
ws.addEventListener("close", async () => {
|
|
738
|
+
tunnelStreamSessions.delete(sessionID);
|
|
739
|
+
await publishTunnelSessionEvent({
|
|
740
|
+
relayUrl, tunnelID, tunnelToken, sessionID,
|
|
741
|
+
event: { sessionId: sessionID, protocol: TUNNEL_PROTOCOL_WS, kind: "server_close", createdAt: new Date().toISOString() },
|
|
742
|
+
}).catch(() => {});
|
|
743
|
+
});
|
|
744
|
+
ws.addEventListener("error", async (error) => {
|
|
745
|
+
await publishTunnelSessionEvent({
|
|
746
|
+
relayUrl, tunnelID, tunnelToken, sessionID,
|
|
747
|
+
event: {
|
|
748
|
+
sessionId: sessionID, protocol: TUNNEL_PROTOCOL_WS,
|
|
749
|
+
kind: "server_error",
|
|
750
|
+
error: String(error?.message ?? error?.error?.message ?? "websocket error"),
|
|
751
|
+
createdAt: new Date().toISOString(),
|
|
752
|
+
},
|
|
753
|
+
}).catch(() => {});
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function openLocalTCPSession({ event, targetBaseUrl, relayUrl, tunnelID, tunnelToken, logger }) {
|
|
758
|
+
const sessionID = event.sessionId;
|
|
759
|
+
const { host, port } = parseTunnelTCPAddress(targetBaseUrl);
|
|
760
|
+
const socket = net.createConnection({ host, port });
|
|
761
|
+
await new Promise((resolve, reject) => {
|
|
762
|
+
const timer = setTimeout(() => reject(new Error("tcp open timeout")), DEFAULT_REQUEST_TIMEOUT_MS);
|
|
763
|
+
socket.once("connect", () => { clearTimeout(timer); resolve(); });
|
|
764
|
+
socket.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
765
|
+
});
|
|
766
|
+
tunnelStreamSessions.set(sessionID, {
|
|
767
|
+
protocol: TUNNEL_PROTOCOL_TCP,
|
|
768
|
+
send(payload) { socket.write(payload); },
|
|
769
|
+
close() { socket.destroy(); },
|
|
770
|
+
});
|
|
771
|
+
logger.info("stream open", { sessionId: sessionID, protocol: TUNNEL_PROTOCOL_TCP, target: `${host}:${port}` });
|
|
772
|
+
|
|
773
|
+
socket.on("data", async (chunk) => {
|
|
774
|
+
await publishTunnelSessionEvent({
|
|
775
|
+
relayUrl, tunnelID, tunnelToken, sessionID,
|
|
776
|
+
event: {
|
|
777
|
+
sessionId: sessionID, protocol: TUNNEL_PROTOCOL_TCP,
|
|
778
|
+
kind: "server_data",
|
|
779
|
+
payloadBase64: Buffer.from(chunk).toString("base64"),
|
|
780
|
+
createdAt: new Date().toISOString(),
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
socket.on("close", async () => {
|
|
785
|
+
tunnelStreamSessions.delete(sessionID);
|
|
786
|
+
await publishTunnelSessionEvent({
|
|
787
|
+
relayUrl, tunnelID, tunnelToken, sessionID,
|
|
788
|
+
event: { sessionId: sessionID, protocol: TUNNEL_PROTOCOL_TCP, kind: "server_close", createdAt: new Date().toISOString() },
|
|
789
|
+
}).catch(() => {});
|
|
790
|
+
});
|
|
791
|
+
socket.on("error", async (error) => {
|
|
792
|
+
await publishTunnelSessionEvent({
|
|
793
|
+
relayUrl, tunnelID, tunnelToken, sessionID,
|
|
794
|
+
event: {
|
|
795
|
+
sessionId: sessionID, protocol: TUNNEL_PROTOCOL_TCP,
|
|
796
|
+
kind: "server_error",
|
|
797
|
+
error: String(error instanceof Error ? error.message : error),
|
|
798
|
+
createdAt: new Date().toISOString(),
|
|
799
|
+
},
|
|
800
|
+
}).catch(() => {});
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function writeTunnelStreamSession({ sessionID, event }) {
|
|
805
|
+
const session = tunnelStreamSessions.get(sessionID);
|
|
806
|
+
if (!session) return;
|
|
807
|
+
const payload = Buffer.from(String(event.payloadBase64 ?? ""), "base64");
|
|
808
|
+
session.send(payload, event.messageType);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function closeTunnelStreamSession(sessionID) {
|
|
812
|
+
const session = tunnelStreamSessions.get(sessionID);
|
|
813
|
+
if (!session) return;
|
|
814
|
+
tunnelStreamSessions.delete(sessionID);
|
|
815
|
+
session.close();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ---- agent 主循环 (HTTP 模式 long-poll, WS/TCP 模式 stream-poll) ----
|
|
819
|
+
|
|
820
|
+
export async function runHttpAgentLoop({ stopState, relayUrl, tunnelID, tunnelToken, targetBaseUrl, logger, publicPathPrefix = "", publicWsPathPrefix = "" }) {
|
|
821
|
+
let idlePolls = 0;
|
|
822
|
+
while (!stopState.stopping) {
|
|
823
|
+
let request;
|
|
824
|
+
try {
|
|
825
|
+
request = await readNextTunnelRequest({ relayUrl, tunnelID, tunnelToken });
|
|
826
|
+
} catch (error) {
|
|
827
|
+
logger.error("poll failed", { error: String(error instanceof Error ? error.message : error) });
|
|
828
|
+
await sleep(1000);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (!request) {
|
|
832
|
+
idlePolls += 1;
|
|
833
|
+
if (idlePolls === 1 || idlePolls % TUNNEL_IDLE_LOG_EVERY === 0) {
|
|
834
|
+
logger.info("poll idle", { count: idlePolls });
|
|
835
|
+
}
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
idlePolls = 0;
|
|
839
|
+
const requestSummary = summarizeTunnelRequest(request);
|
|
840
|
+
logger.info("request start", requestSummary);
|
|
841
|
+
const startedAt = Date.now();
|
|
842
|
+
let response;
|
|
843
|
+
try {
|
|
844
|
+
response = await proxyTunnelRequest({ request, targetBaseUrl, publicPathPrefix, publicWsPathPrefix });
|
|
845
|
+
} catch (error) {
|
|
846
|
+
logger.error("proxy failed", {
|
|
847
|
+
...requestSummary,
|
|
848
|
+
error: String(error instanceof Error ? error.message : error),
|
|
849
|
+
});
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
logger.info("request proxied", {
|
|
853
|
+
...requestSummary,
|
|
854
|
+
status: response.statusCode,
|
|
855
|
+
bytes: decodeBase64Length(response.bodyBase64),
|
|
856
|
+
durationMs: Date.now() - startedAt,
|
|
857
|
+
});
|
|
858
|
+
try {
|
|
859
|
+
await publishTunnelResponse({ relayUrl, tunnelID, tunnelToken, requestID: request.requestId, response });
|
|
860
|
+
logger.info("response published", {
|
|
861
|
+
requestId: request.requestId,
|
|
862
|
+
status: response.statusCode,
|
|
863
|
+
durationMs: Date.now() - startedAt,
|
|
864
|
+
});
|
|
865
|
+
} catch (error) {
|
|
866
|
+
logger.error("publish failed", {
|
|
867
|
+
requestId: request.requestId,
|
|
868
|
+
error: String(error instanceof Error ? error.message : error),
|
|
869
|
+
});
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export async function runStreamAgentLoop({
|
|
876
|
+
stopState, protocol, relayUrl, tunnelID, tunnelToken, targetBaseUrl, logger,
|
|
877
|
+
}) {
|
|
878
|
+
let idlePolls = 0;
|
|
879
|
+
while (!stopState.stopping) {
|
|
880
|
+
let event;
|
|
881
|
+
try {
|
|
882
|
+
event = await readNextTunnelStreamEvent({ relayUrl, tunnelID, tunnelToken });
|
|
883
|
+
} catch (error) {
|
|
884
|
+
logger.error("poll failed", { error: String(error instanceof Error ? error.message : error) });
|
|
885
|
+
await sleep(1000);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (!event) {
|
|
889
|
+
idlePolls += 1;
|
|
890
|
+
if (idlePolls === 1 || idlePolls % TUNNEL_STREAM_IDLE_LOG_EVERY === 0) {
|
|
891
|
+
logger.info("stream idle", { count: idlePolls });
|
|
892
|
+
}
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
idlePolls = 0;
|
|
896
|
+
try {
|
|
897
|
+
await handleTunnelStreamEvent({
|
|
898
|
+
event, protocol, targetBaseUrl,
|
|
899
|
+
relayUrl, tunnelID, tunnelToken, logger,
|
|
900
|
+
});
|
|
901
|
+
} catch (error) {
|
|
902
|
+
logger.error("stream event failed", {
|
|
903
|
+
sessionId: event?.sessionId ?? "-",
|
|
904
|
+
kind: event?.kind ?? "-",
|
|
905
|
+
error: String(error instanceof Error ? error.message : error),
|
|
906
|
+
});
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 协议常量在调用方需要,re-export 便于 tunnel.mjs 引用。
|
|
913
|
+
export { TUNNEL_PROTOCOL_HTTP, TUNNEL_PROTOCOL_WS, TUNNEL_PROTOCOL_TCP };
|