squidclaw 3.0.19 → 3.0.21
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/dist/{accounts-s5e9zidf.js → accounts-C7pGQPcS.js} +2 -2
- package/dist/{accounts-Bkb-J8V6.js → accounts-DjhBQg_8.js} +2 -2
- package/dist/{accounts-B6F_XCgS.js → accounts-DwGoZKGm.js} +17 -17
- package/dist/{active-listener-6-Svu8Dx.js → active-listener-DPc_PGsA.js} +2 -2
- package/dist/{agents-DL8uYsUq.js → agents-DXKtU4Il.js} +4 -4
- package/dist/{agents.config-B4KAAVvR.js → agents.config-BM-2SLCh.js} +1 -1
- package/dist/{agents.config-D93fF1eJ.js → agents.config-eMSUW-iw.js} +1 -1
- package/dist/{plugin-sdk/api-key-rotation-DyjMI2n3.js → api-key-rotation-DVyYtQxC.js} +2 -2
- package/dist/{audio-preflight-DZ1LNMJp.js → audio-preflight-BUCBED7N.js} +32 -32
- package/dist/{audio-preflight-Bzo_zN4j.js → audio-preflight-lT9iRnYi.js} +4 -4
- package/dist/{audio-transcription-runner-B3u2x_ja.js → audio-transcription-runner-Cv0Q47cM.js} +12 -12
- package/dist/{audio-transcription-runner-Cp_lkLCv.js → audio-transcription-runner-YiR1ym3a.js} +1 -1
- package/dist/{audit-membership-runtime-w23FnNAN.js → audit-membership-runtime-DyLj-uhz.js} +4 -4
- package/dist/{auth-choice-Ba4AEm_k.js → auth-choice-CXepQc7c.js} +1 -1
- package/dist/{auth-choice-ziFj5bXd.js → auth-choice-D2JrMJn-.js} +1 -1
- package/dist/{banner-DDih2slD.js → banner-DMfuBV69.js} +1 -1
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +51 -51
- package/dist/bundled/bootstrap-extra-files/handler.js +6 -6
- package/dist/bundled/command-logger/handler.js +2 -2
- package/dist/bundled/session-memory/handler.js +687 -51
- package/dist/canvas-host/a2ui/a2ui.bundle.js +1 -17815
- package/dist/{channel-activity-CxJbx4sD.js → channel-activity-CPpt4XaL.js} +3 -3
- package/dist/{channel-options-DTDfU0R6.js → channel-options-CQ-gER0J.js} +1 -1
- package/dist/{channel-options-BBSsevyl.js → channel-options-SijIkAlT.js} +1 -1
- package/dist/{channel-web-BXl9jCKV.js → channel-web-7t1a2N13.js} +1 -1
- package/dist/{channel-web-SF18oYqo.js → channel-web-LLzBxZrT.js} +1 -1
- package/dist/{channels-cli-BiuWJpJu.js → channels-cli-D4xUEYYC.js} +6 -6
- package/dist/{channels-cli-BDLabpWH.js → channels-cli-DYaFNMme.js} +6 -6
- package/dist/{chrome-CZQnp4VH.js → chrome-CuBHInKC.js} +8 -8
- package/dist/{chrome-Dxm25ptH.js → chrome-OgCChbC_.js} +26 -26
- package/dist/{cli-CfgDCCuC.js → cli-CAID9zkL.js} +1 -1
- package/dist/{cli-DAZ1MKrJ.js → cli-Dlc9IFWZ.js} +1 -1
- package/dist/{command-registry-DkjNuW8w.js → command-registry-AHS8_9da.js} +9 -9
- package/dist/{commands-registry-B44COZFz.js → commands-registry-BJjv8_oR.js} +4 -4
- package/dist/{completion-cli-DbNma3J9.js → completion-cli-B8xjpHDs.js} +1 -1
- package/dist/{completion-cli-Du8H92bM.js → completion-cli-BR1r9_h4.js} +2 -2
- package/dist/{config-cli-CQhQ8Bcc.js → config-cli-D0A8Awoo.js} +1 -1
- package/dist/{config-cli-D16tlTWa.js → config-cli-D4Rxoozg.js} +1 -1
- package/dist/{configure-B9b3cQTC.js → configure-DOiI8_8c.js} +3 -3
- package/dist/{configure-B4TiK50K.js → configure-XmOqwwy9.js} +3 -3
- package/dist/control-ui/assets/{index-cM5P_3w7.js → index-BqxuPQOl.js} +2 -2
- package/dist/control-ui/assets/{index-cM5P_3w7.js.map → index-BqxuPQOl.js.map} +1 -1
- package/dist/control-ui/index.html +1 -1
- package/dist/{deliver-B4WWPQvt.js → deliver-6zfT7daI.js} +1 -1
- package/dist/{deliver-BcnsjbAB.js → deliver-DMTUTpTM.js} +21 -21
- package/dist/deliver-runtime-B80olQwJ.js +36 -0
- package/dist/{deliver-runtime-CeE1X9si.js → deliver-runtime-MBexxUuG.js} +3 -3
- package/dist/deps-send-discord.runtime-GIuvX7Xw.js +26 -0
- package/dist/deps-send-imessage.runtime-B3TC7G80.js +25 -0
- package/dist/deps-send-signal.runtime-CuVJyw7n.js +24 -0
- package/dist/deps-send-slack.runtime-cbfFoLZ4.js +22 -0
- package/dist/deps-send-telegram.runtime-CRyP-xDQ.js +27 -0
- package/dist/deps-send-whatsapp.runtime-4aOUBP2J.js +60 -0
- package/dist/{deps-send-whatsapp.runtime-B9uivfkM.js → deps-send-whatsapp.runtime-CETLGt-t.js} +3 -3
- package/dist/{deps-send-whatsapp.runtime-miTfQkK9.js → deps-send-whatsapp.runtime-CrxkfP31.js} +7 -7
- package/dist/{deps-send-whatsapp.runtime-CFnZsKW0.js → deps-send-whatsapp.runtime-__ogpmJj.js} +3 -3
- package/dist/{diagnostic-C2lklhkn.js → diagnostic-CnVwZNbm.js} +2 -2
- package/dist/{doctor-completion-ujZ_JdIz.js → doctor-completion-CZoEdMS-.js} +1 -1
- package/dist/{doctor-completion-CEiVFPte.js → doctor-completion-DhLEFUhN.js} +1 -1
- package/dist/entry.js +2 -2
- package/dist/{plugin-sdk/errors-B8oJXuCF.js → errors-kkRuS2Cs.js} +1 -1
- package/dist/extensionAPI.js +6 -6
- package/dist/{fetch-b8tSR7_e.js → fetch-DP-JjB6Z.js} +5 -5
- package/dist/{fetch-guard-D16tjNsZ.js → fetch-guard-BDy975wP.js} +2 -2
- package/dist/{frontmatter-CjKtFduT.js → frontmatter-Cq1TcIQ2.js} +3 -3
- package/dist/{fs-safe-CwHbZdFH.js → fs-safe-BoB4X3GD.js} +4 -4
- package/dist/{gateway-cli-NXeo-J8r.js → gateway-cli-BJHBChfI.js} +8 -8
- package/dist/{gateway-cli-2jaWM_Ci.js → gateway-cli-BpHskeDd.js} +8 -8
- package/dist/{github-copilot-token-Cw3tAXM9.js → github-copilot-token-B5cPlwaz.js} +7 -7
- package/dist/{health-BPZALM__.js → health-TmpUGSqu.js} +1 -1
- package/dist/{health-DFZKMrW2.js → health-XFKFZ7ZJ.js} +1 -1
- package/dist/{hooks-cli-B3rkhWcT.js → hooks-cli-CXsZK8H9.js} +2 -2
- package/dist/{hooks-cli-BO_gZJVD.js → hooks-cli-DsDV-Pxz.js} +2 -2
- package/dist/{image-DKkuLtZ4.js → image-VgwN31FZ.js} +1 -1
- package/dist/{image-C-8Kd2Mh.js → image-kKMG59st.js} +6 -6
- package/dist/{image-ops-BoN1E_WZ.js → image-ops-Dg8iraUV.js} +2 -2
- package/dist/image-runtime-BqIv7p_K.js +29 -0
- package/dist/{image-runtime-BMavqm9n.js → image-runtime-CwMuTYvd.js} +3 -3
- package/dist/index.js +6 -6
- package/dist/{ir-Dut0zXyS.js → ir-CKK03mBV.js} +8 -8
- package/dist/{legacy-names-B0wgIP0Q.js → legacy-names-aGJJuzM_.js} +1 -1
- package/dist/llm-slug-generator.js +51 -51
- package/dist/{logger-oGKcCLZ5.js → logger-CnTSBL7T.js} +7 -7
- package/dist/{login-DJ357UQV.js → login-CeKDrz6_.js} +5 -5
- package/dist/{login-qr-C0fDBnpM.js → login-qr-KbOpR0GQ.js} +10 -10
- package/dist/{manager-BPGhs30n.js → manager-DINhLnMi.js} +13 -13
- package/dist/manager-runtime-D6ckUNSs.js +18 -0
- package/dist/{model-selection-CHnojCCK.js → model-selection-DuNLFQPR.js} +43 -43
- package/dist/{models-Bb-GYqHr.js → models-5VXwJBU2.js} +2 -2
- package/dist/{models-cli-DffAz0ji.js → models-cli-Br56IHfy.js} +3 -3
- package/dist/{models-cli-CAIJM1Wh.js → models-cli-CrR1RN3j.js} +2 -2
- package/dist/{npm-resolution-D2_zGJM-.js → npm-resolution-CKtyq4FH.js} +1 -1
- package/dist/{npm-resolution-D5pBCkUw.js → npm-resolution-CPk7dS7F.js} +1 -1
- package/dist/{onboard-DcVsL9Jx.js → onboard-DUnBamC0.js} +2 -2
- package/dist/{onboard-channels-mNXKTvFs.js → onboard-channels-D45grihx.js} +1 -1
- package/dist/{onboard-channels-C-cQFUBH.js → onboard-channels-_kVo3Apf.js} +1 -1
- package/dist/{onboard-dhG2YBez.js → onboard-lFwpCpC3.js} +2 -2
- package/dist/{onboarding-B3-IwkNi.js → onboarding-CyCSQ__q.js} +3 -3
- package/dist/{onboarding-Cl6kW_iq.js → onboarding-EEd_g8Zg.js} +3 -3
- package/dist/{onboarding.finalize-CCU-ykTR.js → onboarding.finalize-CbMq7C4K.js} +5 -5
- package/dist/{onboarding.finalize-BZqsNG2H.js → onboarding.finalize-Cs1ukjFN.js} +6 -6
- package/dist/{outbound-qTioiTJg.js → outbound-C9svt6RH.js} +6 -6
- package/dist/{outbound-attachment-rlW7G5df.js → outbound-attachment-DwupUxYF.js} +2 -2
- package/dist/{path-alias-guards-Cpsiv2KL.js → path-alias-guards-DORgbZ1w.js} +1 -1
- package/dist/{paths-CSdAWKDO.js → paths-DA5srn0U.js} +5 -5
- package/dist/{paths-CXClY8zC.js → paths-DSd911Oe.js} +4 -4
- package/dist/{pi-embedded-B79nZERv.js → pi-embedded-BN8fghaF.js} +123 -26
- package/dist/{pi-embedded-C5U8Qxn6.js → pi-embedded-BiC4tIJ8.js} +266 -169
- package/dist/{pi-embedded-helpers-CEHpGDRs.js → pi-embedded-helpers-A9VYPVCH.js} +3 -3
- package/dist/{pi-embedded-helpers-DQ7IaeOi.js → pi-embedded-helpers-Di58J7Eh.js} +52 -52
- package/dist/{pi-model-discovery-o-WX5w2t.js → pi-model-discovery-V-InbjOM.js} +7 -7
- package/dist/pi-model-discovery-runtime--t6tAlar.js +11 -0
- package/dist/{pi-tools.before-tool-call.runtime-B_LUttg1.js → pi-tools.before-tool-call.runtime-Bpk4qTgV.js} +9 -9
- package/dist/{plugin-registry-CB47SSz1.js → plugin-registry-CXm125Uz.js} +1 -1
- package/dist/{plugin-registry-Gy_VByyf.js → plugin-registry-D3PnPE1D.js} +1 -1
- package/dist/plugin-sdk/{accounts-0kF5cxkn.js → accounts-C5PAuCTj.js} +2 -2
- package/dist/plugin-sdk/{accounts-u0-JAHzj.js → accounts-DC5cbH9r.js} +2 -2
- package/dist/plugin-sdk/{accounts-xZOA23tQ.js → accounts-DncG0Hx9.js} +3 -3
- package/dist/plugin-sdk/{active-listener-BO7eBEG_.js → active-listener-Bd3HH2km.js} +2 -2
- package/dist/plugin-sdk/agents/agent-tier.d.ts +22 -2
- package/dist/plugin-sdk/{api-key-rotation-C4C_mDsg.js → api-key-rotation-BAZ0GD26.js} +2 -2
- package/dist/plugin-sdk/{audio-preflight-C0q7lu6y.js → audio-preflight-tpVm-t9O.js} +26 -26
- package/dist/plugin-sdk/{audio-transcription-runner-Ced47O1H.js → audio-transcription-runner-DSScb434.js} +11 -11
- package/dist/plugin-sdk/{audit-membership-runtime-Xl20kCBe.js → audit-membership-runtime-DHQDvH4u.js} +2 -2
- package/dist/plugin-sdk/{channel-activity-CNffKOEQ.js → channel-activity-cYEaofTH.js} +3 -3
- package/dist/plugin-sdk/{channel-web-vB3Dcd8-.js → channel-web-CbeCrQ4C.js} +18 -18
- package/dist/plugin-sdk/{channel-web-BrhP6FQO.js → channel-web-CqiSEc52.js} +1 -1
- package/dist/plugin-sdk/{chrome-DJQWo149.js → chrome-DB2h0uN0.js} +6 -6
- package/dist/plugin-sdk/{commands-registry-_kBPE22q.js → commands-registry-DGZ1oFXJ.js} +4 -4
- package/dist/plugin-sdk/{common-J8vIST9Q.js → common-D5lLWoCW.js} +2 -2
- package/dist/plugin-sdk/compat.js +50 -50
- package/dist/plugin-sdk/{config-CnZ1TlEw.js → config-Bt-c7PWF.js} +7 -7
- package/dist/plugin-sdk/{deliver-Xh6voz9H.js → deliver-MvrkIeKJ.js} +10 -10
- package/dist/plugin-sdk/deliver-runtime-i50kjcNM.js +32 -0
- package/dist/plugin-sdk/deps-send-discord.runtime-hOYq9ov0.js +23 -0
- package/dist/plugin-sdk/deps-send-imessage.runtime-D5n9DXyL.js +22 -0
- package/dist/plugin-sdk/deps-send-signal.runtime-CwEaRyJU.js +21 -0
- package/dist/plugin-sdk/deps-send-slack.runtime-DOZeLIaC.js +19 -0
- package/dist/plugin-sdk/deps-send-telegram.runtime-BW1hSPKh.js +24 -0
- package/dist/plugin-sdk/{deps-send-whatsapp.runtime-Ce4Gzz-J.js → deps-send-whatsapp.runtime-DvxhnHR_.js} +3 -3
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-JKmTtCFM.js +57 -0
- package/dist/plugin-sdk/{diagnostic-DKsyROPi.js → diagnostic-B6F3BtCX.js} +2 -2
- package/dist/plugin-sdk/{errors-CgRPdp3o.js → errors-9oVz7reJ.js} +1 -1
- package/dist/plugin-sdk/{fetch-guard-BBAT8G-1.js → fetch-guard-cfPCfkrw.js} +2 -2
- package/dist/plugin-sdk/{fs-safe-DqCO1D4C.js → fs-safe-DFbwq9CS.js} +3 -3
- package/dist/plugin-sdk/{image-4ay2cw7G.js → image-FK5xhY5u.js} +6 -6
- package/dist/plugin-sdk/{image-ops-G8KoEfY8.js → image-ops-BSYgrL7E.js} +2 -2
- package/dist/plugin-sdk/image-runtime-B95EPFpg.js +25 -0
- package/dist/plugin-sdk/index.js +2 -2
- package/dist/plugin-sdk/{ir-DXj1KGnL.js → ir-CWmryq5f.js} +7 -7
- package/dist/plugin-sdk/{local-roots-ovKHgVSP.js → local-roots-U25IdeDH.js} +4 -4
- package/dist/plugin-sdk/{logger-DIb2cGHp.js → logger-Bg4vIUJn.js} +2 -2
- package/dist/plugin-sdk/{login-Cgtm70by.js → login-8qzl2H7G.js} +4 -4
- package/dist/plugin-sdk/{login-qr-BrixqhMx.js → login-qr-kalCTJEw.js} +5 -5
- package/dist/plugin-sdk/{manager-PEQ_cPYF.js → manager-ijYHktIt.js} +8 -8
- package/dist/plugin-sdk/manager-runtime--JrLQE03.js +15 -0
- package/dist/plugin-sdk/{outbound-BK75h9CQ.js → outbound-BIVf0aPq.js} +5 -5
- package/dist/plugin-sdk/{outbound-attachment-BI1QngTS.js → outbound-attachment-F5tzeNUD.js} +2 -2
- package/dist/plugin-sdk/{path-alias-guards-TnxupPQC.js → path-alias-guards-DA0MhfkG.js} +1 -1
- package/dist/plugin-sdk/{paths-B7_75Pdr.js → paths-CP67O8eN.js} +1 -1
- package/dist/plugin-sdk/{pi-embedded-helpers-BioULNev.js → pi-embedded-helpers-CxMnPQ37.js} +16 -16
- package/dist/plugin-sdk/{pi-model-discovery-BMmAbnil.js → pi-model-discovery-8OL77kCh.js} +1 -1
- package/dist/plugin-sdk/pi-model-discovery-runtime-DGRFpUfd.js +8 -0
- package/dist/plugin-sdk/{pi-tools.before-tool-call.runtime-rdRqZ9lk.js → pi-tools.before-tool-call.runtime-B7FW36KX.js} +4 -4
- package/dist/plugin-sdk/{plugins-BqKpJkqt.js → plugins-D3mrhffi.js} +4 -4
- package/dist/plugin-sdk/{proxy-env-C4s12rr8.js → proxy-env-CUUXO6rv.js} +1 -1
- package/dist/plugin-sdk/{proxy-fetch-ZPEvp58f.js → proxy-fetch-Cf3IUSDw.js} +1 -1
- package/dist/plugin-sdk/{pw-ai-Cb0ZDSq4.js → pw-ai-dPmKdw_w.js} +9 -9
- package/dist/plugin-sdk/{qmd-manager-Bei6TaFq.js → qmd-manager-Ov9ElEfG.js} +7 -7
- package/dist/plugin-sdk/{query-expansion-POz2za8a.js → query-expansion-CzjwW461.js} +4 -4
- package/dist/plugin-sdk/{redact-9WsNyb7S.js → redact-DfACyt0X.js} +1 -1
- package/dist/plugin-sdk/{reply-DxvpiQBp.js → reply-BczXGzC_.js} +137 -40
- package/dist/plugin-sdk/{reply-sFKB3ofA.js → reply-Dfjh1YtV.js} +206 -109
- package/dist/plugin-sdk/{resolve-outbound-target-DL1adXpk.js → resolve-outbound-target-BeIB_jm0.js} +2 -2
- package/dist/plugin-sdk/{run-with-concurrency-DmTrN5JG.js → run-with-concurrency-kVooFCVo.js} +1 -1
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-B6EcC22F.js +10 -0
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-DH8J0Ie7.js +19 -0
- package/dist/plugin-sdk/{send-BCVXt-3e.js → send-CgkNzAhS.js} +8 -8
- package/dist/plugin-sdk/{send-CAG0Or0G.js → send-CuENGOhq.js} +7 -7
- package/dist/plugin-sdk/{send-X6QuS7x0.js → send-D6Uh2D1D.js} +5 -5
- package/dist/plugin-sdk/{send-BNePC8CO.js → send-DV5kR0Hg.js} +6 -6
- package/dist/plugin-sdk/{send-DhGDAZnT.js → send-DtA6ngBJ.js} +13 -13
- package/dist/plugin-sdk/{session-CAqYQVhe.js → session-CnlZn-bR.js} +3 -3
- package/dist/plugin-sdk/{skill-commands-Bk-IFyNw.js → skill-commands-Ckfii7h8.js} +4 -4
- package/dist/plugin-sdk/{skills-C5bT9-q4.js → skills-v0WQqKTa.js} +6 -6
- package/dist/plugin-sdk/slash-commands.runtime-BYoxsxkX.js +13 -0
- package/dist/plugin-sdk/{slash-dispatch.runtime-Dyb_EkUt.js → slash-dispatch.runtime-DOo1IzuY.js} +1 -1
- package/dist/plugin-sdk/slash-dispatch.runtime-JfFr7bNy.js +52 -0
- package/dist/plugin-sdk/slash-skill-commands.runtime-D0399tia.js +16 -0
- package/dist/plugin-sdk/{store-D3lgWnS2.js → store-C_UrNuM3.js} +2 -2
- package/dist/plugin-sdk/{subagent-registry-runtime-hdnabonI.js → subagent-registry-runtime-DLvup9ph.js} +1 -1
- package/dist/plugin-sdk/subagent-registry-runtime-DVl5xKOW.js +52 -0
- package/dist/plugin-sdk/{tables-Dmlu4_q_.js → tables-CJP05iMa.js} +1 -1
- package/dist/plugin-sdk/{thinking-DRNOh5Xx.js → thinking-BhP3vn1l.js} +7 -7
- package/dist/plugin-sdk/{tokens-CTIYTLWu.js → tokens-DAL_5WHL.js} +1 -1
- package/dist/plugin-sdk/{tool-images-GSlvf6RP.js → tool-images-ZE5G5UM0.js} +2 -2
- package/dist/plugin-sdk/{web-D9LAdsuU.js → web-C2Sj2WW0.js} +2 -2
- package/dist/plugin-sdk/web-CNY_ky8h.js +56 -0
- package/dist/plugin-sdk/{whatsapp-actions-BCJYmWCB.js → whatsapp-actions-DJmx6ihj.js} +17 -17
- package/dist/plugin-sdk/whatsapp.js +50 -50
- package/dist/{plugins-AqsVZZk3.js → plugins-DvejjZnJ.js} +11 -11
- package/dist/{plugins-cli-BYnoJwF1.js → plugins-cli-BA_2daJe.js} +2 -2
- package/dist/{plugins-cli-gtgkmDcv.js → plugins-cli-C0WKWrZo.js} +2 -2
- package/dist/{program-BGlgDpHI.js → program-D_xdLzmM.js} +7 -7
- package/dist/{program-context-M4FTWqqg.js → program-context-DfmCIQ91.js} +17 -17
- package/dist/{prompt-select-styled-AL5Tm3yO.js → prompt-select-styled-ApPSadr5.js} +4 -4
- package/dist/{prompt-select-styled-BVC2byQV.js → prompt-select-styled-BsheNEnh.js} +4 -4
- package/dist/{provider-auth-helpers-DDgrFku5.js → provider-auth-helpers-B5kD4Lt6.js} +1 -1
- package/dist/{provider-auth-helpers-Q7aIhDgv.js → provider-auth-helpers-DpOFkV28.js} +1 -1
- package/dist/{proxy-env-DXXfs2WL.js → proxy-env-D-fike7s.js} +1 -1
- package/dist/{plugin-sdk/proxy-fetch-Dt5BedH8.js → proxy-fetch-lH6RsRTE.js} +1 -1
- package/dist/{push-apns-Ga8xvhkh.js → push-apns-BbenjEX4.js} +1 -1
- package/dist/{push-apns-FyGbUyh2.js → push-apns-Ci2sdIKf.js} +1 -1
- package/dist/{pw-ai-GOBxzChI.js → pw-ai-B8ymIYub.js} +14 -14
- package/dist/{pw-ai-Du22SYoO.js → pw-ai-CM87tQwG.js} +1 -1
- package/dist/{qmd-manager-CO-shcLU.js → qmd-manager-BN0siR2Z.js} +10 -10
- package/dist/{query-expansion-DlQOkf-g.js → query-expansion-Dzxt6kXo.js} +6 -6
- package/dist/{redact-NmPEVjIo.js → redact-DvzicBMu.js} +1 -1
- package/dist/{register.agent-koAXw1ay.js → register.agent-CY6R-7fn.js} +6 -6
- package/dist/{register.agent-zP8a-sNB.js → register.agent-Dk_5V5oA.js} +7 -7
- package/dist/{register.configure-CUuP5J8W.js → register.configure-C8OFfGln.js} +7 -7
- package/dist/{register.configure-CYrmyjlf.js → register.configure-DT1RLeTR.js} +7 -7
- package/dist/{register.maintenance-BZuEljPU.js → register.maintenance-CluQOq8b.js} +8 -8
- package/dist/{register.maintenance-ByEQ2CVH.js → register.maintenance-YHXmOQzf.js} +7 -7
- package/dist/{register.message-BkSXhv9I.js → register.message-BCTXT5yK.js} +2 -2
- package/dist/{register.message-CgHfmDHm.js → register.message-BuoFjRqA.js} +2 -2
- package/dist/{register.onboard-HcQwPAEK.js → register.onboard-BJaRMtxU.js} +2 -2
- package/dist/{register.onboard-4ke40MNP.js → register.onboard-C64oXnvq.js} +2 -2
- package/dist/{register.setup-B41xZb1r.js → register.setup-Dl3nk2Ui.js} +2 -2
- package/dist/{register.setup-xAb7Cvzy.js → register.setup-Drc55K_w.js} +2 -2
- package/dist/{register.status-health-sessions-CcOVL0x8.js → register.status-health-sessions-BHtbPeCO.js} +3 -3
- package/dist/{register.status-health-sessions-BbHAAKwv.js → register.status-health-sessions-xgC_gtEM.js} +3 -3
- package/dist/{register.subclis-_uSASnfC.js → register.subclis-CDMFyaKR.js} +9 -9
- package/dist/{reply-BFTOdZcK.js → reply-DwjHsBuj.js} +137 -40
- package/dist/{run-main-C1eZrW-5.js → run-main-BcA22Ipv.js} +14 -14
- package/dist/{run-with-concurrency-FczpX8ng.js → run-with-concurrency-BFR3ReeF.js} +4 -4
- package/dist/runtime-whatsapp-login.runtime-DxV9iv6l.js +13 -0
- package/dist/runtime-whatsapp-outbound.runtime-DQqIlwhS.js +22 -0
- package/dist/{send-Bx9CqcZr.js → send-4nRsZJXH.js} +7 -7
- package/dist/{send-D5D0ZGht.js → send-BGlcHjUD.js} +26 -26
- package/dist/{send-DtHQ7_6Z.js → send-BTUU1jWM.js} +8 -8
- package/dist/{send-B9H0BkfO.js → send-DdBbRpTP.js} +6 -6
- package/dist/{send-B5QEmMJ4.js → send-dTQd-IyJ.js} +5 -5
- package/dist/{server-node-events-pYrJmi9w.js → server-node-events-Q0cT2ifj.js} +2 -2
- package/dist/{server-node-events-DbS1jJ5j.js → server-node-events-YN5eJeHa.js} +2 -2
- package/dist/{session-DLTCuoUD.js → session-CnCwDJke.js} +8 -8
- package/dist/{skill-commands-BwTLQRR8.js → skill-commands-Cz45_nme.js} +9 -9
- package/dist/{skills-B9N2bqKU.js → skills-CdCS1HeL.js} +22 -22
- package/dist/slash-commands.runtime-CZz6v6b3.js +16 -0
- package/dist/{slash-dispatch.runtime-PX9_08Hm.js → slash-dispatch.runtime-BOMEVFk0.js} +1 -1
- package/dist/{slash-dispatch.runtime-DmFVOi3w.js → slash-dispatch.runtime-DB1QsBRc.js} +1 -1
- package/dist/slash-dispatch.runtime-SO7HOe8q.js +56 -0
- package/dist/{slash-dispatch.runtime-BGXbtMX6.js → slash-dispatch.runtime-sXaUYn4v.js} +6 -6
- package/dist/slash-skill-commands.runtime-Bawt7j0r.js +20 -0
- package/dist/{status-DtiapFqq.js → status-Bh5x6DsW.js} +2 -2
- package/dist/{status-B4eVRJcO.js → status-cU9cJReo.js} +2 -2
- package/dist/{store-CvQ41zCV.js → store-D9z0dn7D.js} +2 -2
- package/dist/{subagent-registry-Ctrhw4X-.js → subagent-registry-CIgFD2oi.js} +103 -6
- package/dist/{subagent-registry-runtime-CezPaX9P.js → subagent-registry-runtime-232sNNT6.js} +6 -6
- package/dist/subagent-registry-runtime-D2rUxU5J.js +56 -0
- package/dist/{subagent-registry-runtime-Cy7dsGqF.js → subagent-registry-runtime-D7ubAnDZ.js} +1 -1
- package/dist/{subagent-registry-runtime-CTjx4TK5.js → subagent-registry-runtime-DGTjU9Lb.js} +1 -1
- package/dist/{subsystem-BaLYRf7D.js → subsystem-6v7sWnAD.js} +14 -14
- package/dist/{tables-BRYYxYs7.js → tables-BTFiZyRU.js} +1 -1
- package/dist/{target-errors-D0ZJUbbf.js → target-errors-DgNRx3Nr.js} +2 -2
- package/dist/{thinking-B-A99X3F.js → thinking-B75CXkTT.js} +7 -7
- package/dist/{tokens-D2XhLqIz.js → tokens-DfbMVF9y.js} +1 -1
- package/dist/{tool-images-Zn6jVmkX.js → tool-images-Dp5OiXeA.js} +2 -2
- package/dist/{update-cli-_FsTRdQZ.js → update-cli-CktBOXZF.js} +7 -7
- package/dist/{update-cli-DLOWRdjv.js → update-cli-DM_dUip_.js} +8 -8
- package/dist/{update-runner-BY3l7ytB.js → update-runner-BBJZmfZ4.js} +1 -1
- package/dist/{update-runner-BKWHnVYI.js → update-runner-BYGPkHXG.js} +1 -1
- package/dist/{web-BdZ3mX3p.js → web-CZhZC1EA.js} +2 -2
- package/dist/{web-B46aOGK0.js → web-Chw1dtKA.js} +6 -6
- package/dist/{web-BNfDYvlW.js → web-DWRZAXj9.js} +2 -2
- package/dist/{web-1D0d1RHD.js → web-QsxgXbNK.js} +55 -55
- package/dist/{whatsapp-actions-Cuy0qeQK.js → whatsapp-actions-CzqsuSGx.js} +21 -21
- package/dist/{workspace-TqfVSQuO.js → workspace-kVMIaBrV.js} +20 -20
- package/extensions/acpx/node_modules/.bin/acpx +2 -2
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +2 -2
- package/extensions/diffs/node_modules/.bin/playwright-core +2 -2
- package/extensions/googlechat/node_modules/.bin/squidclaw +2 -2
- package/extensions/matrix/node_modules/.bin/markdown-it +2 -2
- package/extensions/memory-core/node_modules/.bin/squidclaw +2 -2
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +2 -2
- package/extensions/memory-lancedb/node_modules/.bin/openai +2 -2
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/tlon/node_modules/.bin/tlon +2 -2
- package/package.json +1 -1
- package/dist/api-key-rotation-TRwuCWbu.js +0 -181
- package/dist/canvas-host/a2ui/.bundle.hash +0 -1
- package/dist/deliver-runtime-_mBfV_4S.js +0 -36
- package/dist/deps-send-discord.runtime-CUTAK2hy.js +0 -26
- package/dist/deps-send-imessage.runtime-CQOiEIuA.js +0 -25
- package/dist/deps-send-signal.runtime-wN7MkzLw.js +0 -24
- package/dist/deps-send-slack.runtime-HEEwW4uU.js +0 -22
- package/dist/deps-send-telegram.runtime-DHlcnjQO.js +0 -27
- package/dist/deps-send-whatsapp.runtime-Cw7U9orE.js +0 -60
- package/dist/errors-DfgAh2Ml.js +0 -54
- package/dist/export-html/vendor/highlight.min.js +0 -1213
- package/dist/export-html/vendor/marked.min.js +0 -6
- package/dist/image-runtime-D11wBIC8.js +0 -29
- package/dist/manager-runtime-BB9lcoFR.js +0 -18
- package/dist/pi-model-discovery-runtime-DBkQoIJw.js +0 -11
- package/dist/plugin-sdk/accounts-CUEuUR3C.js +0 -46
- package/dist/plugin-sdk/accounts-D0W2pELU.js +0 -288
- package/dist/plugin-sdk/accounts-ucj0odJq.js +0 -35
- package/dist/plugin-sdk/active-listener-C5xPUSTb.js +0 -50
- package/dist/plugin-sdk/audio-preflight-Cqdo0JKm.js +0 -69
- package/dist/plugin-sdk/audio-transcription-runner-DnEooIyE.js +0 -2176
- package/dist/plugin-sdk/audit-membership-runtime-B9b-zRwg.js +0 -58
- package/dist/plugin-sdk/channel-activity-BMWLw4o2.js +0 -94
- package/dist/plugin-sdk/channel-web-D9WdAFlv.js +0 -2256
- package/dist/plugin-sdk/chrome-CV-q0Lmc.js +0 -2415
- package/dist/plugin-sdk/commands-registry-e7YoqrbP.js +0 -1125
- package/dist/plugin-sdk/config-B2B64aX0.js +0 -17911
- package/dist/plugin-sdk/deliver-BkyBtcLR.js +0 -1694
- package/dist/plugin-sdk/deliver-runtime-5UVcSskg.js +0 -32
- package/dist/plugin-sdk/deliver-runtime-O4lwAWMw.js +0 -32
- package/dist/plugin-sdk/deps-send-discord.runtime-BAeeBldY.js +0 -23
- package/dist/plugin-sdk/deps-send-discord.runtime-DTspXSCt.js +0 -23
- package/dist/plugin-sdk/deps-send-imessage.runtime-EL-CfikZ.js +0 -22
- package/dist/plugin-sdk/deps-send-imessage.runtime-qThAwDEe.js +0 -22
- package/dist/plugin-sdk/deps-send-signal.runtime-BeemHeUu.js +0 -21
- package/dist/plugin-sdk/deps-send-signal.runtime-DnjnVzZF.js +0 -21
- package/dist/plugin-sdk/deps-send-slack.runtime-CbKevLnv.js +0 -19
- package/dist/plugin-sdk/deps-send-slack.runtime-DTttkC0N.js +0 -19
- package/dist/plugin-sdk/deps-send-telegram.runtime-C6y29O9E.js +0 -24
- package/dist/plugin-sdk/deps-send-telegram.runtime-Dsf9Cnka.js +0 -24
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-CCS71r77.js +0 -57
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-CuxKhIE0.js +0 -57
- package/dist/plugin-sdk/diagnostic-DPRVoKTk.js +0 -319
- package/dist/plugin-sdk/fetch-guard-F0Fnqisy.js +0 -156
- package/dist/plugin-sdk/fs-safe-Dqmpk-Fr.js +0 -352
- package/dist/plugin-sdk/image-cBW8L8pp.js +0 -2310
- package/dist/plugin-sdk/image-ops-BP8ix1GC.js +0 -584
- package/dist/plugin-sdk/image-runtime-9xkgSlNz.js +0 -25
- package/dist/plugin-sdk/image-runtime-CpfepTDc.js +0 -25
- package/dist/plugin-sdk/ir-DWEc6zOp.js +0 -1296
- package/dist/plugin-sdk/local-roots-BIPT8uAO.js +0 -186
- package/dist/plugin-sdk/logger-DDdrdbDu.js +0 -1163
- package/dist/plugin-sdk/login-BMTiGutN.js +0 -57
- package/dist/plugin-sdk/login-qr-BFxqYUkc.js +0 -320
- package/dist/plugin-sdk/manager-BD-aYaZ8.js +0 -3917
- package/dist/plugin-sdk/manager-runtime-BmgTeb5G.js +0 -15
- package/dist/plugin-sdk/manager-runtime-C5bRwUlz.js +0 -15
- package/dist/plugin-sdk/outbound-Bm07xvO6.js +0 -212
- package/dist/plugin-sdk/outbound-attachment-DLsaxDsc.js +0 -19
- package/dist/plugin-sdk/path-alias-guards-gBhrAn14.js +0 -43
- package/dist/plugin-sdk/paths-C6W4VHoa.js +0 -166
- package/dist/plugin-sdk/pi-embedded-helpers-BExwPvTh.js +0 -9627
- package/dist/plugin-sdk/pi-model-discovery-DdctvBeX.js +0 -134
- package/dist/plugin-sdk/pi-model-discovery-runtime-COnuGwZt.js +0 -8
- package/dist/plugin-sdk/pi-model-discovery-runtime-DrtpLJjk.js +0 -8
- package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-rgTz3FBl.js +0 -354
- package/dist/plugin-sdk/plugins-BN64HHZA.js +0 -864
- package/dist/plugin-sdk/pw-ai-DBm3RdBK.js +0 -1938
- package/dist/plugin-sdk/qmd-manager-6bozlfFg.js +0 -1448
- package/dist/plugin-sdk/query-expansion-eeVz_aEm.js +0 -1011
- package/dist/plugin-sdk/redact-BoNEjbpF.js +0 -319
- package/dist/plugin-sdk/reply-CqXMy3Yd.js +0 -98731
- package/dist/plugin-sdk/resolve-outbound-target-DXfjGlZQ.js +0 -40
- package/dist/plugin-sdk/run-with-concurrency-5DMu9szx.js +0 -1994
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-99sCh8RG.js +0 -10
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-D2hkJBa-.js +0 -10
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-C06I4adi.js +0 -19
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-dTkDaXHl.js +0 -19
- package/dist/plugin-sdk/send-B9xnwtQ-.js +0 -540
- package/dist/plugin-sdk/send-BxySs-Cu.js +0 -2587
- package/dist/plugin-sdk/send-D9THKp_J.js +0 -414
- package/dist/plugin-sdk/send-DCuaaP2w.js +0 -503
- package/dist/plugin-sdk/send-Dm_-_xje.js +0 -3135
- package/dist/plugin-sdk/session-DDzIZHxt.js +0 -169
- package/dist/plugin-sdk/skill-commands-DRvqtuFC.js +0 -342
- package/dist/plugin-sdk/skills-BWwlfbVj.js +0 -1428
- package/dist/plugin-sdk/slash-commands.runtime-BzYsaTST.js +0 -13
- package/dist/plugin-sdk/slash-commands.runtime-EAZKpRKq.js +0 -13
- package/dist/plugin-sdk/slash-dispatch.runtime-BC3IAF-I.js +0 -52
- package/dist/plugin-sdk/slash-dispatch.runtime-C7u7mlny.js +0 -52
- package/dist/plugin-sdk/slash-skill-commands.runtime-DGd_JSWX.js +0 -16
- package/dist/plugin-sdk/slash-skill-commands.runtime-sg98L8BK.js +0 -16
- package/dist/plugin-sdk/ssrf-DOBwjFow.js +0 -202
- package/dist/plugin-sdk/store-BKDR_-Qk.js +0 -81
- package/dist/plugin-sdk/subagent-registry-runtime-B6l6PxqP.js +0 -52
- package/dist/plugin-sdk/subagent-registry-runtime-Ceu7AzWi.js +0 -52
- package/dist/plugin-sdk/tables-GIj79us5.js +0 -55
- package/dist/plugin-sdk/target-errors-jlLHihbX.js +0 -195
- package/dist/plugin-sdk/thinking-BgdUnMZ2.js +0 -1206
- package/dist/plugin-sdk/tokens-CLE20fRI.js +0 -52
- package/dist/plugin-sdk/tool-images-BY1gsRyE.js +0 -274
- package/dist/plugin-sdk/web-BK2-uaOo.js +0 -56
- package/dist/plugin-sdk/web-Bnn82S4u.js +0 -56
- package/dist/plugin-sdk/whatsapp-actions-DQpK_5Ds.js +0 -80
- package/dist/proxy-fetch-C2v-Utgg.js +0 -38
- package/dist/runtime-whatsapp-login.runtime-DumUjKRz.js +0 -13
- package/dist/runtime-whatsapp-outbound.runtime-Cgu2MfqR.js +0 -22
- package/dist/slash-commands.runtime-CjBXruwG.js +0 -16
- package/dist/slash-dispatch.runtime-DqZVfX4H.js +0 -56
- package/dist/slash-skill-commands.runtime-DYK20Lxf.js +0 -20
- package/dist/subagent-registry-runtime-BIKGAzgI.js +0 -56
- package/docs/reference/templates/IDENTITY.md +0 -29
- package/docs/reference/templates/USER.md +0 -23
- package/docs/zh-CN/reference/templates/IDENTITY.md +0 -36
- package/docs/zh-CN/reference/templates/USER.md +0 -30
- package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +0 -178
|
@@ -1,2415 +0,0 @@
|
|
|
1
|
-
import { R as isNotFoundPathError, z as isPathInside } from "./run-with-concurrency-5DMu9szx.js";
|
|
2
|
-
import { Q as normalizeIpAddress, X as isLoopbackIpAddress, Z as isPrivateOrLoopbackIpAddress, n as loadConfig } from "./config-B2B64aX0.js";
|
|
3
|
-
import { X as resolvePreferredSquidClawTmpDir, a as createSubsystemLogger, f as CONFIG_DIR } from "./logger-DDdrdbDu.js";
|
|
4
|
-
import { a as isErrno } from "./errors-B8oJXuCF.js";
|
|
5
|
-
import { i as openFileWithinRoot, t as SafeOpenError } from "./fs-safe-Dqmpk-Fr.js";
|
|
6
|
-
import { c as resolvePinnedHostnameWithPolicy, s as isPrivateNetworkAllowedByPolicy, u as hasProxyEnvConfigured } from "./ssrf-DOBwjFow.js";
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import fs$1 from "node:fs/promises";
|
|
11
|
-
import { createHmac } from "node:crypto";
|
|
12
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
13
|
-
import { Buffer as Buffer$1 } from "node:buffer";
|
|
14
|
-
import http, { createServer } from "node:http";
|
|
15
|
-
import https from "node:https";
|
|
16
|
-
import WebSocket, { WebSocketServer } from "ws";
|
|
17
|
-
import net from "node:net";
|
|
18
|
-
|
|
19
|
-
//#region src/infra/ws.ts
|
|
20
|
-
function rawDataToString(data, encoding = "utf8") {
|
|
21
|
-
if (typeof data === "string") return data;
|
|
22
|
-
if (Buffer$1.isBuffer(data)) return data.toString(encoding);
|
|
23
|
-
if (Array.isArray(data)) return Buffer$1.concat(data).toString(encoding);
|
|
24
|
-
if (data instanceof ArrayBuffer) return Buffer$1.from(data).toString(encoding);
|
|
25
|
-
return Buffer$1.from(String(data)).toString(encoding);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
//#endregion
|
|
29
|
-
//#region src/gateway/net.ts
|
|
30
|
-
function isLoopbackAddress(ip) {
|
|
31
|
-
return isLoopbackIpAddress(ip);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Returns true if the IP belongs to a private or loopback network range.
|
|
35
|
-
* Private ranges: RFC1918, link-local, ULA IPv6, and CGNAT (100.64/10), plus loopback.
|
|
36
|
-
*/
|
|
37
|
-
function isPrivateOrLoopbackAddress(ip) {
|
|
38
|
-
return isPrivateOrLoopbackIpAddress(ip);
|
|
39
|
-
}
|
|
40
|
-
function normalizeIp(ip) {
|
|
41
|
-
return normalizeIpAddress(ip);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Check if a hostname or IP refers to the local machine.
|
|
45
|
-
* Handles: localhost, 127.x.x.x, ::1, [::1], ::ffff:127.x.x.x
|
|
46
|
-
* Note: 0.0.0.0 and :: are NOT loopback - they bind to all interfaces.
|
|
47
|
-
*/
|
|
48
|
-
function isLoopbackHost(host) {
|
|
49
|
-
const parsed = parseHostForAddressChecks(host);
|
|
50
|
-
if (!parsed) return false;
|
|
51
|
-
if (parsed.isLocalhost) return true;
|
|
52
|
-
return isLoopbackAddress(parsed.unbracketedHost);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Check if a hostname or IP refers to a private or loopback address.
|
|
56
|
-
* Handles the same hostname formats as isLoopbackHost, but also accepts
|
|
57
|
-
* RFC 1918, link-local, CGNAT, and IPv6 ULA/link-local addresses.
|
|
58
|
-
*/
|
|
59
|
-
function isPrivateOrLoopbackHost(host) {
|
|
60
|
-
const parsed = parseHostForAddressChecks(host);
|
|
61
|
-
if (!parsed) return false;
|
|
62
|
-
if (parsed.isLocalhost) return true;
|
|
63
|
-
const normalized = normalizeIp(parsed.unbracketedHost);
|
|
64
|
-
if (!normalized || !isPrivateOrLoopbackAddress(normalized)) return false;
|
|
65
|
-
if (net.isIP(normalized) === 6) {
|
|
66
|
-
if (normalized.startsWith("ff")) return false;
|
|
67
|
-
if (normalized === "::") return false;
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
function parseHostForAddressChecks(host) {
|
|
72
|
-
if (!host) return null;
|
|
73
|
-
const normalizedHost = host.trim().toLowerCase();
|
|
74
|
-
if (normalizedHost === "localhost") return {
|
|
75
|
-
isLocalhost: true,
|
|
76
|
-
unbracketedHost: normalizedHost
|
|
77
|
-
};
|
|
78
|
-
return {
|
|
79
|
-
isLocalhost: false,
|
|
80
|
-
unbracketedHost: normalizedHost.startsWith("[") && normalizedHost.endsWith("]") ? normalizedHost.slice(1, -1) : normalizedHost
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Security check for WebSocket URLs (CWE-319: Cleartext Transmission of Sensitive Information).
|
|
85
|
-
*
|
|
86
|
-
* Returns true if the URL is secure for transmitting data:
|
|
87
|
-
* - wss:// (TLS) is always secure
|
|
88
|
-
* - ws:// is secure only for loopback addresses by default
|
|
89
|
-
* - optional break-glass: private ws:// can be enabled for trusted networks
|
|
90
|
-
*
|
|
91
|
-
* All other ws:// URLs are considered insecure because both credentials
|
|
92
|
-
* AND chat/conversation data would be exposed to network interception.
|
|
93
|
-
*/
|
|
94
|
-
function isSecureWebSocketUrl(url, opts) {
|
|
95
|
-
let parsed;
|
|
96
|
-
try {
|
|
97
|
-
parsed = new URL(url);
|
|
98
|
-
} catch {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
if (parsed.protocol === "wss:") return true;
|
|
102
|
-
if (parsed.protocol !== "ws:") return false;
|
|
103
|
-
if (isLoopbackHost(parsed.hostname)) return true;
|
|
104
|
-
if (opts?.allowPrivateWs) return isPrivateOrLoopbackHost(parsed.hostname);
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/browser/constants.ts
|
|
110
|
-
const DEFAULT_SQUIDCLAW_BROWSER_ENABLED = true;
|
|
111
|
-
const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
|
|
112
|
-
const DEFAULT_SQUIDCLAW_BROWSER_COLOR = "#FF4500";
|
|
113
|
-
const DEFAULT_SQUIDCLAW_BROWSER_PROFILE_NAME = "squidclaw";
|
|
114
|
-
const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "squidclaw";
|
|
115
|
-
const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 8e4;
|
|
116
|
-
const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 1e4;
|
|
117
|
-
const DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;
|
|
118
|
-
|
|
119
|
-
//#endregion
|
|
120
|
-
//#region src/browser/form-fields.ts
|
|
121
|
-
const DEFAULT_FILL_FIELD_TYPE = "text";
|
|
122
|
-
function normalizeBrowserFormFieldRef(value) {
|
|
123
|
-
return typeof value === "string" ? value.trim() : "";
|
|
124
|
-
}
|
|
125
|
-
function normalizeBrowserFormFieldType(value) {
|
|
126
|
-
return (typeof value === "string" ? value.trim() : "") || DEFAULT_FILL_FIELD_TYPE;
|
|
127
|
-
}
|
|
128
|
-
function normalizeBrowserFormFieldValue(value) {
|
|
129
|
-
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : void 0;
|
|
130
|
-
}
|
|
131
|
-
function normalizeBrowserFormField(record) {
|
|
132
|
-
const ref = normalizeBrowserFormFieldRef(record.ref);
|
|
133
|
-
if (!ref) return null;
|
|
134
|
-
const type = normalizeBrowserFormFieldType(record.type);
|
|
135
|
-
const value = normalizeBrowserFormFieldValue(record.value);
|
|
136
|
-
return value === void 0 ? {
|
|
137
|
-
ref,
|
|
138
|
-
type
|
|
139
|
-
} : {
|
|
140
|
-
ref,
|
|
141
|
-
type,
|
|
142
|
-
value
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region src/browser/paths.ts
|
|
148
|
-
const DEFAULT_BROWSER_TMP_DIR = resolvePreferredSquidClawTmpDir();
|
|
149
|
-
const DEFAULT_TRACE_DIR = DEFAULT_BROWSER_TMP_DIR;
|
|
150
|
-
const DEFAULT_DOWNLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
|
|
151
|
-
const DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
|
|
152
|
-
function invalidPath(scopeLabel) {
|
|
153
|
-
return {
|
|
154
|
-
ok: false,
|
|
155
|
-
error: `Invalid path: must stay within ${scopeLabel}`
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
async function resolveRealPathIfExists(targetPath) {
|
|
159
|
-
try {
|
|
160
|
-
return await fs$1.realpath(targetPath);
|
|
161
|
-
} catch {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
async function resolveTrustedRootRealPath(rootDir) {
|
|
166
|
-
try {
|
|
167
|
-
const rootLstat = await fs$1.lstat(rootDir);
|
|
168
|
-
if (!rootLstat.isDirectory() || rootLstat.isSymbolicLink()) return;
|
|
169
|
-
return await fs$1.realpath(rootDir);
|
|
170
|
-
} catch {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
async function validateCanonicalPathWithinRoot(params) {
|
|
175
|
-
try {
|
|
176
|
-
const candidateLstat = await fs$1.lstat(params.candidatePath);
|
|
177
|
-
if (candidateLstat.isSymbolicLink()) return "invalid";
|
|
178
|
-
if (params.expect === "directory" && !candidateLstat.isDirectory()) return "invalid";
|
|
179
|
-
if (params.expect === "file" && !candidateLstat.isFile()) return "invalid";
|
|
180
|
-
if (params.expect === "file" && candidateLstat.nlink > 1) return "invalid";
|
|
181
|
-
const candidateRealPath = await fs$1.realpath(params.candidatePath);
|
|
182
|
-
return isPathInside(params.rootRealPath, candidateRealPath) ? "ok" : "invalid";
|
|
183
|
-
} catch (err) {
|
|
184
|
-
return isNotFoundPathError(err) ? "not-found" : "invalid";
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
function resolvePathWithinRoot(params) {
|
|
188
|
-
const root = path.resolve(params.rootDir);
|
|
189
|
-
const raw = params.requestedPath.trim();
|
|
190
|
-
if (!raw) {
|
|
191
|
-
if (!params.defaultFileName) return {
|
|
192
|
-
ok: false,
|
|
193
|
-
error: "path is required"
|
|
194
|
-
};
|
|
195
|
-
return {
|
|
196
|
-
ok: true,
|
|
197
|
-
path: path.join(root, params.defaultFileName)
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
const resolved = path.resolve(root, raw);
|
|
201
|
-
const rel = path.relative(root, resolved);
|
|
202
|
-
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return {
|
|
203
|
-
ok: false,
|
|
204
|
-
error: `Invalid path: must stay within ${params.scopeLabel}`
|
|
205
|
-
};
|
|
206
|
-
return {
|
|
207
|
-
ok: true,
|
|
208
|
-
path: resolved
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
async function resolveWritablePathWithinRoot(params) {
|
|
212
|
-
const lexical = resolvePathWithinRoot(params);
|
|
213
|
-
if (!lexical.ok) return lexical;
|
|
214
|
-
const rootRealPath = await resolveTrustedRootRealPath(path.resolve(params.rootDir));
|
|
215
|
-
if (!rootRealPath) return invalidPath(params.scopeLabel);
|
|
216
|
-
const requestedPath = lexical.path;
|
|
217
|
-
if (await validateCanonicalPathWithinRoot({
|
|
218
|
-
rootRealPath,
|
|
219
|
-
candidatePath: path.dirname(requestedPath),
|
|
220
|
-
expect: "directory"
|
|
221
|
-
}) !== "ok") return invalidPath(params.scopeLabel);
|
|
222
|
-
if (await validateCanonicalPathWithinRoot({
|
|
223
|
-
rootRealPath,
|
|
224
|
-
candidatePath: requestedPath,
|
|
225
|
-
expect: "file"
|
|
226
|
-
}) === "invalid") return invalidPath(params.scopeLabel);
|
|
227
|
-
return lexical;
|
|
228
|
-
}
|
|
229
|
-
async function resolveExistingPathsWithinRoot(params) {
|
|
230
|
-
return await resolveCheckedPathsWithinRoot({
|
|
231
|
-
...params,
|
|
232
|
-
allowMissingFallback: true
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
async function resolveStrictExistingPathsWithinRoot(params) {
|
|
236
|
-
return await resolveCheckedPathsWithinRoot({
|
|
237
|
-
...params,
|
|
238
|
-
allowMissingFallback: false
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
async function resolveCheckedPathsWithinRoot(params) {
|
|
242
|
-
const rootDir = path.resolve(params.rootDir);
|
|
243
|
-
const rootRealPath = await resolveRealPathIfExists(rootDir);
|
|
244
|
-
const isInRoot = (relativePath) => Boolean(relativePath) && !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
|
245
|
-
const resolveExistingRelativePath = async (requestedPath) => {
|
|
246
|
-
const raw = requestedPath.trim();
|
|
247
|
-
const lexicalPathResult = resolvePathWithinRoot({
|
|
248
|
-
rootDir,
|
|
249
|
-
requestedPath,
|
|
250
|
-
scopeLabel: params.scopeLabel
|
|
251
|
-
});
|
|
252
|
-
if (lexicalPathResult.ok) return {
|
|
253
|
-
ok: true,
|
|
254
|
-
relativePath: path.relative(rootDir, lexicalPathResult.path),
|
|
255
|
-
fallbackPath: lexicalPathResult.path
|
|
256
|
-
};
|
|
257
|
-
if (!rootRealPath || !raw || !path.isAbsolute(raw)) return lexicalPathResult;
|
|
258
|
-
try {
|
|
259
|
-
const resolvedExistingPath = await fs$1.realpath(raw);
|
|
260
|
-
const relativePath = path.relative(rootRealPath, resolvedExistingPath);
|
|
261
|
-
if (!isInRoot(relativePath)) return lexicalPathResult;
|
|
262
|
-
return {
|
|
263
|
-
ok: true,
|
|
264
|
-
relativePath,
|
|
265
|
-
fallbackPath: resolvedExistingPath
|
|
266
|
-
};
|
|
267
|
-
} catch {
|
|
268
|
-
return lexicalPathResult;
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
const resolvedPaths = [];
|
|
272
|
-
for (const raw of params.requestedPaths) {
|
|
273
|
-
const pathResult = await resolveExistingRelativePath(raw);
|
|
274
|
-
if (!pathResult.ok) return {
|
|
275
|
-
ok: false,
|
|
276
|
-
error: pathResult.error
|
|
277
|
-
};
|
|
278
|
-
let opened;
|
|
279
|
-
try {
|
|
280
|
-
opened = await openFileWithinRoot({
|
|
281
|
-
rootDir,
|
|
282
|
-
relativePath: pathResult.relativePath
|
|
283
|
-
});
|
|
284
|
-
resolvedPaths.push(opened.realPath);
|
|
285
|
-
} catch (err) {
|
|
286
|
-
if (params.allowMissingFallback && err instanceof SafeOpenError && err.code === "not-found") {
|
|
287
|
-
resolvedPaths.push(pathResult.fallbackPath);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (err instanceof SafeOpenError && err.code === "outside-workspace") return {
|
|
291
|
-
ok: false,
|
|
292
|
-
error: `File is outside ${params.scopeLabel}`
|
|
293
|
-
};
|
|
294
|
-
return {
|
|
295
|
-
ok: false,
|
|
296
|
-
error: `Invalid path: must stay within ${params.scopeLabel} and be a regular non-symlink file`
|
|
297
|
-
};
|
|
298
|
-
} finally {
|
|
299
|
-
await opened?.handle.close().catch(() => {});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return {
|
|
303
|
-
ok: true,
|
|
304
|
-
paths: resolvedPaths
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
//#endregion
|
|
309
|
-
//#region src/browser/cdp-proxy-bypass.ts
|
|
310
|
-
/**
|
|
311
|
-
* Proxy bypass for CDP (Chrome DevTools Protocol) localhost connections.
|
|
312
|
-
*
|
|
313
|
-
* When HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables are set,
|
|
314
|
-
* CDP connections to localhost/127.0.0.1 can be incorrectly routed through
|
|
315
|
-
* the proxy, causing browser control to fail.
|
|
316
|
-
*
|
|
317
|
-
* @see https://github.com/nicepkg/squidclaw/issues/31219
|
|
318
|
-
*/
|
|
319
|
-
/** HTTP agent that never uses a proxy — for localhost CDP connections. */
|
|
320
|
-
const directHttpAgent = new http.Agent();
|
|
321
|
-
const directHttpsAgent = new https.Agent();
|
|
322
|
-
/**
|
|
323
|
-
* Returns a plain (non-proxy) agent for WebSocket or HTTP connections
|
|
324
|
-
* when the target is a loopback address. Returns `undefined` otherwise
|
|
325
|
-
* so callers fall through to their default behaviour.
|
|
326
|
-
*/
|
|
327
|
-
function getDirectAgentForCdp(url) {
|
|
328
|
-
try {
|
|
329
|
-
const parsed = new URL(url);
|
|
330
|
-
if (isLoopbackHost(parsed.hostname)) return parsed.protocol === "https:" || parsed.protocol === "wss:" ? directHttpsAgent : directHttpAgent;
|
|
331
|
-
} catch {}
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Returns `true` when any proxy-related env var is set that could
|
|
335
|
-
* interfere with loopback connections.
|
|
336
|
-
*/
|
|
337
|
-
function hasProxyEnv() {
|
|
338
|
-
return hasProxyEnvConfigured();
|
|
339
|
-
}
|
|
340
|
-
const LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
341
|
-
function noProxyAlreadyCoversLocalhost() {
|
|
342
|
-
const current = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
343
|
-
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
344
|
-
}
|
|
345
|
-
function isLoopbackCdpUrl(url) {
|
|
346
|
-
try {
|
|
347
|
-
return isLoopbackHost(new URL(url).hostname);
|
|
348
|
-
} catch {
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
var NoProxyLeaseManager = class {
|
|
353
|
-
constructor() {
|
|
354
|
-
this.leaseCount = 0;
|
|
355
|
-
this.snapshot = null;
|
|
356
|
-
}
|
|
357
|
-
acquire(url) {
|
|
358
|
-
if (!isLoopbackCdpUrl(url) || !hasProxyEnv()) return null;
|
|
359
|
-
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
360
|
-
const noProxy = process.env.NO_PROXY;
|
|
361
|
-
const noProxyLower = process.env.no_proxy;
|
|
362
|
-
const current = noProxy || noProxyLower || "";
|
|
363
|
-
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
364
|
-
process.env.NO_PROXY = applied;
|
|
365
|
-
process.env.no_proxy = applied;
|
|
366
|
-
this.snapshot = {
|
|
367
|
-
noProxy,
|
|
368
|
-
noProxyLower,
|
|
369
|
-
applied
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
this.leaseCount += 1;
|
|
373
|
-
let released = false;
|
|
374
|
-
return () => {
|
|
375
|
-
if (released) return;
|
|
376
|
-
released = true;
|
|
377
|
-
this.release();
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
release() {
|
|
381
|
-
if (this.leaseCount <= 0) return;
|
|
382
|
-
this.leaseCount -= 1;
|
|
383
|
-
if (this.leaseCount > 0 || !this.snapshot) return;
|
|
384
|
-
const { noProxy, noProxyLower, applied } = this.snapshot;
|
|
385
|
-
const currentNoProxy = process.env.NO_PROXY;
|
|
386
|
-
const currentNoProxyLower = process.env.no_proxy;
|
|
387
|
-
if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
|
|
388
|
-
if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
|
|
389
|
-
else delete process.env.NO_PROXY;
|
|
390
|
-
if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
|
|
391
|
-
else delete process.env.no_proxy;
|
|
392
|
-
}
|
|
393
|
-
this.snapshot = null;
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
const noProxyLeaseManager = new NoProxyLeaseManager();
|
|
397
|
-
/**
|
|
398
|
-
* Scoped NO_PROXY bypass for loopback CDP URLs.
|
|
399
|
-
*
|
|
400
|
-
* This wrapper only mutates env vars for loopback destinations. On restore,
|
|
401
|
-
* it avoids clobbering external NO_PROXY changes that happened while calls
|
|
402
|
-
* were in-flight.
|
|
403
|
-
*/
|
|
404
|
-
async function withNoProxyForCdpUrl(url, fn) {
|
|
405
|
-
const release = noProxyLeaseManager.acquire(url);
|
|
406
|
-
try {
|
|
407
|
-
return await fn();
|
|
408
|
-
} finally {
|
|
409
|
-
release?.();
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
//#endregion
|
|
414
|
-
//#region src/browser/cdp-timeouts.ts
|
|
415
|
-
const CDP_HTTP_REQUEST_TIMEOUT_MS = 1500;
|
|
416
|
-
const CDP_WS_HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
417
|
-
const CDP_JSON_NEW_TIMEOUT_MS = 1500;
|
|
418
|
-
const CHROME_REACHABILITY_TIMEOUT_MS = 500;
|
|
419
|
-
const CHROME_WS_READY_TIMEOUT_MS = 800;
|
|
420
|
-
const CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS = 1e4;
|
|
421
|
-
const CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS = 5e3;
|
|
422
|
-
const CHROME_LAUNCH_READY_WINDOW_MS = 15e3;
|
|
423
|
-
const CHROME_LAUNCH_READY_POLL_MS = 200;
|
|
424
|
-
const CHROME_STOP_TIMEOUT_MS = 2500;
|
|
425
|
-
const CHROME_STOP_PROBE_TIMEOUT_MS = 200;
|
|
426
|
-
const CHROME_STDERR_HINT_MAX_CHARS = 2e3;
|
|
427
|
-
const PROFILE_HTTP_REACHABILITY_TIMEOUT_MS = 300;
|
|
428
|
-
const PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS = 200;
|
|
429
|
-
const PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS = 2e3;
|
|
430
|
-
const PROFILE_ATTACH_RETRY_TIMEOUT_MS = 1200;
|
|
431
|
-
const PROFILE_POST_RESTART_WS_TIMEOUT_MS = 600;
|
|
432
|
-
function normalizeTimeoutMs(value) {
|
|
433
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
434
|
-
return Math.max(1, Math.floor(value));
|
|
435
|
-
}
|
|
436
|
-
function resolveCdpReachabilityTimeouts(params) {
|
|
437
|
-
const normalized = normalizeTimeoutMs(params.timeoutMs);
|
|
438
|
-
if (params.profileIsLoopback) {
|
|
439
|
-
const httpTimeoutMs = normalized ?? PROFILE_HTTP_REACHABILITY_TIMEOUT_MS;
|
|
440
|
-
return {
|
|
441
|
-
httpTimeoutMs,
|
|
442
|
-
wsTimeoutMs: Math.max(PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS, Math.min(PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS, httpTimeoutMs * 2))
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
if (normalized !== void 0) return {
|
|
446
|
-
httpTimeoutMs: Math.max(normalized, params.remoteHttpTimeoutMs),
|
|
447
|
-
wsTimeoutMs: Math.max(normalized * 2, params.remoteHandshakeTimeoutMs)
|
|
448
|
-
};
|
|
449
|
-
return {
|
|
450
|
-
httpTimeoutMs: params.remoteHttpTimeoutMs,
|
|
451
|
-
wsTimeoutMs: params.remoteHandshakeTimeoutMs
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
//#endregion
|
|
456
|
-
//#region src/browser/extension-relay-auth.ts
|
|
457
|
-
const RELAY_TOKEN_CONTEXT = "squidclaw-extension-relay-v1";
|
|
458
|
-
const DEFAULT_RELAY_PROBE_TIMEOUT_MS = 500;
|
|
459
|
-
const SQUIDCLAW_RELAY_BROWSER = "SquidClaw/extension-relay";
|
|
460
|
-
function resolveGatewayAuthToken() {
|
|
461
|
-
const envToken = process.env.SQUIDCLAW_GATEWAY_TOKEN?.trim() || process.env.CLAWDBOT_GATEWAY_TOKEN?.trim();
|
|
462
|
-
if (envToken) return envToken;
|
|
463
|
-
try {
|
|
464
|
-
const configToken = loadConfig().gateway?.auth?.token?.trim();
|
|
465
|
-
if (configToken) return configToken;
|
|
466
|
-
} catch {}
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
function deriveRelayAuthToken(gatewayToken, port) {
|
|
470
|
-
return createHmac("sha256", gatewayToken).update(`${RELAY_TOKEN_CONTEXT}:${port}`).digest("hex");
|
|
471
|
-
}
|
|
472
|
-
function resolveRelayAcceptedTokensForPort(port) {
|
|
473
|
-
const gatewayToken = resolveGatewayAuthToken();
|
|
474
|
-
if (!gatewayToken) throw new Error("extension relay requires gateway auth token (set gateway.auth.token or SQUIDCLAW_GATEWAY_TOKEN)");
|
|
475
|
-
const relayToken = deriveRelayAuthToken(gatewayToken, port);
|
|
476
|
-
if (relayToken === gatewayToken) return [relayToken];
|
|
477
|
-
return [relayToken, gatewayToken];
|
|
478
|
-
}
|
|
479
|
-
function resolveRelayAuthTokenForPort(port) {
|
|
480
|
-
return resolveRelayAcceptedTokensForPort(port)[0];
|
|
481
|
-
}
|
|
482
|
-
async function probeAuthenticatedSquidClawRelay(params) {
|
|
483
|
-
const ctrl = new AbortController();
|
|
484
|
-
const timer = setTimeout(() => ctrl.abort(), params.timeoutMs ?? DEFAULT_RELAY_PROBE_TIMEOUT_MS);
|
|
485
|
-
try {
|
|
486
|
-
const versionUrl = new URL("/json/version", `${params.baseUrl}/`).toString();
|
|
487
|
-
const res = await fetch(versionUrl, {
|
|
488
|
-
signal: ctrl.signal,
|
|
489
|
-
headers: { [params.relayAuthHeader]: params.relayAuthToken }
|
|
490
|
-
});
|
|
491
|
-
if (!res.ok) return false;
|
|
492
|
-
const body = await res.json();
|
|
493
|
-
return (typeof body?.Browser === "string" ? body.Browser.trim() : "") === SQUIDCLAW_RELAY_BROWSER;
|
|
494
|
-
} catch {
|
|
495
|
-
return false;
|
|
496
|
-
} finally {
|
|
497
|
-
clearTimeout(timer);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/browser/extension-relay.ts
|
|
503
|
-
const RELAY_AUTH_HEADER = "x-squidclaw-relay-token";
|
|
504
|
-
const DEFAULT_EXTENSION_RECONNECT_GRACE_MS = 2e4;
|
|
505
|
-
const DEFAULT_EXTENSION_COMMAND_RECONNECT_WAIT_MS = 3e3;
|
|
506
|
-
function headerValue(value) {
|
|
507
|
-
if (!value) return;
|
|
508
|
-
if (Array.isArray(value)) return value[0];
|
|
509
|
-
return value;
|
|
510
|
-
}
|
|
511
|
-
function getHeader(req, name) {
|
|
512
|
-
return headerValue(req.headers[name.toLowerCase()]);
|
|
513
|
-
}
|
|
514
|
-
function getRelayAuthTokenFromRequest(req, url) {
|
|
515
|
-
const headerToken = getHeader(req, RELAY_AUTH_HEADER)?.trim();
|
|
516
|
-
if (headerToken) return headerToken;
|
|
517
|
-
const queryToken = url?.searchParams.get("token")?.trim();
|
|
518
|
-
if (queryToken) return queryToken;
|
|
519
|
-
}
|
|
520
|
-
function parseUrlPort(parsed) {
|
|
521
|
-
const port = parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
|
522
|
-
if (!Number.isFinite(port) || port <= 0 || port > 65535) return null;
|
|
523
|
-
return port;
|
|
524
|
-
}
|
|
525
|
-
function parseBaseUrl(raw) {
|
|
526
|
-
const parsed = new URL(raw.trim().replace(/\/$/, ""));
|
|
527
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`extension relay cdpUrl must be http(s), got ${parsed.protocol}`);
|
|
528
|
-
const host = parsed.hostname;
|
|
529
|
-
const port = parseUrlPort(parsed);
|
|
530
|
-
if (!port) throw new Error(`extension relay cdpUrl has invalid port: ${parsed.port || "(empty)"}`);
|
|
531
|
-
return {
|
|
532
|
-
host,
|
|
533
|
-
port,
|
|
534
|
-
baseUrl: parsed.toString().replace(/\/$/, "")
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
function text(res, status, bodyText) {
|
|
538
|
-
const body = Buffer.from(bodyText);
|
|
539
|
-
res.write(`HTTP/1.1 ${status} ${status === 200 ? "OK" : "ERR"}\r\nContent-Type: text/plain; charset=utf-8\r
|
|
540
|
-
Content-Length: ${body.length}\r\nConnection: close\r
|
|
541
|
-
\r
|
|
542
|
-
`);
|
|
543
|
-
res.write(body);
|
|
544
|
-
res.end();
|
|
545
|
-
}
|
|
546
|
-
function rejectUpgrade(socket, status, bodyText) {
|
|
547
|
-
text(socket, status, bodyText);
|
|
548
|
-
try {
|
|
549
|
-
socket.destroy();
|
|
550
|
-
} catch {}
|
|
551
|
-
}
|
|
552
|
-
function envMsOrDefault(name, fallback) {
|
|
553
|
-
const raw = process.env[name];
|
|
554
|
-
if (!raw || raw.trim() === "") return fallback;
|
|
555
|
-
const parsed = Number.parseInt(raw, 10);
|
|
556
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
557
|
-
return parsed;
|
|
558
|
-
}
|
|
559
|
-
const relayRuntimeByPort = /* @__PURE__ */ new Map();
|
|
560
|
-
const relayInitByPort = /* @__PURE__ */ new Map();
|
|
561
|
-
function isAddrInUseError(err) {
|
|
562
|
-
return typeof err === "object" && err !== null && "code" in err && err.code === "EADDRINUSE";
|
|
563
|
-
}
|
|
564
|
-
function relayAuthTokenForUrl(url) {
|
|
565
|
-
try {
|
|
566
|
-
const parsed = new URL(url);
|
|
567
|
-
if (!isLoopbackHost(parsed.hostname)) return null;
|
|
568
|
-
const port = parseUrlPort(parsed);
|
|
569
|
-
if (!port) return null;
|
|
570
|
-
return relayRuntimeByPort.get(port)?.relayAuthToken ?? null;
|
|
571
|
-
} catch {
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
function getChromeExtensionRelayAuthHeaders(url) {
|
|
576
|
-
const token = relayAuthTokenForUrl(url);
|
|
577
|
-
if (!token) return {};
|
|
578
|
-
return { [RELAY_AUTH_HEADER]: token };
|
|
579
|
-
}
|
|
580
|
-
async function ensureChromeExtensionRelayServer(opts) {
|
|
581
|
-
const info = parseBaseUrl(opts.cdpUrl);
|
|
582
|
-
if (!isLoopbackHost(info.host)) throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
|
|
583
|
-
const existing = relayRuntimeByPort.get(info.port);
|
|
584
|
-
if (existing) return existing.server;
|
|
585
|
-
const inFlight = relayInitByPort.get(info.port);
|
|
586
|
-
if (inFlight) return await inFlight;
|
|
587
|
-
const extensionReconnectGraceMs = envMsOrDefault("SQUIDCLAW_EXTENSION_RELAY_RECONNECT_GRACE_MS", DEFAULT_EXTENSION_RECONNECT_GRACE_MS);
|
|
588
|
-
const extensionCommandReconnectWaitMs = envMsOrDefault("SQUIDCLAW_EXTENSION_RELAY_COMMAND_RECONNECT_WAIT_MS", DEFAULT_EXTENSION_COMMAND_RECONNECT_WAIT_MS);
|
|
589
|
-
const initPromise = (async () => {
|
|
590
|
-
const relayAuthToken = resolveRelayAuthTokenForPort(info.port);
|
|
591
|
-
const relayAuthTokens = new Set(resolveRelayAcceptedTokensForPort(info.port));
|
|
592
|
-
let extensionWs = null;
|
|
593
|
-
const cdpClients = /* @__PURE__ */ new Set();
|
|
594
|
-
const connectedTargets = /* @__PURE__ */ new Map();
|
|
595
|
-
const extensionConnected = () => extensionWs?.readyState === WebSocket.OPEN;
|
|
596
|
-
const hasConnectedTargets = () => connectedTargets.size > 0;
|
|
597
|
-
let extensionDisconnectCleanupTimer = null;
|
|
598
|
-
const extensionReconnectWaiters = /* @__PURE__ */ new Set();
|
|
599
|
-
const flushExtensionReconnectWaiters = (connected) => {
|
|
600
|
-
if (extensionReconnectWaiters.size === 0) return;
|
|
601
|
-
const waiters = Array.from(extensionReconnectWaiters);
|
|
602
|
-
extensionReconnectWaiters.clear();
|
|
603
|
-
for (const waiter of waiters) waiter(connected);
|
|
604
|
-
};
|
|
605
|
-
const clearExtensionDisconnectCleanupTimer = () => {
|
|
606
|
-
if (!extensionDisconnectCleanupTimer) return;
|
|
607
|
-
clearTimeout(extensionDisconnectCleanupTimer);
|
|
608
|
-
extensionDisconnectCleanupTimer = null;
|
|
609
|
-
};
|
|
610
|
-
const closeCdpClientsAfterExtensionDisconnect = () => {
|
|
611
|
-
connectedTargets.clear();
|
|
612
|
-
for (const client of cdpClients) try {
|
|
613
|
-
client.close(1011, "extension disconnected");
|
|
614
|
-
} catch {}
|
|
615
|
-
cdpClients.clear();
|
|
616
|
-
flushExtensionReconnectWaiters(false);
|
|
617
|
-
};
|
|
618
|
-
const scheduleExtensionDisconnectCleanup = () => {
|
|
619
|
-
clearExtensionDisconnectCleanupTimer();
|
|
620
|
-
extensionDisconnectCleanupTimer = setTimeout(() => {
|
|
621
|
-
extensionDisconnectCleanupTimer = null;
|
|
622
|
-
if (extensionConnected()) return;
|
|
623
|
-
closeCdpClientsAfterExtensionDisconnect();
|
|
624
|
-
}, extensionReconnectGraceMs);
|
|
625
|
-
};
|
|
626
|
-
const waitForExtensionReconnect = async (timeoutMs) => {
|
|
627
|
-
if (extensionConnected()) return true;
|
|
628
|
-
return await new Promise((resolve) => {
|
|
629
|
-
let settled = false;
|
|
630
|
-
const waiter = (connected) => {
|
|
631
|
-
if (settled) return;
|
|
632
|
-
settled = true;
|
|
633
|
-
clearTimeout(timer);
|
|
634
|
-
extensionReconnectWaiters.delete(waiter);
|
|
635
|
-
resolve(connected);
|
|
636
|
-
};
|
|
637
|
-
const timer = setTimeout(() => {
|
|
638
|
-
waiter(false);
|
|
639
|
-
}, timeoutMs);
|
|
640
|
-
extensionReconnectWaiters.add(waiter);
|
|
641
|
-
});
|
|
642
|
-
};
|
|
643
|
-
const pendingExtension = /* @__PURE__ */ new Map();
|
|
644
|
-
let nextExtensionId = 1;
|
|
645
|
-
const sendToExtension = async (payload) => {
|
|
646
|
-
const ws = extensionWs;
|
|
647
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) throw new Error("Chrome extension not connected");
|
|
648
|
-
ws.send(JSON.stringify(payload));
|
|
649
|
-
return await new Promise((resolve, reject) => {
|
|
650
|
-
const timer = setTimeout(() => {
|
|
651
|
-
pendingExtension.delete(payload.id);
|
|
652
|
-
reject(/* @__PURE__ */ new Error(`extension request timeout: ${payload.params.method}`));
|
|
653
|
-
}, 3e4);
|
|
654
|
-
pendingExtension.set(payload.id, {
|
|
655
|
-
resolve,
|
|
656
|
-
reject,
|
|
657
|
-
timer
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
};
|
|
661
|
-
const broadcastToCdpClients = (evt) => {
|
|
662
|
-
const msg = JSON.stringify(evt);
|
|
663
|
-
for (const ws of cdpClients) {
|
|
664
|
-
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
665
|
-
ws.send(msg);
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
const sendResponseToCdp = (ws, res) => {
|
|
669
|
-
if (ws.readyState !== WebSocket.OPEN) return;
|
|
670
|
-
ws.send(JSON.stringify(res));
|
|
671
|
-
};
|
|
672
|
-
const dropConnectedTargetSession = (sessionId) => {
|
|
673
|
-
const existing = connectedTargets.get(sessionId);
|
|
674
|
-
if (!existing) return;
|
|
675
|
-
connectedTargets.delete(sessionId);
|
|
676
|
-
return existing;
|
|
677
|
-
};
|
|
678
|
-
const dropConnectedTargetsByTargetId = (targetId) => {
|
|
679
|
-
const removed = [];
|
|
680
|
-
for (const [sessionId, target] of connectedTargets) {
|
|
681
|
-
if (target.targetId !== targetId) continue;
|
|
682
|
-
connectedTargets.delete(sessionId);
|
|
683
|
-
removed.push(target);
|
|
684
|
-
}
|
|
685
|
-
return removed;
|
|
686
|
-
};
|
|
687
|
-
const broadcastDetachedTarget = (target, targetId) => {
|
|
688
|
-
broadcastToCdpClients({
|
|
689
|
-
method: "Target.detachedFromTarget",
|
|
690
|
-
params: {
|
|
691
|
-
sessionId: target.sessionId,
|
|
692
|
-
targetId: targetId ?? target.targetId
|
|
693
|
-
},
|
|
694
|
-
sessionId: target.sessionId
|
|
695
|
-
});
|
|
696
|
-
};
|
|
697
|
-
const isMissingTargetError = (err) => {
|
|
698
|
-
const message = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
699
|
-
return message.includes("target not found") || message.includes("no target with given id") || message.includes("session not found") || message.includes("cannot find session");
|
|
700
|
-
};
|
|
701
|
-
const pruneStaleTargetsFromCommandFailure = (cmd, err) => {
|
|
702
|
-
if (!isMissingTargetError(err)) return;
|
|
703
|
-
if (cmd.sessionId) {
|
|
704
|
-
const removed = dropConnectedTargetSession(cmd.sessionId);
|
|
705
|
-
if (removed) {
|
|
706
|
-
broadcastDetachedTarget(removed);
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
const params = cmd.params ?? {};
|
|
711
|
-
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
|
|
712
|
-
if (!targetId) return;
|
|
713
|
-
const removedTargets = dropConnectedTargetsByTargetId(targetId);
|
|
714
|
-
for (const removed of removedTargets) broadcastDetachedTarget(removed, targetId);
|
|
715
|
-
};
|
|
716
|
-
const ensureTargetEventsForClient = (ws, mode) => {
|
|
717
|
-
for (const target of connectedTargets.values()) if (mode === "autoAttach") ws.send(JSON.stringify({
|
|
718
|
-
method: "Target.attachedToTarget",
|
|
719
|
-
params: {
|
|
720
|
-
sessionId: target.sessionId,
|
|
721
|
-
targetInfo: {
|
|
722
|
-
...target.targetInfo,
|
|
723
|
-
attached: true
|
|
724
|
-
},
|
|
725
|
-
waitingForDebugger: false
|
|
726
|
-
}
|
|
727
|
-
}));
|
|
728
|
-
else ws.send(JSON.stringify({
|
|
729
|
-
method: "Target.targetCreated",
|
|
730
|
-
params: { targetInfo: {
|
|
731
|
-
...target.targetInfo,
|
|
732
|
-
attached: true
|
|
733
|
-
} }
|
|
734
|
-
}));
|
|
735
|
-
};
|
|
736
|
-
const routeCdpCommand = async (cmd) => {
|
|
737
|
-
switch (cmd.method) {
|
|
738
|
-
case "Browser.getVersion": return {
|
|
739
|
-
protocolVersion: "1.3",
|
|
740
|
-
product: "Chrome/SquidClaw-Extension-Relay",
|
|
741
|
-
revision: "0",
|
|
742
|
-
userAgent: "SquidClaw-Extension-Relay",
|
|
743
|
-
jsVersion: "V8"
|
|
744
|
-
};
|
|
745
|
-
case "Browser.setDownloadBehavior": return {};
|
|
746
|
-
case "Target.setAutoAttach":
|
|
747
|
-
case "Target.setDiscoverTargets": return {};
|
|
748
|
-
case "Target.getTargets": return { targetInfos: Array.from(connectedTargets.values()).map((t) => ({
|
|
749
|
-
...t.targetInfo,
|
|
750
|
-
attached: true
|
|
751
|
-
})) };
|
|
752
|
-
case "Target.getTargetInfo": {
|
|
753
|
-
const params = cmd.params ?? {};
|
|
754
|
-
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
|
|
755
|
-
if (targetId) {
|
|
756
|
-
for (const t of connectedTargets.values()) if (t.targetId === targetId) return { targetInfo: t.targetInfo };
|
|
757
|
-
}
|
|
758
|
-
if (cmd.sessionId && connectedTargets.has(cmd.sessionId)) {
|
|
759
|
-
const t = connectedTargets.get(cmd.sessionId);
|
|
760
|
-
if (t) return { targetInfo: t.targetInfo };
|
|
761
|
-
}
|
|
762
|
-
return { targetInfo: Array.from(connectedTargets.values())[0]?.targetInfo };
|
|
763
|
-
}
|
|
764
|
-
case "Target.attachToTarget": {
|
|
765
|
-
const params = cmd.params ?? {};
|
|
766
|
-
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
|
|
767
|
-
if (!targetId) throw new Error("targetId required");
|
|
768
|
-
for (const t of connectedTargets.values()) if (t.targetId === targetId) return { sessionId: t.sessionId };
|
|
769
|
-
throw new Error("target not found");
|
|
770
|
-
}
|
|
771
|
-
default: return await sendToExtension({
|
|
772
|
-
id: nextExtensionId++,
|
|
773
|
-
method: "forwardCDPCommand",
|
|
774
|
-
params: {
|
|
775
|
-
method: cmd.method,
|
|
776
|
-
sessionId: cmd.sessionId,
|
|
777
|
-
params: cmd.params
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
};
|
|
782
|
-
const server = createServer((req, res) => {
|
|
783
|
-
const url = new URL(req.url ?? "/", info.baseUrl);
|
|
784
|
-
const path = url.pathname;
|
|
785
|
-
const origin = getHeader(req, "origin");
|
|
786
|
-
const isChromeExtensionOrigin = typeof origin === "string" && origin.startsWith("chrome-extension://");
|
|
787
|
-
if (isChromeExtensionOrigin && origin) {
|
|
788
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
789
|
-
res.setHeader("Vary", "Origin");
|
|
790
|
-
}
|
|
791
|
-
if (req.method === "OPTIONS") {
|
|
792
|
-
if (origin && !isChromeExtensionOrigin) {
|
|
793
|
-
res.writeHead(403);
|
|
794
|
-
res.end("Forbidden");
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
const requestedHeaders = (getHeader(req, "access-control-request-headers") ?? "").split(",").map((header) => header.trim().toLowerCase()).filter((header) => header.length > 0);
|
|
798
|
-
const allowedHeaders = new Set([
|
|
799
|
-
"content-type",
|
|
800
|
-
RELAY_AUTH_HEADER,
|
|
801
|
-
...requestedHeaders
|
|
802
|
-
]);
|
|
803
|
-
res.writeHead(204, {
|
|
804
|
-
"Access-Control-Allow-Origin": origin ?? "*",
|
|
805
|
-
"Access-Control-Allow-Methods": "GET, PUT, POST, OPTIONS",
|
|
806
|
-
"Access-Control-Allow-Headers": Array.from(allowedHeaders).join(", "),
|
|
807
|
-
"Access-Control-Max-Age": "86400",
|
|
808
|
-
Vary: "Origin, Access-Control-Request-Headers"
|
|
809
|
-
});
|
|
810
|
-
res.end();
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
if (path.startsWith("/json")) {
|
|
814
|
-
const token = getRelayAuthTokenFromRequest(req, url);
|
|
815
|
-
if (!token || !relayAuthTokens.has(token)) {
|
|
816
|
-
res.writeHead(401);
|
|
817
|
-
res.end("Unauthorized");
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (req.method === "HEAD" && path === "/") {
|
|
822
|
-
res.writeHead(200);
|
|
823
|
-
res.end();
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
if (path === "/") {
|
|
827
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
828
|
-
res.end("OK");
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
if (path === "/extension/status") {
|
|
832
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
833
|
-
res.end(JSON.stringify({ connected: extensionConnected() }));
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
const cdpWsUrl = `${`ws://${req.headers.host?.trim() || `${info.host}:${info.port}`}`}/cdp`;
|
|
837
|
-
if ((path === "/json/version" || path === "/json/version/") && (req.method === "GET" || req.method === "PUT")) {
|
|
838
|
-
const payload = {
|
|
839
|
-
Browser: "SquidClaw/extension-relay",
|
|
840
|
-
"Protocol-Version": "1.3"
|
|
841
|
-
};
|
|
842
|
-
if (extensionConnected() || hasConnectedTargets()) payload.webSocketDebuggerUrl = cdpWsUrl;
|
|
843
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
844
|
-
res.end(JSON.stringify(payload));
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
if (new Set([
|
|
848
|
-
"/json",
|
|
849
|
-
"/json/",
|
|
850
|
-
"/json/list",
|
|
851
|
-
"/json/list/"
|
|
852
|
-
]).has(path) && (req.method === "GET" || req.method === "PUT")) {
|
|
853
|
-
const list = Array.from(connectedTargets.values()).map((t) => ({
|
|
854
|
-
id: t.targetId,
|
|
855
|
-
type: t.targetInfo.type ?? "page",
|
|
856
|
-
title: t.targetInfo.title ?? "",
|
|
857
|
-
description: t.targetInfo.title ?? "",
|
|
858
|
-
url: t.targetInfo.url ?? "",
|
|
859
|
-
webSocketDebuggerUrl: cdpWsUrl,
|
|
860
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${cdpWsUrl.replace("ws://", "")}`
|
|
861
|
-
}));
|
|
862
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
863
|
-
res.end(JSON.stringify(list));
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
const handleTargetActionRoute = (match, cdpMethod) => {
|
|
867
|
-
if (!match || req.method !== "GET" && req.method !== "PUT") return false;
|
|
868
|
-
let targetId = "";
|
|
869
|
-
try {
|
|
870
|
-
targetId = decodeURIComponent(match[1] ?? "").trim();
|
|
871
|
-
} catch {
|
|
872
|
-
res.writeHead(400);
|
|
873
|
-
res.end("invalid targetId encoding");
|
|
874
|
-
return true;
|
|
875
|
-
}
|
|
876
|
-
if (!targetId) {
|
|
877
|
-
res.writeHead(400);
|
|
878
|
-
res.end("targetId required");
|
|
879
|
-
return true;
|
|
880
|
-
}
|
|
881
|
-
(async () => {
|
|
882
|
-
try {
|
|
883
|
-
await sendToExtension({
|
|
884
|
-
id: nextExtensionId++,
|
|
885
|
-
method: "forwardCDPCommand",
|
|
886
|
-
params: {
|
|
887
|
-
method: cdpMethod,
|
|
888
|
-
params: { targetId }
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
} catch {}
|
|
892
|
-
})();
|
|
893
|
-
res.writeHead(200);
|
|
894
|
-
res.end("OK");
|
|
895
|
-
return true;
|
|
896
|
-
};
|
|
897
|
-
if (handleTargetActionRoute(path.match(/^\/json\/activate\/(.+)$/), "Target.activateTarget")) return;
|
|
898
|
-
if (handleTargetActionRoute(path.match(/^\/json\/close\/(.+)$/), "Target.closeTarget")) return;
|
|
899
|
-
res.writeHead(404);
|
|
900
|
-
res.end("not found");
|
|
901
|
-
});
|
|
902
|
-
const wssExtension = new WebSocketServer({ noServer: true });
|
|
903
|
-
const wssCdp = new WebSocketServer({ noServer: true });
|
|
904
|
-
server.on("upgrade", (req, socket, head) => {
|
|
905
|
-
const url = new URL(req.url ?? "/", info.baseUrl);
|
|
906
|
-
const pathname = url.pathname;
|
|
907
|
-
const remote = req.socket.remoteAddress;
|
|
908
|
-
if (!isLoopbackAddress(remote)) {
|
|
909
|
-
rejectUpgrade(socket, 403, "Forbidden");
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
const origin = headerValue(req.headers.origin);
|
|
913
|
-
if (origin && !origin.startsWith("chrome-extension://")) {
|
|
914
|
-
rejectUpgrade(socket, 403, "Forbidden: invalid origin");
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
if (pathname === "/extension") {
|
|
918
|
-
const token = getRelayAuthTokenFromRequest(req, url);
|
|
919
|
-
if (!token || !relayAuthTokens.has(token)) {
|
|
920
|
-
rejectUpgrade(socket, 401, "Unauthorized");
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
if (extensionWs && extensionWs.readyState !== WebSocket.OPEN) {
|
|
924
|
-
try {
|
|
925
|
-
extensionWs.terminate();
|
|
926
|
-
} catch {}
|
|
927
|
-
extensionWs = null;
|
|
928
|
-
}
|
|
929
|
-
if (extensionConnected()) {
|
|
930
|
-
rejectUpgrade(socket, 409, "Extension already connected");
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
wssExtension.handleUpgrade(req, socket, head, (ws) => {
|
|
934
|
-
wssExtension.emit("connection", ws, req);
|
|
935
|
-
});
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
if (pathname === "/cdp") {
|
|
939
|
-
const token = getRelayAuthTokenFromRequest(req, url);
|
|
940
|
-
if (!token || !relayAuthTokens.has(token)) {
|
|
941
|
-
rejectUpgrade(socket, 401, "Unauthorized");
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
wssCdp.handleUpgrade(req, socket, head, (ws) => {
|
|
945
|
-
wssCdp.emit("connection", ws, req);
|
|
946
|
-
});
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
rejectUpgrade(socket, 404, "Not Found");
|
|
950
|
-
});
|
|
951
|
-
wssExtension.on("connection", (ws) => {
|
|
952
|
-
extensionWs = ws;
|
|
953
|
-
clearExtensionDisconnectCleanupTimer();
|
|
954
|
-
flushExtensionReconnectWaiters(true);
|
|
955
|
-
const ping = setInterval(() => {
|
|
956
|
-
if (ws.readyState !== WebSocket.OPEN) return;
|
|
957
|
-
ws.send(JSON.stringify({ method: "ping" }));
|
|
958
|
-
}, 5e3);
|
|
959
|
-
ws.on("message", (data) => {
|
|
960
|
-
if (extensionWs !== ws) return;
|
|
961
|
-
let parsed = null;
|
|
962
|
-
try {
|
|
963
|
-
parsed = JSON.parse(rawDataToString(data));
|
|
964
|
-
} catch {
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
if (parsed && typeof parsed === "object" && "id" in parsed && typeof parsed.id === "number") {
|
|
968
|
-
const pending = pendingExtension.get(parsed.id);
|
|
969
|
-
if (!pending) return;
|
|
970
|
-
pendingExtension.delete(parsed.id);
|
|
971
|
-
clearTimeout(pending.timer);
|
|
972
|
-
if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) pending.reject(new Error(parsed.error));
|
|
973
|
-
else pending.resolve(parsed.result);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (parsed && typeof parsed === "object" && "method" in parsed) {
|
|
977
|
-
if (parsed.method === "pong") return;
|
|
978
|
-
if (parsed.method !== "forwardCDPEvent") return;
|
|
979
|
-
const evt = parsed;
|
|
980
|
-
const method = evt.params?.method;
|
|
981
|
-
const params = evt.params?.params;
|
|
982
|
-
const sessionId = evt.params?.sessionId;
|
|
983
|
-
if (!method || typeof method !== "string") return;
|
|
984
|
-
if (method === "Target.attachedToTarget") {
|
|
985
|
-
const attached = params ?? {};
|
|
986
|
-
if ((attached?.targetInfo?.type ?? "page") !== "page") return;
|
|
987
|
-
if (attached?.sessionId && attached?.targetInfo?.targetId) {
|
|
988
|
-
const prev = connectedTargets.get(attached.sessionId);
|
|
989
|
-
const nextTargetId = attached.targetInfo.targetId;
|
|
990
|
-
const prevTargetId = prev?.targetId;
|
|
991
|
-
const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId);
|
|
992
|
-
connectedTargets.set(attached.sessionId, {
|
|
993
|
-
sessionId: attached.sessionId,
|
|
994
|
-
targetId: nextTargetId,
|
|
995
|
-
targetInfo: attached.targetInfo
|
|
996
|
-
});
|
|
997
|
-
if (changedTarget && prevTargetId) broadcastToCdpClients({
|
|
998
|
-
method: "Target.detachedFromTarget",
|
|
999
|
-
params: {
|
|
1000
|
-
sessionId: attached.sessionId,
|
|
1001
|
-
targetId: prevTargetId
|
|
1002
|
-
},
|
|
1003
|
-
sessionId: attached.sessionId
|
|
1004
|
-
});
|
|
1005
|
-
if (!prev || changedTarget) broadcastToCdpClients({
|
|
1006
|
-
method,
|
|
1007
|
-
params,
|
|
1008
|
-
sessionId
|
|
1009
|
-
});
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
if (method === "Target.detachedFromTarget") {
|
|
1014
|
-
const detached = params ?? {};
|
|
1015
|
-
if (detached?.sessionId) dropConnectedTargetSession(detached.sessionId);
|
|
1016
|
-
else if (detached?.targetId) dropConnectedTargetsByTargetId(detached.targetId);
|
|
1017
|
-
broadcastToCdpClients({
|
|
1018
|
-
method,
|
|
1019
|
-
params,
|
|
1020
|
-
sessionId
|
|
1021
|
-
});
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (method === "Target.targetDestroyed" || method === "Target.targetCrashed") {
|
|
1025
|
-
const targetEvent = params ?? {};
|
|
1026
|
-
if (targetEvent.targetId) dropConnectedTargetsByTargetId(targetEvent.targetId);
|
|
1027
|
-
broadcastToCdpClients({
|
|
1028
|
-
method,
|
|
1029
|
-
params,
|
|
1030
|
-
sessionId
|
|
1031
|
-
});
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
if (method === "Target.targetInfoChanged") {
|
|
1035
|
-
const targetInfo = (params ?? {})?.targetInfo;
|
|
1036
|
-
const targetId = targetInfo?.targetId;
|
|
1037
|
-
if (targetId && (targetInfo?.type ?? "page") === "page") for (const [sid, target] of connectedTargets) {
|
|
1038
|
-
if (target.targetId !== targetId) continue;
|
|
1039
|
-
connectedTargets.set(sid, {
|
|
1040
|
-
...target,
|
|
1041
|
-
targetInfo: {
|
|
1042
|
-
...target.targetInfo,
|
|
1043
|
-
...targetInfo
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
broadcastToCdpClients({
|
|
1049
|
-
method,
|
|
1050
|
-
params,
|
|
1051
|
-
sessionId
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
});
|
|
1055
|
-
ws.on("close", () => {
|
|
1056
|
-
clearInterval(ping);
|
|
1057
|
-
if (extensionWs !== ws) return;
|
|
1058
|
-
extensionWs = null;
|
|
1059
|
-
for (const [, pending] of pendingExtension) {
|
|
1060
|
-
clearTimeout(pending.timer);
|
|
1061
|
-
pending.reject(/* @__PURE__ */ new Error("extension disconnected"));
|
|
1062
|
-
}
|
|
1063
|
-
pendingExtension.clear();
|
|
1064
|
-
scheduleExtensionDisconnectCleanup();
|
|
1065
|
-
});
|
|
1066
|
-
});
|
|
1067
|
-
wssCdp.on("connection", (ws) => {
|
|
1068
|
-
cdpClients.add(ws);
|
|
1069
|
-
ws.on("message", async (data) => {
|
|
1070
|
-
let cmd = null;
|
|
1071
|
-
try {
|
|
1072
|
-
cmd = JSON.parse(rawDataToString(data));
|
|
1073
|
-
} catch {
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
if (!cmd || typeof cmd !== "object") return;
|
|
1077
|
-
if (typeof cmd.id !== "number" || typeof cmd.method !== "string") return;
|
|
1078
|
-
if (!extensionConnected()) {
|
|
1079
|
-
if (!await waitForExtensionReconnect(extensionCommandReconnectWaitMs) || !extensionConnected()) {
|
|
1080
|
-
sendResponseToCdp(ws, {
|
|
1081
|
-
id: cmd.id,
|
|
1082
|
-
sessionId: cmd.sessionId,
|
|
1083
|
-
error: { message: "Extension not connected" }
|
|
1084
|
-
});
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
try {
|
|
1089
|
-
const result = await routeCdpCommand(cmd);
|
|
1090
|
-
if (cmd.method === "Target.setAutoAttach" && !cmd.sessionId) ensureTargetEventsForClient(ws, "autoAttach");
|
|
1091
|
-
if (cmd.method === "Target.setDiscoverTargets") {
|
|
1092
|
-
if ((cmd.params ?? {}).discover === true) ensureTargetEventsForClient(ws, "discover");
|
|
1093
|
-
}
|
|
1094
|
-
if (cmd.method === "Target.attachToTarget") {
|
|
1095
|
-
const params = cmd.params ?? {};
|
|
1096
|
-
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
|
|
1097
|
-
if (targetId) {
|
|
1098
|
-
const target = Array.from(connectedTargets.values()).find((t) => t.targetId === targetId);
|
|
1099
|
-
if (target) ws.send(JSON.stringify({
|
|
1100
|
-
method: "Target.attachedToTarget",
|
|
1101
|
-
params: {
|
|
1102
|
-
sessionId: target.sessionId,
|
|
1103
|
-
targetInfo: {
|
|
1104
|
-
...target.targetInfo,
|
|
1105
|
-
attached: true
|
|
1106
|
-
},
|
|
1107
|
-
waitingForDebugger: false
|
|
1108
|
-
}
|
|
1109
|
-
}));
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
sendResponseToCdp(ws, {
|
|
1113
|
-
id: cmd.id,
|
|
1114
|
-
sessionId: cmd.sessionId,
|
|
1115
|
-
result
|
|
1116
|
-
});
|
|
1117
|
-
} catch (err) {
|
|
1118
|
-
pruneStaleTargetsFromCommandFailure(cmd, err);
|
|
1119
|
-
sendResponseToCdp(ws, {
|
|
1120
|
-
id: cmd.id,
|
|
1121
|
-
sessionId: cmd.sessionId,
|
|
1122
|
-
error: { message: err instanceof Error ? err.message : String(err) }
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
ws.on("close", () => {
|
|
1127
|
-
cdpClients.delete(ws);
|
|
1128
|
-
});
|
|
1129
|
-
});
|
|
1130
|
-
try {
|
|
1131
|
-
await new Promise((resolve, reject) => {
|
|
1132
|
-
server.listen(info.port, info.host, () => resolve());
|
|
1133
|
-
server.once("error", reject);
|
|
1134
|
-
});
|
|
1135
|
-
} catch (err) {
|
|
1136
|
-
if (isAddrInUseError(err) && await probeAuthenticatedSquidClawRelay({
|
|
1137
|
-
baseUrl: info.baseUrl,
|
|
1138
|
-
relayAuthHeader: RELAY_AUTH_HEADER,
|
|
1139
|
-
relayAuthToken
|
|
1140
|
-
})) {
|
|
1141
|
-
const existingRelay = {
|
|
1142
|
-
host: info.host,
|
|
1143
|
-
port: info.port,
|
|
1144
|
-
baseUrl: info.baseUrl,
|
|
1145
|
-
cdpWsUrl: `ws://${info.host}:${info.port}/cdp`,
|
|
1146
|
-
extensionConnected: () => false,
|
|
1147
|
-
stop: async () => {
|
|
1148
|
-
relayRuntimeByPort.delete(info.port);
|
|
1149
|
-
}
|
|
1150
|
-
};
|
|
1151
|
-
relayRuntimeByPort.set(info.port, {
|
|
1152
|
-
server: existingRelay,
|
|
1153
|
-
relayAuthToken
|
|
1154
|
-
});
|
|
1155
|
-
return existingRelay;
|
|
1156
|
-
}
|
|
1157
|
-
throw err;
|
|
1158
|
-
}
|
|
1159
|
-
const port = server.address()?.port ?? info.port;
|
|
1160
|
-
const host = info.host;
|
|
1161
|
-
const relay = {
|
|
1162
|
-
host,
|
|
1163
|
-
port,
|
|
1164
|
-
baseUrl: `${new URL(info.baseUrl).protocol}//${host}:${port}`,
|
|
1165
|
-
cdpWsUrl: `ws://${host}:${port}/cdp`,
|
|
1166
|
-
extensionConnected,
|
|
1167
|
-
stop: async () => {
|
|
1168
|
-
relayRuntimeByPort.delete(port);
|
|
1169
|
-
clearExtensionDisconnectCleanupTimer();
|
|
1170
|
-
flushExtensionReconnectWaiters(false);
|
|
1171
|
-
for (const [, pending] of pendingExtension) {
|
|
1172
|
-
clearTimeout(pending.timer);
|
|
1173
|
-
pending.reject(/* @__PURE__ */ new Error("server stopping"));
|
|
1174
|
-
}
|
|
1175
|
-
pendingExtension.clear();
|
|
1176
|
-
try {
|
|
1177
|
-
extensionWs?.close(1001, "server stopping");
|
|
1178
|
-
} catch {}
|
|
1179
|
-
for (const ws of cdpClients) try {
|
|
1180
|
-
ws.close(1001, "server stopping");
|
|
1181
|
-
} catch {}
|
|
1182
|
-
await new Promise((resolve) => {
|
|
1183
|
-
server.close(() => resolve());
|
|
1184
|
-
});
|
|
1185
|
-
wssExtension.close();
|
|
1186
|
-
wssCdp.close();
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
relayRuntimeByPort.set(port, {
|
|
1190
|
-
server: relay,
|
|
1191
|
-
relayAuthToken
|
|
1192
|
-
});
|
|
1193
|
-
return relay;
|
|
1194
|
-
})();
|
|
1195
|
-
relayInitByPort.set(info.port, initPromise);
|
|
1196
|
-
try {
|
|
1197
|
-
return await initPromise;
|
|
1198
|
-
} finally {
|
|
1199
|
-
relayInitByPort.delete(info.port);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
async function stopChromeExtensionRelayServer(opts) {
|
|
1203
|
-
const info = parseBaseUrl(opts.cdpUrl);
|
|
1204
|
-
const existing = relayRuntimeByPort.get(info.port);
|
|
1205
|
-
if (!existing) return false;
|
|
1206
|
-
await existing.server.stop();
|
|
1207
|
-
return true;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
//#endregion
|
|
1211
|
-
//#region src/browser/cdp.helpers.ts
|
|
1212
|
-
function getHeadersWithAuth(url, headers = {}) {
|
|
1213
|
-
const mergedHeaders = {
|
|
1214
|
-
...getChromeExtensionRelayAuthHeaders(url),
|
|
1215
|
-
...headers
|
|
1216
|
-
};
|
|
1217
|
-
try {
|
|
1218
|
-
const parsed = new URL(url);
|
|
1219
|
-
if (Object.keys(mergedHeaders).some((key) => key.toLowerCase() === "authorization")) return mergedHeaders;
|
|
1220
|
-
if (parsed.username || parsed.password) {
|
|
1221
|
-
const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
|
|
1222
|
-
return {
|
|
1223
|
-
...mergedHeaders,
|
|
1224
|
-
Authorization: `Basic ${auth}`
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
} catch {}
|
|
1228
|
-
return mergedHeaders;
|
|
1229
|
-
}
|
|
1230
|
-
function appendCdpPath(cdpUrl, path) {
|
|
1231
|
-
const url = new URL(cdpUrl);
|
|
1232
|
-
url.pathname = `${url.pathname.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1233
|
-
return url.toString();
|
|
1234
|
-
}
|
|
1235
|
-
function createCdpSender(ws) {
|
|
1236
|
-
let nextId = 1;
|
|
1237
|
-
const pending = /* @__PURE__ */ new Map();
|
|
1238
|
-
const send = (method, params, sessionId) => {
|
|
1239
|
-
const id = nextId++;
|
|
1240
|
-
const msg = {
|
|
1241
|
-
id,
|
|
1242
|
-
method,
|
|
1243
|
-
params,
|
|
1244
|
-
sessionId
|
|
1245
|
-
};
|
|
1246
|
-
ws.send(JSON.stringify(msg));
|
|
1247
|
-
return new Promise((resolve, reject) => {
|
|
1248
|
-
pending.set(id, {
|
|
1249
|
-
resolve,
|
|
1250
|
-
reject
|
|
1251
|
-
});
|
|
1252
|
-
});
|
|
1253
|
-
};
|
|
1254
|
-
const closeWithError = (err) => {
|
|
1255
|
-
for (const [, p] of pending) p.reject(err);
|
|
1256
|
-
pending.clear();
|
|
1257
|
-
try {
|
|
1258
|
-
ws.close();
|
|
1259
|
-
} catch {}
|
|
1260
|
-
};
|
|
1261
|
-
ws.on("error", (err) => {
|
|
1262
|
-
closeWithError(err instanceof Error ? err : new Error(String(err)));
|
|
1263
|
-
});
|
|
1264
|
-
ws.on("message", (data) => {
|
|
1265
|
-
try {
|
|
1266
|
-
const parsed = JSON.parse(rawDataToString(data));
|
|
1267
|
-
if (typeof parsed.id !== "number") return;
|
|
1268
|
-
const p = pending.get(parsed.id);
|
|
1269
|
-
if (!p) return;
|
|
1270
|
-
pending.delete(parsed.id);
|
|
1271
|
-
if (parsed.error?.message) {
|
|
1272
|
-
p.reject(new Error(parsed.error.message));
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
p.resolve(parsed.result);
|
|
1276
|
-
} catch {}
|
|
1277
|
-
});
|
|
1278
|
-
ws.on("close", () => {
|
|
1279
|
-
closeWithError(/* @__PURE__ */ new Error("CDP socket closed"));
|
|
1280
|
-
});
|
|
1281
|
-
return {
|
|
1282
|
-
send,
|
|
1283
|
-
closeWithError
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
async function fetchJson(url, timeoutMs = CDP_HTTP_REQUEST_TIMEOUT_MS, init) {
|
|
1287
|
-
return await (await fetchCdpChecked(url, timeoutMs, init)).json();
|
|
1288
|
-
}
|
|
1289
|
-
async function fetchCdpChecked(url, timeoutMs = CDP_HTTP_REQUEST_TIMEOUT_MS, init) {
|
|
1290
|
-
const ctrl = new AbortController();
|
|
1291
|
-
const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
|
|
1292
|
-
try {
|
|
1293
|
-
const headers = getHeadersWithAuth(url, init?.headers || {});
|
|
1294
|
-
const res = await withNoProxyForCdpUrl(url, () => fetch(url, {
|
|
1295
|
-
...init,
|
|
1296
|
-
headers,
|
|
1297
|
-
signal: ctrl.signal
|
|
1298
|
-
}));
|
|
1299
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1300
|
-
return res;
|
|
1301
|
-
} finally {
|
|
1302
|
-
clearTimeout(t);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
async function fetchOk(url, timeoutMs = CDP_HTTP_REQUEST_TIMEOUT_MS, init) {
|
|
1306
|
-
await fetchCdpChecked(url, timeoutMs, init);
|
|
1307
|
-
}
|
|
1308
|
-
function openCdpWebSocket(wsUrl, opts) {
|
|
1309
|
-
const headers = getHeadersWithAuth(wsUrl, opts?.headers ?? {});
|
|
1310
|
-
const handshakeTimeoutMs = typeof opts?.handshakeTimeoutMs === "number" && Number.isFinite(opts.handshakeTimeoutMs) ? Math.max(1, Math.floor(opts.handshakeTimeoutMs)) : CDP_WS_HANDSHAKE_TIMEOUT_MS;
|
|
1311
|
-
const agent = getDirectAgentForCdp(wsUrl);
|
|
1312
|
-
return new WebSocket(wsUrl, {
|
|
1313
|
-
handshakeTimeout: handshakeTimeoutMs,
|
|
1314
|
-
...Object.keys(headers).length ? { headers } : {},
|
|
1315
|
-
...agent ? { agent } : {}
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
async function withCdpSocket(wsUrl, fn, opts) {
|
|
1319
|
-
const ws = openCdpWebSocket(wsUrl, opts);
|
|
1320
|
-
const { send, closeWithError } = createCdpSender(ws);
|
|
1321
|
-
const openPromise = new Promise((resolve, reject) => {
|
|
1322
|
-
ws.once("open", () => resolve());
|
|
1323
|
-
ws.once("error", (err) => reject(err));
|
|
1324
|
-
ws.once("close", () => reject(/* @__PURE__ */ new Error("CDP socket closed")));
|
|
1325
|
-
});
|
|
1326
|
-
try {
|
|
1327
|
-
await openPromise;
|
|
1328
|
-
} catch (err) {
|
|
1329
|
-
closeWithError(err instanceof Error ? err : new Error(String(err)));
|
|
1330
|
-
throw err;
|
|
1331
|
-
}
|
|
1332
|
-
try {
|
|
1333
|
-
return await fn(send);
|
|
1334
|
-
} catch (err) {
|
|
1335
|
-
closeWithError(err instanceof Error ? err : new Error(String(err)));
|
|
1336
|
-
throw err;
|
|
1337
|
-
} finally {
|
|
1338
|
-
try {
|
|
1339
|
-
ws.close();
|
|
1340
|
-
} catch {}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
//#endregion
|
|
1345
|
-
//#region src/browser/navigation-guard.ts
|
|
1346
|
-
const NETWORK_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
|
|
1347
|
-
const SAFE_NON_NETWORK_URLS = new Set(["about:blank"]);
|
|
1348
|
-
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
1349
|
-
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
1350
|
-
}
|
|
1351
|
-
var InvalidBrowserNavigationUrlError = class extends Error {
|
|
1352
|
-
constructor(message) {
|
|
1353
|
-
super(message);
|
|
1354
|
-
this.name = "InvalidBrowserNavigationUrlError";
|
|
1355
|
-
}
|
|
1356
|
-
};
|
|
1357
|
-
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1358
|
-
return ssrfPolicy ? { ssrfPolicy } : {};
|
|
1359
|
-
}
|
|
1360
|
-
async function assertBrowserNavigationAllowed(opts) {
|
|
1361
|
-
const rawUrl = String(opts.url ?? "").trim();
|
|
1362
|
-
if (!rawUrl) throw new InvalidBrowserNavigationUrlError("url is required");
|
|
1363
|
-
let parsed;
|
|
1364
|
-
try {
|
|
1365
|
-
parsed = new URL(rawUrl);
|
|
1366
|
-
} catch {
|
|
1367
|
-
throw new InvalidBrowserNavigationUrlError(`Invalid URL: ${rawUrl}`);
|
|
1368
|
-
}
|
|
1369
|
-
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
1370
|
-
if (isAllowedNonNetworkNavigationUrl(parsed)) return;
|
|
1371
|
-
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
1372
|
-
}
|
|
1373
|
-
if (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) throw new InvalidBrowserNavigationUrlError("Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set");
|
|
1374
|
-
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
|
1375
|
-
lookupFn: opts.lookupFn,
|
|
1376
|
-
policy: opts.ssrfPolicy
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
/**
|
|
1380
|
-
* Best-effort post-navigation guard for final page URLs.
|
|
1381
|
-
* Only validates network URLs (http/https) and about:blank to avoid false
|
|
1382
|
-
* positives on browser-internal error pages (e.g. chrome-error://).
|
|
1383
|
-
*/
|
|
1384
|
-
async function assertBrowserNavigationResultAllowed(opts) {
|
|
1385
|
-
const rawUrl = String(opts.url ?? "").trim();
|
|
1386
|
-
if (!rawUrl) return;
|
|
1387
|
-
let parsed;
|
|
1388
|
-
try {
|
|
1389
|
-
parsed = new URL(rawUrl);
|
|
1390
|
-
} catch {
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || isAllowedNonNetworkNavigationUrl(parsed)) await assertBrowserNavigationAllowed(opts);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
//#endregion
|
|
1397
|
-
//#region src/browser/cdp.ts
|
|
1398
|
-
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
1399
|
-
const ws = new URL(wsUrl);
|
|
1400
|
-
const cdp = new URL(cdpUrl);
|
|
1401
|
-
if (isLoopbackHost(ws.hostname) && !isLoopbackHost(cdp.hostname)) {
|
|
1402
|
-
ws.hostname = cdp.hostname;
|
|
1403
|
-
const cdpPort = cdp.port || (cdp.protocol === "https:" ? "443" : "80");
|
|
1404
|
-
if (cdpPort) ws.port = cdpPort;
|
|
1405
|
-
ws.protocol = cdp.protocol === "https:" ? "wss:" : "ws:";
|
|
1406
|
-
}
|
|
1407
|
-
if (cdp.protocol === "https:" && ws.protocol === "ws:") ws.protocol = "wss:";
|
|
1408
|
-
if (!ws.username && !ws.password && (cdp.username || cdp.password)) {
|
|
1409
|
-
ws.username = cdp.username;
|
|
1410
|
-
ws.password = cdp.password;
|
|
1411
|
-
}
|
|
1412
|
-
for (const [key, value] of cdp.searchParams.entries()) if (!ws.searchParams.has(key)) ws.searchParams.append(key, value);
|
|
1413
|
-
return ws.toString();
|
|
1414
|
-
}
|
|
1415
|
-
async function captureScreenshot(opts) {
|
|
1416
|
-
return await withCdpSocket(opts.wsUrl, async (send) => {
|
|
1417
|
-
await send("Page.enable");
|
|
1418
|
-
let clip;
|
|
1419
|
-
if (opts.fullPage) {
|
|
1420
|
-
const metrics = await send("Page.getLayoutMetrics");
|
|
1421
|
-
const size = metrics?.cssContentSize ?? metrics?.contentSize;
|
|
1422
|
-
const width = Number(size?.width ?? 0);
|
|
1423
|
-
const height = Number(size?.height ?? 0);
|
|
1424
|
-
if (width > 0 && height > 0) clip = {
|
|
1425
|
-
x: 0,
|
|
1426
|
-
y: 0,
|
|
1427
|
-
width,
|
|
1428
|
-
height,
|
|
1429
|
-
scale: 1
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
const format = opts.format ?? "png";
|
|
1433
|
-
const quality = format === "jpeg" ? Math.max(0, Math.min(100, Math.round(opts.quality ?? 85))) : void 0;
|
|
1434
|
-
const base64 = (await send("Page.captureScreenshot", {
|
|
1435
|
-
format,
|
|
1436
|
-
...quality !== void 0 ? { quality } : {},
|
|
1437
|
-
fromSurface: true,
|
|
1438
|
-
captureBeyondViewport: true,
|
|
1439
|
-
...clip ? { clip } : {}
|
|
1440
|
-
}))?.data;
|
|
1441
|
-
if (!base64) throw new Error("Screenshot failed: missing data");
|
|
1442
|
-
return Buffer.from(base64, "base64");
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
async function createTargetViaCdp(opts) {
|
|
1446
|
-
await assertBrowserNavigationAllowed({
|
|
1447
|
-
url: opts.url,
|
|
1448
|
-
...withBrowserNavigationPolicy(opts.ssrfPolicy)
|
|
1449
|
-
});
|
|
1450
|
-
const version = await fetchJson(appendCdpPath(opts.cdpUrl, "/json/version"), 1500);
|
|
1451
|
-
const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim();
|
|
1452
|
-
const wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : "";
|
|
1453
|
-
if (!wsUrl) throw new Error("CDP /json/version missing webSocketDebuggerUrl");
|
|
1454
|
-
return await withCdpSocket(wsUrl, async (send) => {
|
|
1455
|
-
const created = await send("Target.createTarget", { url: opts.url });
|
|
1456
|
-
const targetId = String(created?.targetId ?? "").trim();
|
|
1457
|
-
if (!targetId) throw new Error("CDP Target.createTarget returned no targetId");
|
|
1458
|
-
return { targetId };
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
function axValue(v) {
|
|
1462
|
-
if (!v || typeof v !== "object") return "";
|
|
1463
|
-
const value = v.value;
|
|
1464
|
-
if (typeof value === "string") return value;
|
|
1465
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1466
|
-
return "";
|
|
1467
|
-
}
|
|
1468
|
-
function formatAriaSnapshot(nodes, limit) {
|
|
1469
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1470
|
-
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
1471
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
1472
|
-
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
1473
|
-
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
1474
|
-
if (!root?.nodeId) return [];
|
|
1475
|
-
const out = [];
|
|
1476
|
-
const stack = [{
|
|
1477
|
-
id: root.nodeId,
|
|
1478
|
-
depth: 0
|
|
1479
|
-
}];
|
|
1480
|
-
while (stack.length && out.length < limit) {
|
|
1481
|
-
const popped = stack.pop();
|
|
1482
|
-
if (!popped) break;
|
|
1483
|
-
const { id, depth } = popped;
|
|
1484
|
-
const n = byId.get(id);
|
|
1485
|
-
if (!n) continue;
|
|
1486
|
-
const role = axValue(n.role);
|
|
1487
|
-
const name = axValue(n.name);
|
|
1488
|
-
const value = axValue(n.value);
|
|
1489
|
-
const description = axValue(n.description);
|
|
1490
|
-
const ref = `ax${out.length + 1}`;
|
|
1491
|
-
out.push({
|
|
1492
|
-
ref,
|
|
1493
|
-
role: role || "unknown",
|
|
1494
|
-
name: name || "",
|
|
1495
|
-
...value ? { value } : {},
|
|
1496
|
-
...description ? { description } : {},
|
|
1497
|
-
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
1498
|
-
depth
|
|
1499
|
-
});
|
|
1500
|
-
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
1501
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
1502
|
-
const child = children[i];
|
|
1503
|
-
if (child) stack.push({
|
|
1504
|
-
id: child,
|
|
1505
|
-
depth: depth + 1
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
return out;
|
|
1510
|
-
}
|
|
1511
|
-
async function snapshotAria(opts) {
|
|
1512
|
-
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
1513
|
-
return await withCdpSocket(opts.wsUrl, async (send) => {
|
|
1514
|
-
await send("Accessibility.enable").catch(() => {});
|
|
1515
|
-
const res = await send("Accessibility.getFullAXTree");
|
|
1516
|
-
return { nodes: formatAriaSnapshot(Array.isArray(res?.nodes) ? res.nodes : [], limit) };
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
//#endregion
|
|
1521
|
-
//#region src/browser/chrome.executables.ts
|
|
1522
|
-
const CHROMIUM_BUNDLE_IDS = new Set([
|
|
1523
|
-
"com.google.Chrome",
|
|
1524
|
-
"com.google.Chrome.beta",
|
|
1525
|
-
"com.google.Chrome.canary",
|
|
1526
|
-
"com.google.Chrome.dev",
|
|
1527
|
-
"com.brave.Browser",
|
|
1528
|
-
"com.brave.Browser.beta",
|
|
1529
|
-
"com.brave.Browser.nightly",
|
|
1530
|
-
"com.microsoft.Edge",
|
|
1531
|
-
"com.microsoft.EdgeBeta",
|
|
1532
|
-
"com.microsoft.EdgeDev",
|
|
1533
|
-
"com.microsoft.EdgeCanary",
|
|
1534
|
-
"org.chromium.Chromium",
|
|
1535
|
-
"com.vivaldi.Vivaldi",
|
|
1536
|
-
"com.operasoftware.Opera",
|
|
1537
|
-
"com.operasoftware.OperaGX",
|
|
1538
|
-
"com.yandex.desktop.yandex-browser",
|
|
1539
|
-
"company.thebrowser.Browser"
|
|
1540
|
-
]);
|
|
1541
|
-
const CHROMIUM_DESKTOP_IDS = new Set([
|
|
1542
|
-
"google-chrome.desktop",
|
|
1543
|
-
"google-chrome-beta.desktop",
|
|
1544
|
-
"google-chrome-unstable.desktop",
|
|
1545
|
-
"brave-browser.desktop",
|
|
1546
|
-
"microsoft-edge.desktop",
|
|
1547
|
-
"microsoft-edge-beta.desktop",
|
|
1548
|
-
"microsoft-edge-dev.desktop",
|
|
1549
|
-
"microsoft-edge-canary.desktop",
|
|
1550
|
-
"chromium.desktop",
|
|
1551
|
-
"chromium-browser.desktop",
|
|
1552
|
-
"vivaldi.desktop",
|
|
1553
|
-
"vivaldi-stable.desktop",
|
|
1554
|
-
"opera.desktop",
|
|
1555
|
-
"opera-gx.desktop",
|
|
1556
|
-
"yandex-browser.desktop",
|
|
1557
|
-
"org.chromium.Chromium.desktop"
|
|
1558
|
-
]);
|
|
1559
|
-
const CHROMIUM_EXE_NAMES = new Set([
|
|
1560
|
-
"chrome.exe",
|
|
1561
|
-
"msedge.exe",
|
|
1562
|
-
"brave.exe",
|
|
1563
|
-
"brave-browser.exe",
|
|
1564
|
-
"chromium.exe",
|
|
1565
|
-
"vivaldi.exe",
|
|
1566
|
-
"opera.exe",
|
|
1567
|
-
"launcher.exe",
|
|
1568
|
-
"yandex.exe",
|
|
1569
|
-
"yandexbrowser.exe",
|
|
1570
|
-
"google chrome",
|
|
1571
|
-
"google chrome canary",
|
|
1572
|
-
"brave browser",
|
|
1573
|
-
"microsoft edge",
|
|
1574
|
-
"chromium",
|
|
1575
|
-
"chrome",
|
|
1576
|
-
"brave",
|
|
1577
|
-
"msedge",
|
|
1578
|
-
"brave-browser",
|
|
1579
|
-
"google-chrome",
|
|
1580
|
-
"google-chrome-stable",
|
|
1581
|
-
"google-chrome-beta",
|
|
1582
|
-
"google-chrome-unstable",
|
|
1583
|
-
"microsoft-edge",
|
|
1584
|
-
"microsoft-edge-beta",
|
|
1585
|
-
"microsoft-edge-dev",
|
|
1586
|
-
"microsoft-edge-canary",
|
|
1587
|
-
"chromium-browser",
|
|
1588
|
-
"vivaldi",
|
|
1589
|
-
"vivaldi-stable",
|
|
1590
|
-
"opera",
|
|
1591
|
-
"opera-stable",
|
|
1592
|
-
"opera-gx",
|
|
1593
|
-
"yandex-browser"
|
|
1594
|
-
]);
|
|
1595
|
-
function exists$1(filePath) {
|
|
1596
|
-
try {
|
|
1597
|
-
return fs.existsSync(filePath);
|
|
1598
|
-
} catch {
|
|
1599
|
-
return false;
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
function execText(command, args, timeoutMs = 1200, maxBuffer = 1024 * 1024) {
|
|
1603
|
-
try {
|
|
1604
|
-
const output = execFileSync(command, args, {
|
|
1605
|
-
timeout: timeoutMs,
|
|
1606
|
-
encoding: "utf8",
|
|
1607
|
-
maxBuffer
|
|
1608
|
-
});
|
|
1609
|
-
return String(output ?? "").trim() || null;
|
|
1610
|
-
} catch {
|
|
1611
|
-
return null;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
function inferKindFromIdentifier(identifier) {
|
|
1615
|
-
const id = identifier.toLowerCase();
|
|
1616
|
-
if (id.includes("brave")) return "brave";
|
|
1617
|
-
if (id.includes("edge")) return "edge";
|
|
1618
|
-
if (id.includes("chromium")) return "chromium";
|
|
1619
|
-
if (id.includes("canary")) return "canary";
|
|
1620
|
-
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) return "chromium";
|
|
1621
|
-
return "chrome";
|
|
1622
|
-
}
|
|
1623
|
-
function inferKindFromExecutableName(name) {
|
|
1624
|
-
const lower = name.toLowerCase();
|
|
1625
|
-
if (lower.includes("brave")) return "brave";
|
|
1626
|
-
if (lower.includes("edge") || lower.includes("msedge")) return "edge";
|
|
1627
|
-
if (lower.includes("chromium")) return "chromium";
|
|
1628
|
-
if (lower.includes("canary") || lower.includes("sxs")) return "canary";
|
|
1629
|
-
if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) return "chromium";
|
|
1630
|
-
return "chrome";
|
|
1631
|
-
}
|
|
1632
|
-
function detectDefaultChromiumExecutable(platform) {
|
|
1633
|
-
if (platform === "darwin") return detectDefaultChromiumExecutableMac();
|
|
1634
|
-
if (platform === "linux") return detectDefaultChromiumExecutableLinux();
|
|
1635
|
-
if (platform === "win32") return detectDefaultChromiumExecutableWindows();
|
|
1636
|
-
return null;
|
|
1637
|
-
}
|
|
1638
|
-
function detectDefaultChromiumExecutableMac() {
|
|
1639
|
-
const bundleId = detectDefaultBrowserBundleIdMac();
|
|
1640
|
-
if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
|
|
1641
|
-
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
|
|
1642
|
-
if (!appPathRaw) return null;
|
|
1643
|
-
const appPath = appPathRaw.trim().replace(/\/$/, "");
|
|
1644
|
-
const exeName = execText("/usr/bin/defaults", [
|
|
1645
|
-
"read",
|
|
1646
|
-
path.join(appPath, "Contents", "Info"),
|
|
1647
|
-
"CFBundleExecutable"
|
|
1648
|
-
]);
|
|
1649
|
-
if (!exeName) return null;
|
|
1650
|
-
const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
|
|
1651
|
-
if (!exists$1(exePath)) return null;
|
|
1652
|
-
return {
|
|
1653
|
-
kind: inferKindFromIdentifier(bundleId),
|
|
1654
|
-
path: exePath
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
function detectDefaultBrowserBundleIdMac() {
|
|
1658
|
-
const plistPath = path.join(os.homedir(), "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
|
|
1659
|
-
if (!exists$1(plistPath)) return null;
|
|
1660
|
-
const handlersRaw = execText("/usr/bin/plutil", [
|
|
1661
|
-
"-extract",
|
|
1662
|
-
"LSHandlers",
|
|
1663
|
-
"json",
|
|
1664
|
-
"-o",
|
|
1665
|
-
"-",
|
|
1666
|
-
"--",
|
|
1667
|
-
plistPath
|
|
1668
|
-
], 2e3, 5 * 1024 * 1024);
|
|
1669
|
-
if (!handlersRaw) return null;
|
|
1670
|
-
let handlers;
|
|
1671
|
-
try {
|
|
1672
|
-
handlers = JSON.parse(handlersRaw);
|
|
1673
|
-
} catch {
|
|
1674
|
-
return null;
|
|
1675
|
-
}
|
|
1676
|
-
if (!Array.isArray(handlers)) return null;
|
|
1677
|
-
const resolveScheme = (scheme) => {
|
|
1678
|
-
let candidate = null;
|
|
1679
|
-
for (const entry of handlers) {
|
|
1680
|
-
if (!entry || typeof entry !== "object") continue;
|
|
1681
|
-
const record = entry;
|
|
1682
|
-
if (record.LSHandlerURLScheme !== scheme) continue;
|
|
1683
|
-
const role = typeof record.LSHandlerRoleAll === "string" && record.LSHandlerRoleAll || typeof record.LSHandlerRoleViewer === "string" && record.LSHandlerRoleViewer || null;
|
|
1684
|
-
if (role) candidate = role;
|
|
1685
|
-
}
|
|
1686
|
-
return candidate;
|
|
1687
|
-
};
|
|
1688
|
-
return resolveScheme("http") ?? resolveScheme("https");
|
|
1689
|
-
}
|
|
1690
|
-
function detectDefaultChromiumExecutableLinux() {
|
|
1691
|
-
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) || execText("xdg-mime", [
|
|
1692
|
-
"query",
|
|
1693
|
-
"default",
|
|
1694
|
-
"x-scheme-handler/http"
|
|
1695
|
-
]);
|
|
1696
|
-
if (!desktopId) return null;
|
|
1697
|
-
const trimmed = desktopId.trim();
|
|
1698
|
-
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
|
|
1699
|
-
const desktopPath = findDesktopFilePath(trimmed);
|
|
1700
|
-
if (!desktopPath) return null;
|
|
1701
|
-
const execLine = readDesktopExecLine(desktopPath);
|
|
1702
|
-
if (!execLine) return null;
|
|
1703
|
-
const command = extractExecutableFromExecLine(execLine);
|
|
1704
|
-
if (!command) return null;
|
|
1705
|
-
const resolved = resolveLinuxExecutablePath(command);
|
|
1706
|
-
if (!resolved) return null;
|
|
1707
|
-
const exeName = path.posix.basename(resolved).toLowerCase();
|
|
1708
|
-
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
1709
|
-
return {
|
|
1710
|
-
kind: inferKindFromExecutableName(exeName),
|
|
1711
|
-
path: resolved
|
|
1712
|
-
};
|
|
1713
|
-
}
|
|
1714
|
-
function detectDefaultChromiumExecutableWindows() {
|
|
1715
|
-
const progId = readWindowsProgId();
|
|
1716
|
-
const command = (progId ? readWindowsCommandForProgId(progId) : null) || readWindowsCommandForProgId("http");
|
|
1717
|
-
if (!command) return null;
|
|
1718
|
-
const exePath = extractWindowsExecutablePath(expandWindowsEnvVars(command));
|
|
1719
|
-
if (!exePath) return null;
|
|
1720
|
-
if (!exists$1(exePath)) return null;
|
|
1721
|
-
const exeName = path.win32.basename(exePath).toLowerCase();
|
|
1722
|
-
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
1723
|
-
return {
|
|
1724
|
-
kind: inferKindFromExecutableName(exeName),
|
|
1725
|
-
path: exePath
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
function findDesktopFilePath(desktopId) {
|
|
1729
|
-
const candidates = [
|
|
1730
|
-
path.join(os.homedir(), ".local", "share", "applications", desktopId),
|
|
1731
|
-
path.join("/usr/local/share/applications", desktopId),
|
|
1732
|
-
path.join("/usr/share/applications", desktopId),
|
|
1733
|
-
path.join("/var/lib/snapd/desktop/applications", desktopId)
|
|
1734
|
-
];
|
|
1735
|
-
for (const candidate of candidates) if (exists$1(candidate)) return candidate;
|
|
1736
|
-
return null;
|
|
1737
|
-
}
|
|
1738
|
-
function readDesktopExecLine(desktopPath) {
|
|
1739
|
-
try {
|
|
1740
|
-
const lines = fs.readFileSync(desktopPath, "utf8").split(/\r?\n/);
|
|
1741
|
-
for (const line of lines) if (line.startsWith("Exec=")) return line.slice(5).trim();
|
|
1742
|
-
} catch {}
|
|
1743
|
-
return null;
|
|
1744
|
-
}
|
|
1745
|
-
function extractExecutableFromExecLine(execLine) {
|
|
1746
|
-
const tokens = splitExecLine(execLine);
|
|
1747
|
-
for (const token of tokens) {
|
|
1748
|
-
if (!token) continue;
|
|
1749
|
-
if (token === "env") continue;
|
|
1750
|
-
if (token.includes("=") && !token.startsWith("/") && !token.includes("\\")) continue;
|
|
1751
|
-
return token.replace(/^["']|["']$/g, "");
|
|
1752
|
-
}
|
|
1753
|
-
return null;
|
|
1754
|
-
}
|
|
1755
|
-
function splitExecLine(line) {
|
|
1756
|
-
const tokens = [];
|
|
1757
|
-
let current = "";
|
|
1758
|
-
let inQuotes = false;
|
|
1759
|
-
let quoteChar = "";
|
|
1760
|
-
for (let i = 0; i < line.length; i += 1) {
|
|
1761
|
-
const ch = line[i];
|
|
1762
|
-
if ((ch === "\"" || ch === "'") && (!inQuotes || ch === quoteChar)) {
|
|
1763
|
-
if (inQuotes) {
|
|
1764
|
-
inQuotes = false;
|
|
1765
|
-
quoteChar = "";
|
|
1766
|
-
} else {
|
|
1767
|
-
inQuotes = true;
|
|
1768
|
-
quoteChar = ch;
|
|
1769
|
-
}
|
|
1770
|
-
continue;
|
|
1771
|
-
}
|
|
1772
|
-
if (!inQuotes && /\s/.test(ch)) {
|
|
1773
|
-
if (current) {
|
|
1774
|
-
tokens.push(current);
|
|
1775
|
-
current = "";
|
|
1776
|
-
}
|
|
1777
|
-
continue;
|
|
1778
|
-
}
|
|
1779
|
-
current += ch;
|
|
1780
|
-
}
|
|
1781
|
-
if (current) tokens.push(current);
|
|
1782
|
-
return tokens;
|
|
1783
|
-
}
|
|
1784
|
-
function resolveLinuxExecutablePath(command) {
|
|
1785
|
-
const cleaned = command.trim().replace(/%[a-zA-Z]/g, "");
|
|
1786
|
-
if (!cleaned) return null;
|
|
1787
|
-
if (cleaned.startsWith("/")) return cleaned;
|
|
1788
|
-
const resolved = execText("which", [cleaned], 800);
|
|
1789
|
-
return resolved ? resolved.trim() : null;
|
|
1790
|
-
}
|
|
1791
|
-
function readWindowsProgId() {
|
|
1792
|
-
const output = execText("reg", [
|
|
1793
|
-
"query",
|
|
1794
|
-
"HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
|
|
1795
|
-
"/v",
|
|
1796
|
-
"ProgId"
|
|
1797
|
-
]);
|
|
1798
|
-
if (!output) return null;
|
|
1799
|
-
return output.match(/ProgId\s+REG_\w+\s+(.+)$/im)?.[1]?.trim() || null;
|
|
1800
|
-
}
|
|
1801
|
-
function readWindowsCommandForProgId(progId) {
|
|
1802
|
-
const output = execText("reg", [
|
|
1803
|
-
"query",
|
|
1804
|
-
progId === "http" ? "HKCR\\http\\shell\\open\\command" : `HKCR\\${progId}\\shell\\open\\command`,
|
|
1805
|
-
"/ve"
|
|
1806
|
-
]);
|
|
1807
|
-
if (!output) return null;
|
|
1808
|
-
return output.match(/REG_\w+\s+(.+)$/im)?.[1]?.trim() || null;
|
|
1809
|
-
}
|
|
1810
|
-
function expandWindowsEnvVars(value) {
|
|
1811
|
-
return value.replace(/%([^%]+)%/g, (_match, name) => {
|
|
1812
|
-
const key = String(name ?? "").trim();
|
|
1813
|
-
return key ? process.env[key] ?? `%${key}%` : _match;
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
function extractWindowsExecutablePath(command) {
|
|
1817
|
-
const quoted = command.match(/"([^"]+\\.exe)"/i);
|
|
1818
|
-
if (quoted?.[1]) return quoted[1];
|
|
1819
|
-
const unquoted = command.match(/([^\\s]+\\.exe)/i);
|
|
1820
|
-
if (unquoted?.[1]) return unquoted[1];
|
|
1821
|
-
return null;
|
|
1822
|
-
}
|
|
1823
|
-
function findFirstExecutable(candidates) {
|
|
1824
|
-
for (const candidate of candidates) if (exists$1(candidate.path)) return candidate;
|
|
1825
|
-
return null;
|
|
1826
|
-
}
|
|
1827
|
-
function findChromeExecutableMac() {
|
|
1828
|
-
return findFirstExecutable([
|
|
1829
|
-
{
|
|
1830
|
-
kind: "chrome",
|
|
1831
|
-
path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
1832
|
-
},
|
|
1833
|
-
{
|
|
1834
|
-
kind: "chrome",
|
|
1835
|
-
path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
|
|
1836
|
-
},
|
|
1837
|
-
{
|
|
1838
|
-
kind: "brave",
|
|
1839
|
-
path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
1840
|
-
},
|
|
1841
|
-
{
|
|
1842
|
-
kind: "brave",
|
|
1843
|
-
path: path.join(os.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser")
|
|
1844
|
-
},
|
|
1845
|
-
{
|
|
1846
|
-
kind: "edge",
|
|
1847
|
-
path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
|
1848
|
-
},
|
|
1849
|
-
{
|
|
1850
|
-
kind: "edge",
|
|
1851
|
-
path: path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge")
|
|
1852
|
-
},
|
|
1853
|
-
{
|
|
1854
|
-
kind: "chromium",
|
|
1855
|
-
path: "/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
1856
|
-
},
|
|
1857
|
-
{
|
|
1858
|
-
kind: "chromium",
|
|
1859
|
-
path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium")
|
|
1860
|
-
},
|
|
1861
|
-
{
|
|
1862
|
-
kind: "canary",
|
|
1863
|
-
path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
|
|
1864
|
-
},
|
|
1865
|
-
{
|
|
1866
|
-
kind: "canary",
|
|
1867
|
-
path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
|
|
1868
|
-
}
|
|
1869
|
-
]);
|
|
1870
|
-
}
|
|
1871
|
-
function findChromeExecutableLinux() {
|
|
1872
|
-
return findFirstExecutable([
|
|
1873
|
-
{
|
|
1874
|
-
kind: "chrome",
|
|
1875
|
-
path: "/usr/bin/google-chrome"
|
|
1876
|
-
},
|
|
1877
|
-
{
|
|
1878
|
-
kind: "chrome",
|
|
1879
|
-
path: "/usr/bin/google-chrome-stable"
|
|
1880
|
-
},
|
|
1881
|
-
{
|
|
1882
|
-
kind: "chrome",
|
|
1883
|
-
path: "/usr/bin/chrome"
|
|
1884
|
-
},
|
|
1885
|
-
{
|
|
1886
|
-
kind: "brave",
|
|
1887
|
-
path: "/usr/bin/brave-browser"
|
|
1888
|
-
},
|
|
1889
|
-
{
|
|
1890
|
-
kind: "brave",
|
|
1891
|
-
path: "/usr/bin/brave-browser-stable"
|
|
1892
|
-
},
|
|
1893
|
-
{
|
|
1894
|
-
kind: "brave",
|
|
1895
|
-
path: "/usr/bin/brave"
|
|
1896
|
-
},
|
|
1897
|
-
{
|
|
1898
|
-
kind: "brave",
|
|
1899
|
-
path: "/snap/bin/brave"
|
|
1900
|
-
},
|
|
1901
|
-
{
|
|
1902
|
-
kind: "edge",
|
|
1903
|
-
path: "/usr/bin/microsoft-edge"
|
|
1904
|
-
},
|
|
1905
|
-
{
|
|
1906
|
-
kind: "edge",
|
|
1907
|
-
path: "/usr/bin/microsoft-edge-stable"
|
|
1908
|
-
},
|
|
1909
|
-
{
|
|
1910
|
-
kind: "chromium",
|
|
1911
|
-
path: "/usr/bin/chromium"
|
|
1912
|
-
},
|
|
1913
|
-
{
|
|
1914
|
-
kind: "chromium",
|
|
1915
|
-
path: "/usr/bin/chromium-browser"
|
|
1916
|
-
},
|
|
1917
|
-
{
|
|
1918
|
-
kind: "chromium",
|
|
1919
|
-
path: "/snap/bin/chromium"
|
|
1920
|
-
}
|
|
1921
|
-
]);
|
|
1922
|
-
}
|
|
1923
|
-
function findChromeExecutableWindows() {
|
|
1924
|
-
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
1925
|
-
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
|
1926
|
-
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
1927
|
-
const joinWin = path.win32.join;
|
|
1928
|
-
const candidates = [];
|
|
1929
|
-
if (localAppData) {
|
|
1930
|
-
candidates.push({
|
|
1931
|
-
kind: "chrome",
|
|
1932
|
-
path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe")
|
|
1933
|
-
});
|
|
1934
|
-
candidates.push({
|
|
1935
|
-
kind: "brave",
|
|
1936
|
-
path: joinWin(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1937
|
-
});
|
|
1938
|
-
candidates.push({
|
|
1939
|
-
kind: "edge",
|
|
1940
|
-
path: joinWin(localAppData, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
1941
|
-
});
|
|
1942
|
-
candidates.push({
|
|
1943
|
-
kind: "chromium",
|
|
1944
|
-
path: joinWin(localAppData, "Chromium", "Application", "chrome.exe")
|
|
1945
|
-
});
|
|
1946
|
-
candidates.push({
|
|
1947
|
-
kind: "canary",
|
|
1948
|
-
path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe")
|
|
1949
|
-
});
|
|
1950
|
-
}
|
|
1951
|
-
candidates.push({
|
|
1952
|
-
kind: "chrome",
|
|
1953
|
-
path: joinWin(programFiles, "Google", "Chrome", "Application", "chrome.exe")
|
|
1954
|
-
});
|
|
1955
|
-
candidates.push({
|
|
1956
|
-
kind: "chrome",
|
|
1957
|
-
path: joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe")
|
|
1958
|
-
});
|
|
1959
|
-
candidates.push({
|
|
1960
|
-
kind: "brave",
|
|
1961
|
-
path: joinWin(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1962
|
-
});
|
|
1963
|
-
candidates.push({
|
|
1964
|
-
kind: "brave",
|
|
1965
|
-
path: joinWin(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1966
|
-
});
|
|
1967
|
-
candidates.push({
|
|
1968
|
-
kind: "edge",
|
|
1969
|
-
path: joinWin(programFiles, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
1970
|
-
});
|
|
1971
|
-
candidates.push({
|
|
1972
|
-
kind: "edge",
|
|
1973
|
-
path: joinWin(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
1974
|
-
});
|
|
1975
|
-
return findFirstExecutable(candidates);
|
|
1976
|
-
}
|
|
1977
|
-
function resolveBrowserExecutableForPlatform(resolved, platform) {
|
|
1978
|
-
if (resolved.executablePath) {
|
|
1979
|
-
if (!exists$1(resolved.executablePath)) throw new Error(`browser.executablePath not found: ${resolved.executablePath}`);
|
|
1980
|
-
return {
|
|
1981
|
-
kind: "custom",
|
|
1982
|
-
path: resolved.executablePath
|
|
1983
|
-
};
|
|
1984
|
-
}
|
|
1985
|
-
const detected = detectDefaultChromiumExecutable(platform);
|
|
1986
|
-
if (detected) return detected;
|
|
1987
|
-
if (platform === "darwin") return findChromeExecutableMac();
|
|
1988
|
-
if (platform === "linux") return findChromeExecutableLinux();
|
|
1989
|
-
if (platform === "win32") return findChromeExecutableWindows();
|
|
1990
|
-
return null;
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
//#endregion
|
|
1994
|
-
//#region src/infra/ports-lsof.ts
|
|
1995
|
-
const LSOF_CANDIDATES = process.platform === "darwin" ? ["/usr/sbin/lsof", "/usr/bin/lsof"] : ["/usr/bin/lsof", "/usr/sbin/lsof"];
|
|
1996
|
-
function resolveLsofCommandSync() {
|
|
1997
|
-
for (const candidate of LSOF_CANDIDATES) try {
|
|
1998
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1999
|
-
return candidate;
|
|
2000
|
-
} catch {}
|
|
2001
|
-
return "lsof";
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
//#endregion
|
|
2005
|
-
//#region src/infra/ports-probe.ts
|
|
2006
|
-
async function tryListenOnPort(params) {
|
|
2007
|
-
const listenOptions = { port: params.port };
|
|
2008
|
-
if (params.host) listenOptions.host = params.host;
|
|
2009
|
-
if (typeof params.exclusive === "boolean") listenOptions.exclusive = params.exclusive;
|
|
2010
|
-
await new Promise((resolve, reject) => {
|
|
2011
|
-
const tester = net.createServer().once("error", (err) => reject(err)).once("listening", () => {
|
|
2012
|
-
tester.close(() => resolve());
|
|
2013
|
-
}).listen(listenOptions);
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
//#endregion
|
|
2018
|
-
//#region src/infra/ports.ts
|
|
2019
|
-
var PortInUseError = class extends Error {
|
|
2020
|
-
constructor(port, details) {
|
|
2021
|
-
super(`Port ${port} is already in use.`);
|
|
2022
|
-
this.name = "PortInUseError";
|
|
2023
|
-
this.port = port;
|
|
2024
|
-
this.details = details;
|
|
2025
|
-
}
|
|
2026
|
-
};
|
|
2027
|
-
async function ensurePortAvailable(port) {
|
|
2028
|
-
try {
|
|
2029
|
-
await tryListenOnPort({ port });
|
|
2030
|
-
} catch (err) {
|
|
2031
|
-
if (isErrno(err) && err.code === "EADDRINUSE") throw new PortInUseError(port);
|
|
2032
|
-
throw err;
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
//#endregion
|
|
2037
|
-
//#region src/browser/chrome.profile-decoration.ts
|
|
2038
|
-
function decoratedMarkerPath(userDataDir) {
|
|
2039
|
-
return path.join(userDataDir, ".squidclaw-profile-decorated");
|
|
2040
|
-
}
|
|
2041
|
-
function safeReadJson(filePath) {
|
|
2042
|
-
try {
|
|
2043
|
-
if (!fs.existsSync(filePath)) return null;
|
|
2044
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
2045
|
-
const parsed = JSON.parse(raw);
|
|
2046
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
2047
|
-
return parsed;
|
|
2048
|
-
} catch {
|
|
2049
|
-
return null;
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
function safeWriteJson(filePath, data) {
|
|
2053
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2054
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
2055
|
-
}
|
|
2056
|
-
function setDeep(obj, keys, value) {
|
|
2057
|
-
let node = obj;
|
|
2058
|
-
for (const key of keys.slice(0, -1)) {
|
|
2059
|
-
const next = node[key];
|
|
2060
|
-
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
|
|
2061
|
-
node = node[key];
|
|
2062
|
-
}
|
|
2063
|
-
node[keys[keys.length - 1] ?? ""] = value;
|
|
2064
|
-
}
|
|
2065
|
-
function parseHexRgbToSignedArgbInt(hex) {
|
|
2066
|
-
const cleaned = hex.trim().replace(/^#/, "");
|
|
2067
|
-
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return null;
|
|
2068
|
-
const argbUnsigned = 255 << 24 | Number.parseInt(cleaned, 16);
|
|
2069
|
-
return argbUnsigned > 2147483647 ? argbUnsigned - 4294967296 : argbUnsigned;
|
|
2070
|
-
}
|
|
2071
|
-
function isProfileDecorated(userDataDir, desiredName, desiredColorHex) {
|
|
2072
|
-
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
|
|
2073
|
-
const localStatePath = path.join(userDataDir, "Local State");
|
|
2074
|
-
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
2075
|
-
const profile = safeReadJson(localStatePath)?.profile;
|
|
2076
|
-
const infoCache = typeof profile === "object" && profile !== null && !Array.isArray(profile) ? profile.info_cache : null;
|
|
2077
|
-
const info = typeof infoCache === "object" && infoCache !== null && !Array.isArray(infoCache) && typeof infoCache.Default === "object" && infoCache.Default !== null && !Array.isArray(infoCache.Default) ? infoCache.Default : null;
|
|
2078
|
-
const prefs = safeReadJson(preferencesPath);
|
|
2079
|
-
const browserTheme = (() => {
|
|
2080
|
-
const browser = prefs?.browser;
|
|
2081
|
-
const theme = typeof browser === "object" && browser !== null && !Array.isArray(browser) ? browser.theme : null;
|
|
2082
|
-
return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? theme : null;
|
|
2083
|
-
})();
|
|
2084
|
-
const autogeneratedTheme = (() => {
|
|
2085
|
-
const autogenerated = prefs?.autogenerated;
|
|
2086
|
-
const theme = typeof autogenerated === "object" && autogenerated !== null && !Array.isArray(autogenerated) ? autogenerated.theme : null;
|
|
2087
|
-
return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? theme : null;
|
|
2088
|
-
})();
|
|
2089
|
-
const nameOk = typeof info?.name === "string" ? info.name === desiredName : true;
|
|
2090
|
-
if (desiredColorInt == null) return nameOk;
|
|
2091
|
-
const localSeedOk = typeof info?.profile_color_seed === "number" ? info.profile_color_seed === desiredColorInt : false;
|
|
2092
|
-
const prefOk = typeof browserTheme?.user_color2 === "number" && browserTheme.user_color2 === desiredColorInt || typeof autogeneratedTheme?.color === "number" && autogeneratedTheme.color === desiredColorInt;
|
|
2093
|
-
return nameOk && localSeedOk && prefOk;
|
|
2094
|
-
}
|
|
2095
|
-
/**
|
|
2096
|
-
* Best-effort profile decoration (name + lobster-orange). Chrome preference keys
|
|
2097
|
-
* vary by version; we keep this conservative and idempotent.
|
|
2098
|
-
*/
|
|
2099
|
-
function decorateSquidClawProfile(userDataDir, opts) {
|
|
2100
|
-
const desiredName = opts?.name ?? DEFAULT_SQUIDCLAW_BROWSER_PROFILE_NAME;
|
|
2101
|
-
const desiredColor = (opts?.color ?? DEFAULT_SQUIDCLAW_BROWSER_COLOR).toUpperCase();
|
|
2102
|
-
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor);
|
|
2103
|
-
const localStatePath = path.join(userDataDir, "Local State");
|
|
2104
|
-
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
2105
|
-
const localState = safeReadJson(localStatePath) ?? {};
|
|
2106
|
-
setDeep(localState, [
|
|
2107
|
-
"profile",
|
|
2108
|
-
"info_cache",
|
|
2109
|
-
"Default",
|
|
2110
|
-
"name"
|
|
2111
|
-
], desiredName);
|
|
2112
|
-
setDeep(localState, [
|
|
2113
|
-
"profile",
|
|
2114
|
-
"info_cache",
|
|
2115
|
-
"Default",
|
|
2116
|
-
"shortcut_name"
|
|
2117
|
-
], desiredName);
|
|
2118
|
-
setDeep(localState, [
|
|
2119
|
-
"profile",
|
|
2120
|
-
"info_cache",
|
|
2121
|
-
"Default",
|
|
2122
|
-
"user_name"
|
|
2123
|
-
], desiredName);
|
|
2124
|
-
setDeep(localState, [
|
|
2125
|
-
"profile",
|
|
2126
|
-
"info_cache",
|
|
2127
|
-
"Default",
|
|
2128
|
-
"profile_color"
|
|
2129
|
-
], desiredColor);
|
|
2130
|
-
setDeep(localState, [
|
|
2131
|
-
"profile",
|
|
2132
|
-
"info_cache",
|
|
2133
|
-
"Default",
|
|
2134
|
-
"user_color"
|
|
2135
|
-
], desiredColor);
|
|
2136
|
-
if (desiredColorInt != null) {
|
|
2137
|
-
setDeep(localState, [
|
|
2138
|
-
"profile",
|
|
2139
|
-
"info_cache",
|
|
2140
|
-
"Default",
|
|
2141
|
-
"profile_color_seed"
|
|
2142
|
-
], desiredColorInt);
|
|
2143
|
-
setDeep(localState, [
|
|
2144
|
-
"profile",
|
|
2145
|
-
"info_cache",
|
|
2146
|
-
"Default",
|
|
2147
|
-
"profile_highlight_color"
|
|
2148
|
-
], desiredColorInt);
|
|
2149
|
-
setDeep(localState, [
|
|
2150
|
-
"profile",
|
|
2151
|
-
"info_cache",
|
|
2152
|
-
"Default",
|
|
2153
|
-
"default_avatar_fill_color"
|
|
2154
|
-
], desiredColorInt);
|
|
2155
|
-
setDeep(localState, [
|
|
2156
|
-
"profile",
|
|
2157
|
-
"info_cache",
|
|
2158
|
-
"Default",
|
|
2159
|
-
"default_avatar_stroke_color"
|
|
2160
|
-
], desiredColorInt);
|
|
2161
|
-
}
|
|
2162
|
-
safeWriteJson(localStatePath, localState);
|
|
2163
|
-
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
2164
|
-
setDeep(prefs, ["profile", "name"], desiredName);
|
|
2165
|
-
setDeep(prefs, ["profile", "profile_color"], desiredColor);
|
|
2166
|
-
setDeep(prefs, ["profile", "user_color"], desiredColor);
|
|
2167
|
-
if (desiredColorInt != null) {
|
|
2168
|
-
setDeep(prefs, [
|
|
2169
|
-
"autogenerated",
|
|
2170
|
-
"theme",
|
|
2171
|
-
"color"
|
|
2172
|
-
], desiredColorInt);
|
|
2173
|
-
setDeep(prefs, [
|
|
2174
|
-
"browser",
|
|
2175
|
-
"theme",
|
|
2176
|
-
"user_color2"
|
|
2177
|
-
], desiredColorInt);
|
|
2178
|
-
}
|
|
2179
|
-
safeWriteJson(preferencesPath, prefs);
|
|
2180
|
-
try {
|
|
2181
|
-
fs.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}\n`, "utf-8");
|
|
2182
|
-
} catch {}
|
|
2183
|
-
}
|
|
2184
|
-
function ensureProfileCleanExit(userDataDir) {
|
|
2185
|
-
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
2186
|
-
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
2187
|
-
setDeep(prefs, ["exit_type"], "Normal");
|
|
2188
|
-
setDeep(prefs, ["exited_cleanly"], true);
|
|
2189
|
-
safeWriteJson(preferencesPath, prefs);
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
//#endregion
|
|
2193
|
-
//#region src/browser/chrome.ts
|
|
2194
|
-
const log = createSubsystemLogger("browser").child("chrome");
|
|
2195
|
-
function exists(filePath) {
|
|
2196
|
-
try {
|
|
2197
|
-
return fs.existsSync(filePath);
|
|
2198
|
-
} catch {
|
|
2199
|
-
return false;
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
function resolveBrowserExecutable(resolved) {
|
|
2203
|
-
return resolveBrowserExecutableForPlatform(resolved, process.platform);
|
|
2204
|
-
}
|
|
2205
|
-
function resolveSquidClawUserDataDir(profileName = DEFAULT_SQUIDCLAW_BROWSER_PROFILE_NAME) {
|
|
2206
|
-
return path.join(CONFIG_DIR, "browser", profileName, "user-data");
|
|
2207
|
-
}
|
|
2208
|
-
function cdpUrlForPort(cdpPort) {
|
|
2209
|
-
return `http://127.0.0.1:${cdpPort}`;
|
|
2210
|
-
}
|
|
2211
|
-
async function isChromeReachable(cdpUrl, timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS) {
|
|
2212
|
-
const version = await fetchChromeVersion(cdpUrl, timeoutMs);
|
|
2213
|
-
return Boolean(version);
|
|
2214
|
-
}
|
|
2215
|
-
async function fetchChromeVersion(cdpUrl, timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS) {
|
|
2216
|
-
const ctrl = new AbortController();
|
|
2217
|
-
const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
|
|
2218
|
-
try {
|
|
2219
|
-
const data = await (await fetchCdpChecked(appendCdpPath(cdpUrl, "/json/version"), timeoutMs, { signal: ctrl.signal })).json();
|
|
2220
|
-
if (!data || typeof data !== "object") return null;
|
|
2221
|
-
return data;
|
|
2222
|
-
} catch {
|
|
2223
|
-
return null;
|
|
2224
|
-
} finally {
|
|
2225
|
-
clearTimeout(t);
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS) {
|
|
2229
|
-
const version = await fetchChromeVersion(cdpUrl, timeoutMs);
|
|
2230
|
-
const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
|
|
2231
|
-
if (!wsUrl) return null;
|
|
2232
|
-
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
2233
|
-
}
|
|
2234
|
-
async function canRunCdpHealthCommand(wsUrl, timeoutMs = CHROME_WS_READY_TIMEOUT_MS) {
|
|
2235
|
-
return await new Promise((resolve) => {
|
|
2236
|
-
const ws = openCdpWebSocket(wsUrl, { handshakeTimeoutMs: timeoutMs });
|
|
2237
|
-
let settled = false;
|
|
2238
|
-
const onMessage = (raw) => {
|
|
2239
|
-
if (settled) return;
|
|
2240
|
-
let parsed = null;
|
|
2241
|
-
try {
|
|
2242
|
-
parsed = JSON.parse(rawDataToString(raw));
|
|
2243
|
-
} catch {
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
if (parsed?.id !== 1) return;
|
|
2247
|
-
finish(Boolean(parsed.result && typeof parsed.result === "object"));
|
|
2248
|
-
};
|
|
2249
|
-
const finish = (value) => {
|
|
2250
|
-
if (settled) return;
|
|
2251
|
-
settled = true;
|
|
2252
|
-
clearTimeout(timer);
|
|
2253
|
-
ws.off("message", onMessage);
|
|
2254
|
-
try {
|
|
2255
|
-
ws.close();
|
|
2256
|
-
} catch {}
|
|
2257
|
-
resolve(value);
|
|
2258
|
-
};
|
|
2259
|
-
const timer = setTimeout(() => {
|
|
2260
|
-
try {
|
|
2261
|
-
ws.terminate();
|
|
2262
|
-
} catch {}
|
|
2263
|
-
finish(false);
|
|
2264
|
-
}, Math.max(50, timeoutMs + 25));
|
|
2265
|
-
ws.once("open", () => {
|
|
2266
|
-
try {
|
|
2267
|
-
ws.send(JSON.stringify({
|
|
2268
|
-
id: 1,
|
|
2269
|
-
method: "Browser.getVersion"
|
|
2270
|
-
}));
|
|
2271
|
-
} catch {
|
|
2272
|
-
finish(false);
|
|
2273
|
-
}
|
|
2274
|
-
});
|
|
2275
|
-
ws.on("message", onMessage);
|
|
2276
|
-
ws.once("error", () => {
|
|
2277
|
-
finish(false);
|
|
2278
|
-
});
|
|
2279
|
-
ws.once("close", () => {
|
|
2280
|
-
finish(false);
|
|
2281
|
-
});
|
|
2282
|
-
});
|
|
2283
|
-
}
|
|
2284
|
-
async function isChromeCdpReady(cdpUrl, timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS, handshakeTimeoutMs = CHROME_WS_READY_TIMEOUT_MS) {
|
|
2285
|
-
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
2286
|
-
if (!wsUrl) return false;
|
|
2287
|
-
return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
|
|
2288
|
-
}
|
|
2289
|
-
async function launchSquidClawChrome(resolved, profile) {
|
|
2290
|
-
if (!profile.cdpIsLoopback) throw new Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`);
|
|
2291
|
-
await ensurePortAvailable(profile.cdpPort);
|
|
2292
|
-
const exe = resolveBrowserExecutable(resolved);
|
|
2293
|
-
if (!exe) throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows).");
|
|
2294
|
-
const userDataDir = resolveSquidClawUserDataDir(profile.name);
|
|
2295
|
-
fs.mkdirSync(userDataDir, { recursive: true });
|
|
2296
|
-
const needsDecorate = !isProfileDecorated(userDataDir, profile.name, (profile.color ?? DEFAULT_SQUIDCLAW_BROWSER_COLOR).toUpperCase());
|
|
2297
|
-
const spawnOnce = () => {
|
|
2298
|
-
const args = [
|
|
2299
|
-
`--remote-debugging-port=${profile.cdpPort}`,
|
|
2300
|
-
`--user-data-dir=${userDataDir}`,
|
|
2301
|
-
"--no-first-run",
|
|
2302
|
-
"--no-default-browser-check",
|
|
2303
|
-
"--disable-sync",
|
|
2304
|
-
"--disable-background-networking",
|
|
2305
|
-
"--disable-component-update",
|
|
2306
|
-
"--disable-features=Translate,MediaRouter",
|
|
2307
|
-
"--disable-session-crashed-bubble",
|
|
2308
|
-
"--hide-crash-restore-bubble",
|
|
2309
|
-
"--password-store=basic"
|
|
2310
|
-
];
|
|
2311
|
-
if (resolved.headless) {
|
|
2312
|
-
args.push("--headless=new");
|
|
2313
|
-
args.push("--disable-gpu");
|
|
2314
|
-
}
|
|
2315
|
-
if (resolved.noSandbox) {
|
|
2316
|
-
args.push("--no-sandbox");
|
|
2317
|
-
args.push("--disable-setuid-sandbox");
|
|
2318
|
-
}
|
|
2319
|
-
if (process.platform === "linux") args.push("--disable-dev-shm-usage");
|
|
2320
|
-
args.push("--disable-blink-features=AutomationControlled");
|
|
2321
|
-
if (resolved.extraArgs.length > 0) args.push(...resolved.extraArgs);
|
|
2322
|
-
args.push("about:blank");
|
|
2323
|
-
return spawn(exe.path, args, {
|
|
2324
|
-
stdio: "pipe",
|
|
2325
|
-
env: {
|
|
2326
|
-
...process.env,
|
|
2327
|
-
HOME: os.homedir()
|
|
2328
|
-
}
|
|
2329
|
-
});
|
|
2330
|
-
};
|
|
2331
|
-
const startedAt = Date.now();
|
|
2332
|
-
const localStatePath = path.join(userDataDir, "Local State");
|
|
2333
|
-
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
2334
|
-
if (!exists(localStatePath) || !exists(preferencesPath)) {
|
|
2335
|
-
const bootstrap = spawnOnce();
|
|
2336
|
-
const deadline = Date.now() + CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS;
|
|
2337
|
-
while (Date.now() < deadline) {
|
|
2338
|
-
if (exists(localStatePath) && exists(preferencesPath)) break;
|
|
2339
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
2340
|
-
}
|
|
2341
|
-
try {
|
|
2342
|
-
bootstrap.kill("SIGTERM");
|
|
2343
|
-
} catch {}
|
|
2344
|
-
const exitDeadline = Date.now() + CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS;
|
|
2345
|
-
while (Date.now() < exitDeadline) {
|
|
2346
|
-
if (bootstrap.exitCode != null) break;
|
|
2347
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
if (needsDecorate) try {
|
|
2351
|
-
decorateSquidClawProfile(userDataDir, {
|
|
2352
|
-
name: profile.name,
|
|
2353
|
-
color: profile.color
|
|
2354
|
-
});
|
|
2355
|
-
log.info(`🦞 squidclaw browser profile decorated (${profile.color})`);
|
|
2356
|
-
} catch (err) {
|
|
2357
|
-
log.warn(`squidclaw browser profile decoration failed: ${String(err)}`);
|
|
2358
|
-
}
|
|
2359
|
-
try {
|
|
2360
|
-
ensureProfileCleanExit(userDataDir);
|
|
2361
|
-
} catch (err) {
|
|
2362
|
-
log.warn(`squidclaw browser clean-exit prefs failed: ${String(err)}`);
|
|
2363
|
-
}
|
|
2364
|
-
const proc = spawnOnce();
|
|
2365
|
-
const stderrChunks = [];
|
|
2366
|
-
const onStderr = (chunk) => {
|
|
2367
|
-
stderrChunks.push(chunk);
|
|
2368
|
-
};
|
|
2369
|
-
proc.stderr?.on("data", onStderr);
|
|
2370
|
-
const readyDeadline = Date.now() + CHROME_LAUNCH_READY_WINDOW_MS;
|
|
2371
|
-
while (Date.now() < readyDeadline) {
|
|
2372
|
-
if (await isChromeReachable(profile.cdpUrl)) break;
|
|
2373
|
-
await new Promise((r) => setTimeout(r, CHROME_LAUNCH_READY_POLL_MS));
|
|
2374
|
-
}
|
|
2375
|
-
if (!await isChromeReachable(profile.cdpUrl)) {
|
|
2376
|
-
const stderrOutput = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
2377
|
-
const stderrHint = stderrOutput ? `\nChrome stderr:\n${stderrOutput.slice(0, CHROME_STDERR_HINT_MAX_CHARS)}` : "";
|
|
2378
|
-
const sandboxHint = process.platform === "linux" && !resolved.noSandbox ? "\nHint: If running in a container or as root, try setting browser.noSandbox: true in config." : "";
|
|
2379
|
-
try {
|
|
2380
|
-
proc.kill("SIGKILL");
|
|
2381
|
-
} catch {}
|
|
2382
|
-
throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}".${sandboxHint}${stderrHint}`);
|
|
2383
|
-
}
|
|
2384
|
-
proc.stderr?.off("data", onStderr);
|
|
2385
|
-
stderrChunks.length = 0;
|
|
2386
|
-
const pid = proc.pid ?? -1;
|
|
2387
|
-
log.info(`🦞 squidclaw browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`);
|
|
2388
|
-
return {
|
|
2389
|
-
pid,
|
|
2390
|
-
exe,
|
|
2391
|
-
userDataDir,
|
|
2392
|
-
cdpPort: profile.cdpPort,
|
|
2393
|
-
startedAt,
|
|
2394
|
-
proc
|
|
2395
|
-
};
|
|
2396
|
-
}
|
|
2397
|
-
async function stopSquidClawChrome(running, timeoutMs = CHROME_STOP_TIMEOUT_MS) {
|
|
2398
|
-
const proc = running.proc;
|
|
2399
|
-
if (proc.killed) return;
|
|
2400
|
-
try {
|
|
2401
|
-
proc.kill("SIGTERM");
|
|
2402
|
-
} catch {}
|
|
2403
|
-
const start = Date.now();
|
|
2404
|
-
while (Date.now() - start < timeoutMs) {
|
|
2405
|
-
if (!proc.exitCode && proc.killed) break;
|
|
2406
|
-
if (!await isChromeReachable(cdpUrlForPort(running.cdpPort), CHROME_STOP_PROBE_TIMEOUT_MS)) return;
|
|
2407
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
2408
|
-
}
|
|
2409
|
-
try {
|
|
2410
|
-
proc.kill("SIGKILL");
|
|
2411
|
-
} catch {}
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
//#endregion
|
|
2415
|
-
export { DEFAULT_DOWNLOAD_DIR as A, DEFAULT_AI_SNAPSHOT_MAX_CHARS as B, ensureChromeExtensionRelayServer as C, PROFILE_POST_RESTART_WS_TIMEOUT_MS as D, PROFILE_ATTACH_RETRY_TIMEOUT_MS as E, resolveWritablePathWithinRoot as F, DEFAULT_SQUIDCLAW_BROWSER_PROFILE_NAME as G, DEFAULT_BROWSER_EVALUATE_ENABLED as H, DEFAULT_FILL_FIELD_TYPE as I, rawDataToString as J, isLoopbackHost as K, normalizeBrowserFormField as L, DEFAULT_UPLOAD_DIR as M, resolveExistingPathsWithinRoot as N, resolveCdpReachabilityTimeouts as O, resolveStrictExistingPathsWithinRoot as P, DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH as R, withCdpSocket as S, CDP_JSON_NEW_TIMEOUT_MS as T, DEFAULT_SQUIDCLAW_BROWSER_COLOR as U, DEFAULT_BROWSER_DEFAULT_PROFILE_NAME as V, DEFAULT_SQUIDCLAW_BROWSER_ENABLED as W, withBrowserNavigationPolicy as _, resolveSquidClawUserDataDir as a, fetchOk as b, resolveBrowserExecutableForPlatform as c, formatAriaSnapshot as d, normalizeCdpWsUrl as f, assertBrowserNavigationResultAllowed as g, assertBrowserNavigationAllowed as h, launchSquidClawChrome as i, DEFAULT_TRACE_DIR as j, withNoProxyForCdpUrl as k, captureScreenshot as l, InvalidBrowserNavigationUrlError as m, isChromeCdpReady as n, stopSquidClawChrome as o, snapshotAria as p, isSecureWebSocketUrl as q, isChromeReachable as r, resolveLsofCommandSync as s, getChromeWebSocketUrl as t, createTargetViaCdp as u, appendCdpPath as v, stopChromeExtensionRelayServer as w, getHeadersWithAuth as x, fetchJson as y, DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS as z };
|