@organizaone/o1-code 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled/qc-helper/docs/configuration/settings.md +10 -0
- package/bundled/qc-helper/docs/quickstart.md +1 -1
- package/bundled/qc-helper/docs/reference/keyboard-shortcuts.md +36 -36
- package/chunks/{MaxSizedBox-5EOREWNX.js → MaxSizedBox-MPKA4H3S.js} +23 -23
- package/chunks/{StandaloneSessionPicker-3OCB32JD.js → StandaloneSessionPicker-UVCAOTZY.js} +39 -39
- package/chunks/{acp-subagent-executor-JTNA5F4Q.js → acp-subagent-executor-Z7DCA5XD.js} +24 -24
- package/chunks/{acpAgent-IS2JKYZV.js → acpAgent-XD4HSG4S.js} +65 -65
- package/chunks/{agent-U5KE7R4X.js → agent-4PIKN4KJ.js} +11 -11
- package/chunks/{agent-headless-VL764VUV.js → agent-headless-DE5JTWFJ.js} +11 -11
- package/chunks/{anthropicContentGenerator-I7B3ZJTH.js → anthropicContentGenerator-LYCMWQZM.js} +5 -5
- package/chunks/{bridge-4GFWUTJN.js → bridge-CDYVSTRW.js} +24 -24
- package/chunks/{build-RFEU7TIS.js → build-HLEIVF4Y.js} +1 -1
- package/chunks/{chunk-IFJUYGAZ.js → chunk-2USNAY6D.js} +1 -1
- package/chunks/{chunk-MRDWUFJ3.js → chunk-3CVLYID6.js} +3 -3
- package/chunks/{chunk-HXLT4YHM.js → chunk-3NAWLMIE.js} +9 -9
- package/chunks/{chunk-2ZZOVOXT.js → chunk-3QJQWI46.js} +2 -2
- package/chunks/{chunk-4ZTCFNSU.js → chunk-3ZZ62DW7.js} +1 -1
- package/chunks/{chunk-ZOZJOC2D.js → chunk-45P7VYH4.js} +2 -2
- package/chunks/{chunk-FNOLXMFM.js → chunk-46DFZOLD.js} +1 -1
- package/chunks/{chunk-GT5QP7HM.js → chunk-4ALTAZNE.js} +2 -2
- package/chunks/{chunk-OCUQJADH.js → chunk-4J2EVRIE.js} +1 -1
- package/chunks/{chunk-NWQPYJVP.js → chunk-4LZRIWVH.js} +3 -3
- package/chunks/{chunk-3VVXMGS2.js → chunk-4P7BNWDO.js} +1 -1
- package/chunks/{chunk-BO53R5FT.js → chunk-4PE4SRQM.js} +3 -3
- package/chunks/{chunk-NCYMEGY4.js → chunk-5C5U56Q3.js} +5 -5
- package/chunks/{chunk-JY65SH65.js → chunk-5S6C2QM3.js} +3 -3
- package/chunks/{chunk-IKDFLXTG.js → chunk-6IB7ZLX6.js} +4 -4
- package/chunks/{chunk-5OZ6R5FD.js → chunk-6QG24XPQ.js} +3 -3
- package/chunks/{chunk-L2OTZN7G.js → chunk-74Q7U3NS.js} +1 -1
- package/chunks/{chunk-DIVWC7JL.js → chunk-773IX2FW.js} +5 -5
- package/chunks/{chunk-CXL3KGXN.js → chunk-7W7I5SEW.js} +7 -7
- package/chunks/{chunk-4YYRPA6G.js → chunk-APE4IW2O.js} +5 -5
- package/chunks/{chunk-G7HMFP3U.js → chunk-AZY4TCZ7.js} +268 -580
- package/chunks/{chunk-SHDHN4UM.js → chunk-B2FLT75F.js} +1 -1
- package/chunks/{chunk-A6R5ERJF.js → chunk-B43A45RP.js} +4 -4
- package/chunks/{chunk-YPHLY3RQ.js → chunk-B64XOQ4T.js} +5 -5
- package/chunks/{chunk-WVEKU4MO.js → chunk-BH3ZYKWI.js} +2 -2
- package/chunks/{chunk-CLRPG355.js → chunk-BPVDOEUQ.js} +3 -3
- package/chunks/{chunk-SRUCOHM3.js → chunk-BPYLGORC.js} +6 -6
- package/chunks/{chunk-KCIKBXEW.js → chunk-BWWYSCS6.js} +4 -4
- package/chunks/{chunk-73JQWXED.js → chunk-C4LQ2SEV.js} +2 -2
- package/chunks/{chunk-TA5SQUCP.js → chunk-C656OW55.js} +1 -1
- package/chunks/{chunk-XQGFM4KU.js → chunk-CVMZXRZS.js} +5 -5
- package/chunks/{chunk-B74GMYXF.js → chunk-CWDGOQ42.js} +30 -30
- package/chunks/{chunk-AT74D3S6.js → chunk-D33JAHZW.js} +12 -3
- package/chunks/{chunk-PIMJCQZQ.js → chunk-D37ZUISY.js} +1 -1
- package/chunks/{chunk-35XZ5JAH.js → chunk-DEULKIEJ.js} +1 -1
- package/chunks/{chunk-KWUNXWMZ.js → chunk-DKGZJFEL.js} +8 -8
- package/chunks/{chunk-OYF5BLWF.js → chunk-EDDKKBIT.js} +1 -1
- package/chunks/{chunk-FVXJFEVA.js → chunk-EEQPRAGR.js} +1 -1
- package/chunks/{chunk-2XWTDVCK.js → chunk-EHRBJ5RM.js} +2 -2
- package/chunks/{chunk-VL3VAUJC.js → chunk-FAFSNEV5.js} +3 -3
- package/chunks/{chunk-BBH77V6I.js → chunk-FFMBJQC4.js} +1 -1
- package/chunks/{chunk-B5RUUS5Z.js → chunk-FVPKPSA3.js} +1 -1
- package/chunks/{chunk-ZJYMX7GI.js → chunk-G5L3QNEI.js} +3 -3
- package/chunks/{chunk-HFBSP3VC.js → chunk-G5Q6NMUW.js} +1 -1
- package/chunks/{chunk-SD4FTFLM.js → chunk-GELXLAFP.js} +2 -2
- package/chunks/{chunk-PZG6YNNW.js → chunk-GHVRSKPO.js} +1 -1
- package/chunks/{chunk-XNOIGECT.js → chunk-H73FHD45.js} +10 -10
- package/chunks/{chunk-SNSYYPUE.js → chunk-HD6YZF4W.js} +7 -7
- package/chunks/{chunk-PVQB3PSU.js → chunk-HNYPLSAN.js} +1 -1
- package/chunks/{chunk-WFUXHQZO.js → chunk-IBPY5AJI.js} +31 -9
- package/chunks/{chunk-J5XJUCP6.js → chunk-JIE2LIL5.js} +3 -3
- package/chunks/{chunk-KQRLWGYH.js → chunk-JL3XSVHA.js} +1 -1
- package/chunks/{chunk-XMR45LXQ.js → chunk-JOAJ57HE.js} +1 -1
- package/chunks/{chunk-LDZQTXZL.js → chunk-JS4EYSJA.js} +4 -4
- package/chunks/{chunk-XPFD45HB.js → chunk-KCFOR2YE.js} +6 -6
- package/chunks/{chunk-KPGPUSJ4.js → chunk-L4PQEMAM.js} +5 -5
- package/chunks/{chunk-2TR3LXCO.js → chunk-LQLXIPYI.js} +4 -4
- package/chunks/{chunk-X2RYRNAP.js → chunk-LYER2K7O.js} +2 -2
- package/chunks/{chunk-DGB4GDUC.js → chunk-MSVH2BBU.js} +15 -15
- package/chunks/{chunk-UQ6BHHFA.js → chunk-O6SXVQIJ.js} +4 -4
- package/chunks/{chunk-NEG6TBRH.js → chunk-OMTAQ5EZ.js} +1 -1
- package/chunks/{chunk-25B3YO64.js → chunk-PBD5EUD6.js} +1 -1
- package/chunks/{chunk-KFOL4KHG.js → chunk-QBVSXKVU.js} +8 -8
- package/chunks/{chunk-5KLD2C2A.js → chunk-QCXAYVHY.js} +1 -1
- package/chunks/{chunk-NHRNNWWS.js → chunk-QN7NIHRH.js} +1 -1
- package/chunks/{chunk-C2RMEAXF.js → chunk-S7GI7ORV.js} +1 -1
- package/chunks/{chunk-ADSXYGOQ.js → chunk-SYCNHXV2.js} +1 -1
- package/chunks/{chunk-Y2DKG24P.js → chunk-TEX55VSH.js} +1 -1
- package/chunks/{chunk-IF22E34Z.js → chunk-TNG42Z7B.js} +3 -3
- package/chunks/{chunk-GKX63AXY.js → chunk-TUQW42RF.js} +2 -2
- package/chunks/{chunk-ZTD5FNCI.js → chunk-UAFSY3G5.js} +1 -1
- package/chunks/{chunk-3RRITSOA.js → chunk-UFJZ2C4R.js} +2 -2
- package/chunks/{chunk-QKJBBOEM.js → chunk-UWIT4O2Y.js} +2 -2
- package/chunks/{chunk-SJLUEEH6.js → chunk-V5OSSYNA.js} +2 -2
- package/chunks/{chunk-DFHGNP5W.js → chunk-V7HMP3JJ.js} +17 -17
- package/chunks/{chunk-ZCCCPNWD.js → chunk-V7SQBCBF.js} +1 -1
- package/chunks/{chunk-TUQWDNKR.js → chunk-VMDI4JJ3.js} +2 -2
- package/chunks/{chunk-SRM5ACHK.js → chunk-WDPFG2UH.js} +1 -1
- package/chunks/{chunk-4D3AFN6X.js → chunk-XCVOM2VI.js} +3 -3
- package/chunks/{chunk-V2WRTFQU.js → chunk-XK4OF3J4.js} +5 -5
- package/chunks/{chunk-OUANBR2O.js → chunk-XLLMQVGN.js} +1 -1
- package/chunks/{chunk-VEFKFFT5.js → chunk-XRF3D35U.js} +1 -1
- package/chunks/{chunk-DD5QAE42.js → chunk-YDM2USQE.js} +4 -4
- package/chunks/{chunk-BXRUYSL6.js → chunk-YICEY4QV.js} +1 -1
- package/chunks/{chunk-WI4BVBZO.js → chunk-YIDTDNOF.js} +1 -1
- package/chunks/{chunk-HWHXSAP6.js → chunk-YKJL5SJA.js} +10 -10
- package/chunks/{chunk-NIIG7L3H.js → chunk-YO5ABP3C.js} +1 -1
- package/chunks/{chunk-NHVJTPZA.js → chunk-YSNEIJMX.js} +33 -33
- package/chunks/{chunk-N2PY5BWY.js → chunk-Z6I4GSXT.js} +1 -1
- package/chunks/{chunk-LKTMEG3O.js → chunk-ZBXTBSTM.js} +2 -2
- package/chunks/{chunk-SALGNBVH.js → chunk-ZFFHT6AU.js} +3 -3
- package/chunks/{chunk-YK4BICUN.js → chunk-ZLKDL25I.js} +3 -3
- package/chunks/{chunk-UDJURQZ6.js → chunk-ZRB6OWYJ.js} +3 -3
- package/chunks/{chunk-YNYV52Z4.js → chunk-ZXWZU5JK.js} +3 -3
- package/chunks/{chunk-7UG5NBEG.js → chunk-ZZRKSYC2.js} +1 -1
- package/chunks/{codex-subagent-executor-H23KDM2L.js → codex-subagent-executor-QPQRH7VF.js} +24 -24
- package/chunks/{contextCommand-FTM7H7IU.js → contextCommand-MUYCLXI2.js} +23 -23
- package/chunks/{core-runtime-EOPJ5NTV.js → core-runtime-K375FMVY.js} +21 -21
- package/chunks/{create-sub-session-TYQLJBIC.js → create-sub-session-ETVNIXD4.js} +22 -22
- package/chunks/{daemon-git-worktree-guard-XYJOCQD4.js → daemon-git-worktree-guard-URKE4X7B.js} +21 -21
- package/chunks/{daemon-status-provider-WSERMTQ3.js → daemon-status-provider-KBJMZ5VU.js} +29 -29
- package/chunks/{daemon-trust-policy-2TLIJCGQ.js → daemon-trust-policy-5K5J5KAL.js} +27 -27
- package/chunks/{daemon-trust-policy-monitor-5WZ6JDKZ.js → daemon-trust-policy-monitor-KOMAIXNG.js} +27 -27
- package/chunks/{deferred-core-runtime-55WMDAXO.js → deferred-core-runtime-QNITNKSR.js} +21 -21
- package/chunks/{edit-UPV3RDKW.js → edit-NWFXC7XD.js} +13 -13
- package/chunks/{en-S424EGDF.js → en-HPW4HDED.js} +7 -0
- package/chunks/{enterPlanMode-IFHOH44O.js → enterPlanMode-QDDMKHKN.js} +11 -11
- package/chunks/{environment-YEQIXHG6.js → environment-U5DQC4TM.js} +23 -23
- package/chunks/{errors-X2UJSPQL.js → errors-5LSM7QXJ.js} +23 -23
- package/chunks/{execution-tool-EXIGBLTL.js → execution-tool-E56GOUSU.js} +13 -13
- package/chunks/{execution-worker-6PKEW2P6.js → execution-worker-Z5BHN5K5.js} +16 -16
- package/chunks/{exitPlanMode-7NRDPCOW.js → exitPlanMode-KGJAK2Y4.js} +11 -11
- package/chunks/{fast-path-I65CU5I2.js → fast-path-PJGTPCNM.js} +2 -2
- package/chunks/{glob-XTHACKUG.js → glob-EVTXD6VF.js} +11 -11
- package/chunks/{goal-tools-KL2MBM6T.js → goal-tools-AGBJACM2.js} +11 -11
- package/chunks/{handleAutoUpdate-AN36IGA3.js → handleAutoUpdate-43BIPZWM.js} +24 -24
- package/chunks/{i18n-ZOWYH5SP.js → i18n-24CRZK22.js} +22 -22
- package/chunks/{initializer-7VQKSVVS.js → initializer-4DVR3OFQ.js} +27 -27
- package/chunks/{installationInfo-L6JLTT7O.js → installationInfo-ZUYQ74QC.js} +21 -21
- package/chunks/{list-HZEUOA3H.js → list-D4DH2YUE.js} +29 -29
- package/chunks/{llm-VIAH4AFT.js → llm-7O2S5FZZ.js} +53 -53
- package/chunks/{llm-content-generator-AE3URFE7.js → llm-content-generator-EYG44T5V.js} +3 -3
- package/chunks/{loadedSettingsAdapter-LLENP2KA.js → loadedSettingsAdapter-6SDUBKWT.js} +27 -27
- package/chunks/{local-execution-environment-B2UY3HMI.js → local-execution-environment-T3BRDBZD.js} +16 -16
- package/chunks/{loggingContentGenerator-5I5NLC6I.js → loggingContentGenerator-Y4TPIBBN.js} +12 -12
- package/chunks/{managed-npm-update-6XAPVQBT.js → managed-npm-update-ZWNMNX4Z.js} +21 -21
- package/chunks/{mcp-PIYFXVT4.js → mcp-7D3UDC46.js} +27 -27
- package/chunks/{model-call-collection-7DIS7C2G.js → model-call-collection-KCCY7UXB.js} +1 -1
- package/chunks/{model-configuration-J7N3CHD5.js → model-configuration-6CBTIRM4.js} +28 -28
- package/chunks/{modelConfigUtils-6YYQJIHD.js → modelConfigUtils-CPCJMZL2.js} +23 -23
- package/chunks/{monitor-5GBFRXN7.js → monitor-YVEEBOJ2.js} +11 -11
- package/chunks/{nonInteractiveCli-YFE2ILFH.js → nonInteractiveCli-JUQU6XOW.js} +56 -56
- package/chunks/{nonInteractiveToolExecutor-BRFDL2BK.js → nonInteractiveToolExecutor-7E2KS5G3.js} +12 -12
- package/chunks/{notebook-edit-ORYWDEMK.js → notebook-edit-RKCHUGBX.js} +12 -12
- package/chunks/{omni-ANTNX2XB.js → omni-TBNGAJHP.js} +9 -9
- package/chunks/{openaiContentGenerator-AIWBAUPV.js → openaiContentGenerator-W5HZYWHH.js} +7 -7
- package/chunks/{openaiResponsesContentGenerator-SABJHG34.js → openaiResponsesContentGenerator-XFY7636U.js} +3 -3
- package/chunks/{processUtils-UAADVXTD.js → processUtils-AKEG25VQ.js} +2 -2
- package/chunks/{prompt-terminal-ledger-I7POO7OK.js → prompt-terminal-ledger-FSAFESTR.js} +22 -22
- package/chunks/{pt-OHD6RIUC.js → pt-6FF6V764.js} +7 -0
- package/chunks/{reactive-degrade-VHPS7U44.js → reactive-degrade-PKHNF3KO.js} +8 -8
- package/chunks/{read-file-SERUEXAP.js → read-file-E57BEGBG.js} +2 -2
- package/chunks/{resumeHistoryUtils-B6GT4AOO.js → resumeHistoryUtils-HPJKUBOD.js} +26 -26
- package/chunks/{ripGrep-2NTNXBII.js → ripGrep-EX2NI7O3.js} +3 -3
- package/chunks/{run-o1-code-serve-WROVR5TA.js → run-o1-code-serve-NOBIE4OX.js} +32 -32
- package/chunks/{sandboxConfig-UVIRIBNM.js → sandboxConfig-A3IJTTT4.js} +21 -21
- package/chunks/{scheduled-tasks-ILH3HQXS.js → scheduled-tasks-2PZSPNSI.js} +27 -27
- package/chunks/{scheduler-Y2TYB53M.js → scheduler-CJCTKITE.js} +23 -23
- package/chunks/{serve-YUVXMXLS.js → serve-EWPAQLW6.js} +27 -27
- package/chunks/{server-K75VXRXS.js → server-NGE2FORR.js} +57 -57
- package/chunks/{session-KZSKLSES.js → session-PVLA7F4T.js} +57 -57
- package/chunks/{session-attachments-root-MYYIOBTM.js → session-attachments-root-JWDXTWCG.js} +21 -21
- package/chunks/{session-pr-refresh-33KB2JND.js → session-pr-refresh-KSA4SOPP.js} +26 -26
- package/chunks/{settings-HVF3IJ2I.js → settings-D52DDJD6.js} +26 -26
- package/chunks/{shell-PN27CAHY.js → shell-WXVQNSMT.js} +11 -11
- package/chunks/{skill-L6CRUQRR.js → skill-AMXJBJ77.js} +3 -3
- package/chunks/{skill-settings-3Y3SG5O5.js → skill-settings-HK2TQVCB.js} +27 -27
- package/chunks/{spawnChannel-XSDXM46P.js → spawnChannel-2BKO2JU3.js} +23 -23
- package/chunks/{standalone-update-3LDGOGBF.js → standalone-update-FAISIFXV.js} +23 -23
- package/chunks/{startInteractiveUI-GIDY23D3.js → startInteractiveUI-GGN2B6OO.js} +2054 -1919
- package/chunks/{team-create-LFN7HQGC.js → team-create-YTQ2D6AQ.js} +11 -11
- package/chunks/{team-plan-approval-EFVOZVSP.js → team-plan-approval-5I5VH4DM.js} +11 -11
- package/chunks/{terminal-image-renderer-GOF6SZEL.js → terminal-image-renderer-PLNIOAID.js} +21 -21
- package/chunks/{theme-manager-MQ7UIBL2.js → theme-manager-NARRJWQY.js} +21 -21
- package/chunks/{tool-result-media-NQAPMXYD.js → tool-result-media-SVT627AG.js} +9 -9
- package/chunks/{tool-search-JJ7KQQ5S.js → tool-search-YFKQ6DHN.js} +2 -2
- package/chunks/{total-session-admission-GTFQVE5J.js → total-session-admission-LS7DR34W.js} +24 -24
- package/chunks/{trustedFolders-W3AE6S4D.js → trustedFolders-GDOE6W5P.js} +22 -22
- package/chunks/{update-relaunch-VIWAZWCG.js → update-relaunch-FEKGARQ5.js} +5 -5
- package/chunks/{updateCheck-EHHLVFT3.js → updateCheck-BKJJHSTQ.js} +23 -23
- package/chunks/{upload-config-6Z3CPQTV.js → upload-config-2NB472QI.js} +6 -6
- package/chunks/{useAutoAcceptIndicator-2BDM2ZLG.js → useAutoAcceptIndicator-7OUYCX3A.js} +30 -30
- package/chunks/{validateNonInterActiveAuth-SBUIY7JB.js → validateNonInterActiveAuth-IRRCBNZX.js} +53 -53
- package/chunks/{version-LJX5EMFD.js → version-J4YMGCW6.js} +1 -1
- package/chunks/{web-fetch-NMKFGF5A.js → web-fetch-HMJ5ZCBP.js} +2 -2
- package/chunks/{web-search-FT3NC3MF.js → web-search-7JL754Q6.js} +7 -7
- package/chunks/{workflow-5WQJENZ6.js → workflow-L2WL3ZDX.js} +12 -12
- package/chunks/{workspace-providers-status-U3G2IL7D.js → workspace-providers-status-BDNAQ7FG.js} +33 -33
- package/chunks/{workspace-registration-store-XGN5KM7D.js → workspace-registration-store-BJMDZRJN.js} +1 -1
- package/chunks/{workspace-registry-V7Z7GRVC.js → workspace-registry-TYAN4F7B.js} +24 -24
- package/chunks/{workspace-runtime-coordinator-TEWHWJZI.js → workspace-runtime-coordinator-ST75S32T.js} +25 -25
- package/chunks/{workspace-service-JM6TECMD.js → workspace-service-NXC5LXSU.js} +31 -31
- package/chunks/{workspace-skills-status-ESUOVYAN.js → workspace-skills-status-CXJVMP4H.js} +30 -30
- package/chunks/{workspace-trust-reconciler-PGZAZMT3.js → workspace-trust-reconciler-ZPGBWKCS.js} +30 -30
- package/chunks/{write-file-AHGK3PK4.js → write-file-IRIAC5H3.js} +13 -13
- package/chunks/{zoom-image-IU5DRM6D.js → zoom-image-I26GRMX4.js} +2 -2
- package/cli.js +12 -12
- package/execution-worker.js +1 -1
- package/export-transcript-document.js +1 -1
- package/locales/en.js +10 -0
- package/locales/pt.js +10 -0
- package/package.json +2 -2
- package/release-notes.json +12 -0
- package/vendor/o1-connect/.lintstagedrc.mjs +7 -0
- package/vendor/o1-connect/README.md +10 -7
- package/vendor/o1-connect/VENDOR.json +4 -4
- package/vendor/o1-connect/lib.d.mts +3 -1
- package/vendor/o1-connect/lib.mjs +442 -860
- package/web-shell/assets/{abnfDiagram-VCTEODGH-PVbSspuv.js → abnfDiagram-VCTEODGH-Bk3CX78A.js} +1 -1
- package/web-shell/assets/{arc-VsWasC21.js → arc-BR_nvNsB.js} +1 -1
- package/web-shell/assets/{architectureDiagram-5GKGNRK7-CQGJih80.js → architectureDiagram-5GKGNRK7-9N6jxIjW.js} +1 -1
- package/web-shell/assets/{blockDiagram-NRAW4CY4-Df9FTUp0.js → blockDiagram-NRAW4CY4-BtrdWykE.js} +1 -1
- package/web-shell/assets/{c4Diagram-UCG6FXSJ-Cq2wSg56.js → c4Diagram-UCG6FXSJ-D12hT1cu.js} +1 -1
- package/web-shell/assets/channel-CBirg-LI.js +1 -0
- package/web-shell/assets/{chunk-2Q5K7J3B-Chp7JPzQ.js → chunk-2Q5K7J3B-Bwd9bma4.js} +1 -1
- package/web-shell/assets/{chunk-5VM5RSS4-gml5kfyF.js → chunk-5VM5RSS4-DXDhytPU.js} +1 -1
- package/web-shell/assets/{chunk-F27PBJKO-By_hW2gT.js → chunk-F27PBJKO-D2so4bRw.js} +1 -1
- package/web-shell/assets/{chunk-G27WJ6UU-8NLL7TjL.js → chunk-G27WJ6UU-QMGf_hdg.js} +1 -1
- package/web-shell/assets/{chunk-JWPE2WC7-Bz8c6lcT.js → chunk-JWPE2WC7-D4UNrq92.js} +1 -1
- package/web-shell/assets/{chunk-LCL6LL3I-ZkGZaXXw.js → chunk-LCL6LL3I-DvzcfF9r.js} +1 -1
- package/web-shell/assets/{chunk-POPQ4Y6H-B8iYNlKK.js → chunk-POPQ4Y6H-DTgfcLRJ.js} +1 -1
- package/web-shell/assets/{chunk-SVP7TREG-DwVdGXs4.js → chunk-SVP7TREG-izf3rZJp.js} +1 -1
- package/web-shell/assets/{chunk-XXDRQBXY-CsCbtvRS.js → chunk-XXDRQBXY-DljhFpef.js} +1 -1
- package/web-shell/assets/classDiagram-DTDB5LWJ-OPWv8k1L.js +1 -0
- package/web-shell/assets/classDiagram-v2-JRS7N3AN-OPWv8k1L.js +1 -0
- package/web-shell/assets/{cose-bilkent-JH36ORCC-DwnxQIaV.js → cose-bilkent-JH36ORCC-CpIh97Lb.js} +1 -1
- package/web-shell/assets/{cynefin-OW5HDTMX-BvGw520V.js → cynefin-OW5HDTMX-Dd_PEuNo.js} +1 -1
- package/web-shell/assets/{cynefinDiagram-5FMLGOSQ-DErV7NUm.js → cynefinDiagram-5FMLGOSQ-B1iPtQOd.js} +1 -1
- package/web-shell/assets/{dagre-3AP2YEHR-DUKJascy.js → dagre-3AP2YEHR-BrvQN_6z.js} +1 -1
- package/web-shell/assets/{diagram-S7CK7UJ4-DC2A1fK2.js → diagram-S7CK7UJ4-PDnxya9q.js} +1 -1
- package/web-shell/assets/{diagram-UQ7AKVKN-Dv3eUqcO.js → diagram-UQ7AKVKN-BkRMXeTB.js} +1 -1
- package/web-shell/assets/{diagram-VSXAHHWV-B_k7qlTN.js → diagram-VSXAHHWV-lAiZzifg.js} +1 -1
- package/web-shell/assets/{diagram-VX7I27RA-DH7Bx528.js → diagram-VX7I27RA-B6MvRJIJ.js} +1 -1
- package/web-shell/assets/{diagram-Z3DM3KII-CtOVvMVx.js → diagram-Z3DM3KII-CUjAutSP.js} +1 -1
- package/web-shell/assets/{ebnfDiagram-PWID7BFC-DrkkLJdI.js → ebnfDiagram-PWID7BFC-ByDyIWnZ.js} +1 -1
- package/web-shell/assets/{erDiagram-SSCWMZ5O-C0uNwMNd.js → erDiagram-SSCWMZ5O-CMF9JtS6.js} +1 -1
- package/web-shell/assets/{flowDiagram-A5DVABFB-B3sbye5k.js → flowDiagram-A5DVABFB-Cvwsq1Bo.js} +1 -1
- package/web-shell/assets/{ganttDiagram-EL5Y4UJY-D3Ms91Ay.js → ganttDiagram-EL5Y4UJY-BDfktaDD.js} +1 -1
- package/web-shell/assets/{gitGraphDiagram-WWUBYQGX-BHrKQ3iH.js → gitGraphDiagram-WWUBYQGX-DEEdlWcc.js} +1 -1
- package/web-shell/assets/{index-BL5DGwqU.js → index-C08mPO2E.js} +3 -3
- package/web-shell/assets/{index-DvuDn2OC.js → index-CGrLBpT2.js} +1 -1
- package/web-shell/assets/{infoDiagram-RXCK75RN-DWyT73_I.js → infoDiagram-RXCK75RN-D1VQ_kZI.js} +1 -1
- package/web-shell/assets/{ishikawaDiagram-5VMMS53U-DybYyMJT.js → ishikawaDiagram-5VMMS53U-C7_B_Tul.js} +1 -1
- package/web-shell/assets/{journeyDiagram-EYS64GPL-D-v8PiYH.js → journeyDiagram-EYS64GPL-Dzeh8Ykq.js} +1 -1
- package/web-shell/assets/{kanban-definition-3QL26DDD-BUWrNJxx.js → kanban-definition-3QL26DDD-BUVwFNEo.js} +1 -1
- package/web-shell/assets/{layout-DTXwh-A4.js → layout-CHOKfXHU.js} +1 -1
- package/web-shell/assets/{linear-C8iFKFtJ.js → linear-C4Wpe0iD.js} +1 -1
- package/web-shell/assets/{mermaid.core-DZcsifbF.js → mermaid.core-m4gQMm4Q.js} +6 -6
- package/web-shell/assets/{mindmap-definition-FBJOCRG2-D1OyJM6T.js → mindmap-definition-FBJOCRG2-BGrDRA57.js} +1 -1
- package/web-shell/assets/{pegDiagram-XKGWAZYB-b2dM3687.js → pegDiagram-XKGWAZYB-CXhAo2be.js} +1 -1
- package/web-shell/assets/{pieDiagram-E7YTZNPT-n-qlKbdF.js → pieDiagram-E7YTZNPT-BNPT9sa7.js} +1 -1
- package/web-shell/assets/{quadrantDiagram-AXDQQJYC-DbiRHm_L.js → quadrantDiagram-AXDQQJYC-DiLNHUaM.js} +1 -1
- package/web-shell/assets/{railroadDiagram-O6MQD6OU-CAUM_6wo.js → railroadDiagram-O6MQD6OU-CmEaRU9P.js} +1 -1
- package/web-shell/assets/{requirementDiagram-EFPCY7ZU-D_YoabOy.js → requirementDiagram-EFPCY7ZU-CTiKG6rg.js} +1 -1
- package/web-shell/assets/{sankeyDiagram-P5KCCOFB-C17c2F-F.js → sankeyDiagram-P5KCCOFB-BoAEtGxz.js} +1 -1
- package/web-shell/assets/{sequenceDiagram-WJ2MYXX4-DtEF8IM9.js → sequenceDiagram-WJ2MYXX4-GBchxwdW.js} +1 -1
- package/web-shell/assets/{sizeCapture-X5ZJPWSS-CEayyeYf.js → sizeCapture-X5ZJPWSS-EAmGoIFW.js} +1 -1
- package/web-shell/assets/{stateDiagram-HBIQ2CUA-D34ANpKU.js → stateDiagram-HBIQ2CUA-BDNlELRq.js} +1 -1
- package/web-shell/assets/stateDiagram-v2-4QOOHH4V-CQuU4s5n.js +1 -0
- package/web-shell/assets/{swimlanes-XN3QIQJK-CxuDoZvW.js → swimlanes-XN3QIQJK-CBOzrWrz.js} +1 -1
- package/web-shell/assets/swimlanesDiagram-VK2B7HYN-Bf4cDpVo.js +8 -0
- package/web-shell/assets/{timeline-definition-24CTP7MA-ewm8t6bY.js → timeline-definition-24CTP7MA-0zGAApyZ.js} +1 -1
- package/web-shell/assets/{vennDiagram-4TSXK5OY-J5Luz0Pz.js → vennDiagram-4TSXK5OY-DF4g99lN.js} +1 -1
- package/web-shell/assets/{wardleyDiagram-VM6X3IG4-g9-5UNQz.js → wardleyDiagram-VM6X3IG4-eonsHLeg.js} +1 -1
- package/web-shell/assets/{xychartDiagram-S5SC5T6Z-Dy0uNlVX.js → xychartDiagram-S5SC5T6Z-Cq-H1yjY.js} +1 -1
- package/web-shell/index.html +1 -1
- package/web-shell/sw.js +1 -1
- package/web-shell/assets/channel-8zIIJdbY.js +0 -1
- package/web-shell/assets/classDiagram-DTDB5LWJ-BZiDhVAk.js +0 -1
- package/web-shell/assets/classDiagram-v2-JRS7N3AN-BZiDhVAk.js +0 -1
- package/web-shell/assets/stateDiagram-v2-4QOOHH4V-BkgMHPaH.js +0 -1
- package/web-shell/assets/swimlanesDiagram-VK2B7HYN-B_dlOslo.js +0 -8
|
@@ -1,106 +1,85 @@
|
|
|
1
|
-
// o1-connect-lib 9.
|
|
1
|
+
// o1-connect-lib 9.2.0 - https://github.com/organizaone/o1-gateway (MIT)
|
|
2
2
|
|
|
3
3
|
// src/shared/tunnel-protocol.ts
|
|
4
|
-
import { createHash } from
|
|
5
|
-
var TUNNEL_PATH =
|
|
6
|
-
var TUNNEL_PINS_PATH =
|
|
7
|
-
var TUNNEL_PROTOCOL =
|
|
8
|
-
var TUNNEL_SERVERNAME =
|
|
9
|
-
var CODE_PREFIX =
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
var TUNNEL_PATH = "/tunnel";
|
|
6
|
+
var TUNNEL_PINS_PATH = "/tunnel/pins";
|
|
7
|
+
var TUNNEL_PROTOCOL = "o1gw-tunnel.v1";
|
|
8
|
+
var TUNNEL_SERVERNAME = "o1gw-tunnel";
|
|
9
|
+
var CODE_PREFIX = "o1gw1.";
|
|
10
10
|
var PIN = /^sha256\/[A-Za-z0-9+/]{43}=$/;
|
|
11
11
|
var TOKEN = /^[0-9a-f]{64}$/;
|
|
12
12
|
var DEVICE = /^[a-z0-9][a-z0-9_-]{0,31}$/i;
|
|
13
13
|
var MAX_PINS = 4;
|
|
14
|
-
var
|
|
14
|
+
var MAX_HOSTS = 4;
|
|
15
|
+
var ConnectionCodeError = class extends Error {
|
|
16
|
+
};
|
|
15
17
|
function pinOfSpki(spkiDer) {
|
|
16
|
-
return `sha256/${createHash(
|
|
18
|
+
return `sha256/${createHash("sha256").update(spkiDer).digest("base64")}`;
|
|
17
19
|
}
|
|
18
20
|
function isPin(s) {
|
|
19
21
|
return PIN.test(s);
|
|
20
22
|
}
|
|
21
23
|
function fingerprint(pin) {
|
|
22
|
-
if (!isPin(pin)) throw new ConnectionCodeError(
|
|
23
|
-
const hex = Buffer.from(pin.slice(
|
|
24
|
-
|
|
25
|
-
.toUpperCase();
|
|
26
|
-
return (hex.match(/.{4}/g) ?? []).join('-');
|
|
24
|
+
if (!isPin(pin)) throw new ConnectionCodeError("not a pin");
|
|
25
|
+
const hex = Buffer.from(pin.slice("sha256/".length), "base64").toString("hex").toUpperCase();
|
|
26
|
+
return (hex.match(/.{4}/g) ?? []).join("-");
|
|
27
27
|
}
|
|
28
28
|
function sniOfPin(pin) {
|
|
29
|
-
if (!isPin(pin)) throw new ConnectionCodeError(
|
|
30
|
-
return `k-${Buffer.from(pin.slice(
|
|
29
|
+
if (!isPin(pin)) throw new ConnectionCodeError("not a pin");
|
|
30
|
+
return `k-${Buffer.from(pin.slice("sha256/".length), "base64").toString("hex").slice(0, 20)}.o1gw.invalid`;
|
|
31
31
|
}
|
|
32
32
|
function isLoopbackHost(hostname) {
|
|
33
|
-
return
|
|
34
|
-
hostname === '127.0.0.1' || hostname === 'localhost' || hostname === '[::1]'
|
|
35
|
-
);
|
|
33
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]";
|
|
36
34
|
}
|
|
37
|
-
function
|
|
38
|
-
if (!c || typeof c !== 'object')
|
|
39
|
-
throw new ConnectionCodeError('the code does not hold a connection');
|
|
40
|
-
const o = c;
|
|
35
|
+
function originOf(value, what) {
|
|
41
36
|
let url;
|
|
42
37
|
try {
|
|
43
|
-
url = new URL(
|
|
38
|
+
url = new URL(value);
|
|
44
39
|
} catch {
|
|
45
|
-
throw new ConnectionCodeError(
|
|
46
|
-
}
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
if (typeof o.token !== 'string' || !TOKEN.test(o.token))
|
|
75
|
-
throw new ConnectionCodeError('the device token is not valid');
|
|
76
|
-
return {
|
|
77
|
-
url: url.origin,
|
|
78
|
-
device: o.device,
|
|
79
|
-
pins: [...o.pins],
|
|
80
|
-
token: o.token,
|
|
81
|
-
};
|
|
40
|
+
throw new ConnectionCodeError(`the proxy ${what} is not a URL`);
|
|
41
|
+
}
|
|
42
|
+
if (!(url.protocol === "https:" || url.protocol === "http:" && isLoopbackHost(url.hostname))) {
|
|
43
|
+
throw new ConnectionCodeError(`the proxy ${what} must be https`);
|
|
44
|
+
}
|
|
45
|
+
if (url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
|
|
46
|
+
throw new ConnectionCodeError(`the proxy ${what} must be the proxy's base URL`);
|
|
47
|
+
}
|
|
48
|
+
return url.origin;
|
|
49
|
+
}
|
|
50
|
+
function validateCode(c) {
|
|
51
|
+
if (!c || typeof c !== "object") throw new ConnectionCodeError("the code does not hold a connection");
|
|
52
|
+
const o = c;
|
|
53
|
+
const url = originOf(String(o.url), "URL");
|
|
54
|
+
if (typeof o.device !== "string" || !DEVICE.test(o.device)) throw new ConnectionCodeError("the device name is not valid");
|
|
55
|
+
if (!Array.isArray(o.pins) || o.pins.length === 0 || o.pins.length > MAX_PINS || !o.pins.every((p) => typeof p === "string" && isPin(p))) {
|
|
56
|
+
throw new ConnectionCodeError("the pin list is not valid");
|
|
57
|
+
}
|
|
58
|
+
if (typeof o.token !== "string" || !TOKEN.test(o.token)) throw new ConnectionCodeError("the device token is not valid");
|
|
59
|
+
let urls;
|
|
60
|
+
if (o.urls !== void 0) {
|
|
61
|
+
if (!Array.isArray(o.urls) || o.urls.length === 0 || o.urls.some((u) => typeof u !== "string")) throw new ConnectionCodeError("the failover host list is not valid");
|
|
62
|
+
urls = o.urls.map((u, i) => originOf(u, `failover host ${i + 1}`));
|
|
63
|
+
const all = [url, ...urls];
|
|
64
|
+
if (new Set(all).size !== all.length) throw new ConnectionCodeError("the failover host list repeats a host");
|
|
65
|
+
if (all.length > MAX_HOSTS) throw new ConnectionCodeError(`the code names more than ${MAX_HOSTS} hosts`);
|
|
66
|
+
}
|
|
67
|
+
return { url, ...urls === void 0 ? {} : { urls }, device: o.device, pins: [...o.pins], token: o.token };
|
|
82
68
|
}
|
|
83
69
|
function decodeConnectionCode(s) {
|
|
84
70
|
const text = s.trim();
|
|
85
|
-
if (!text.startsWith(CODE_PREFIX))
|
|
86
|
-
throw new ConnectionCodeError(
|
|
87
|
-
'this is not an o1-connect code (it starts with o1gw1.)',
|
|
88
|
-
);
|
|
71
|
+
if (!text.startsWith(CODE_PREFIX)) throw new ConnectionCodeError("this is not an o1-connect code (it starts with o1gw1.)");
|
|
89
72
|
let parsed;
|
|
90
73
|
try {
|
|
91
|
-
parsed = JSON.parse(
|
|
92
|
-
Buffer.from(text.slice(CODE_PREFIX.length), 'base64url').toString('utf8'),
|
|
93
|
-
);
|
|
74
|
+
parsed = JSON.parse(Buffer.from(text.slice(CODE_PREFIX.length), "base64url").toString("utf8"));
|
|
94
75
|
} catch {
|
|
95
|
-
throw new ConnectionCodeError(
|
|
96
|
-
'the code is damaged: copy it again from the console',
|
|
97
|
-
);
|
|
76
|
+
throw new ConnectionCodeError("the code is damaged: copy it again from the console");
|
|
98
77
|
}
|
|
99
78
|
return validateCode(parsed);
|
|
100
79
|
}
|
|
101
80
|
|
|
102
81
|
// src/client/inner.ts
|
|
103
|
-
import tls3 from
|
|
82
|
+
import tls3 from "node:tls";
|
|
104
83
|
|
|
105
84
|
// src/client/peer-cert.ts
|
|
106
85
|
var seen = /* @__PURE__ */ new WeakMap();
|
|
@@ -113,150 +92,115 @@ function peerCertificate(socket) {
|
|
|
113
92
|
}
|
|
114
93
|
|
|
115
94
|
// src/client/poll-client.ts
|
|
116
|
-
import { randomBytes as randomBytes2 } from
|
|
117
|
-
import http3 from
|
|
118
|
-
import https2 from
|
|
119
|
-
import { isIP as isIP2 } from
|
|
120
|
-
import { Duplex as Duplex2 } from
|
|
121
|
-
import tls2 from
|
|
95
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
96
|
+
import http3 from "node:http";
|
|
97
|
+
import https2 from "node:https";
|
|
98
|
+
import { isIP as isIP2 } from "node:net";
|
|
99
|
+
import { Duplex as Duplex2 } from "node:stream";
|
|
100
|
+
import tls2 from "node:tls";
|
|
122
101
|
|
|
123
102
|
// src/shared/tunnel-poll.ts
|
|
124
|
-
var POLL_PATH =
|
|
103
|
+
var POLL_PATH = "/tunnel/poll";
|
|
125
104
|
var POLL_VERSION = 1;
|
|
126
105
|
var SESSION_BYTES = 32;
|
|
127
106
|
var MAX_UP = 256 * 1024;
|
|
128
107
|
var MAX_DOWN = 1024 * 1024;
|
|
129
108
|
var POLL_WAIT_MS = 2e4;
|
|
130
|
-
var TUNNEL_BUSY =
|
|
109
|
+
var TUNNEL_BUSY = "tunnel_busy";
|
|
131
110
|
var REQUEST_HEAD = 1 + SESSION_BYTES + 8 + 8 + 1;
|
|
132
111
|
var RESPONSE_HEAD = 1 + SESSION_BYTES + 8 + 1;
|
|
133
112
|
var FLAG_CLOSE = 1;
|
|
134
113
|
var FLAG_POLL = 2;
|
|
135
114
|
var FLAG_OPEN = 4;
|
|
136
115
|
var FLAG_CLOSED = 1;
|
|
137
|
-
var PollFormatError = class extends Error {
|
|
116
|
+
var PollFormatError = class extends Error {
|
|
117
|
+
};
|
|
138
118
|
var ZERO_SESSION = Buffer.alloc(SESSION_BYTES);
|
|
139
119
|
function sessionOf(id) {
|
|
140
|
-
if (id.length !== SESSION_BYTES)
|
|
141
|
-
throw new PollFormatError('the session id must be 32 bytes');
|
|
120
|
+
if (id.length !== SESSION_BYTES) throw new PollFormatError("the session id must be 32 bytes");
|
|
142
121
|
return id;
|
|
143
122
|
}
|
|
144
123
|
function requestSession(id) {
|
|
145
|
-
if (sessionOf(id).equals(ZERO_SESSION))
|
|
146
|
-
throw new PollFormatError('the session id must not be all zero');
|
|
124
|
+
if (sessionOf(id).equals(ZERO_SESSION)) throw new PollFormatError("the session id must not be all zero");
|
|
147
125
|
return id;
|
|
148
126
|
}
|
|
149
127
|
function toU64(n, what) {
|
|
150
|
-
if (!Number.isSafeInteger(n) || n < 0)
|
|
151
|
-
throw new PollFormatError(`${what} is out of range`);
|
|
128
|
+
if (!Number.isSafeInteger(n) || n < 0) throw new PollFormatError(`${what} is out of range`);
|
|
152
129
|
return BigInt(n);
|
|
153
130
|
}
|
|
154
131
|
function fromU64(b, at, what) {
|
|
155
132
|
const v = b.readBigUInt64BE(at);
|
|
156
|
-
if (v > BigInt(Number.MAX_SAFE_INTEGER))
|
|
157
|
-
throw new PollFormatError(`${what} is out of range`);
|
|
133
|
+
if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new PollFormatError(`${what} is out of range`);
|
|
158
134
|
return Number(v);
|
|
159
135
|
}
|
|
160
136
|
function encodeRequest(r) {
|
|
161
|
-
if (r.data.length > MAX_UP)
|
|
162
|
-
throw new PollFormatError('the request carries too much data');
|
|
137
|
+
if (r.data.length > MAX_UP) throw new PollFormatError("the request carries too much data");
|
|
163
138
|
const head = Buffer.alloc(REQUEST_HEAD);
|
|
164
139
|
head[0] = POLL_VERSION;
|
|
165
140
|
requestSession(r.session).copy(head, 1);
|
|
166
|
-
head.writeBigUInt64BE(toU64(r.upOffset,
|
|
167
|
-
head.writeBigUInt64BE(toU64(r.ack,
|
|
168
|
-
head[49] =
|
|
169
|
-
(r.close ? FLAG_CLOSE : 0) |
|
|
170
|
-
(r.poll ? FLAG_POLL : 0) |
|
|
171
|
-
(r.open ? FLAG_OPEN : 0);
|
|
141
|
+
head.writeBigUInt64BE(toU64(r.upOffset, "upOffset"), 33);
|
|
142
|
+
head.writeBigUInt64BE(toU64(r.ack, "ack"), 41);
|
|
143
|
+
head[49] = (r.close ? FLAG_CLOSE : 0) | (r.poll ? FLAG_POLL : 0) | (r.open ? FLAG_OPEN : 0);
|
|
172
144
|
return Buffer.concat([head, r.data]);
|
|
173
145
|
}
|
|
174
146
|
function decodeResponse(b) {
|
|
175
|
-
if (b.length < RESPONSE_HEAD)
|
|
176
|
-
|
|
177
|
-
if (b
|
|
178
|
-
throw new PollFormatError("the answer is not a tunnel's");
|
|
179
|
-
if (b.length - RESPONSE_HEAD > MAX_DOWN)
|
|
180
|
-
throw new PollFormatError('the answer carries too much data');
|
|
147
|
+
if (b.length < RESPONSE_HEAD) throw new PollFormatError("the answer is too short");
|
|
148
|
+
if (b[0] !== POLL_VERSION) throw new PollFormatError("the answer is not a tunnel's");
|
|
149
|
+
if (b.length - RESPONSE_HEAD > MAX_DOWN) throw new PollFormatError("the answer carries too much data");
|
|
181
150
|
return {
|
|
182
151
|
session: Buffer.from(b.subarray(1, 33)),
|
|
183
|
-
downOffset: fromU64(b, 33,
|
|
152
|
+
downOffset: fromU64(b, 33, "downOffset"),
|
|
184
153
|
closed: (b[41] & FLAG_CLOSED) !== 0,
|
|
185
|
-
data: Buffer.from(b.subarray(RESPONSE_HEAD))
|
|
154
|
+
data: Buffer.from(b.subarray(RESPONSE_HEAD))
|
|
186
155
|
};
|
|
187
156
|
}
|
|
188
157
|
|
|
189
158
|
// src/client/proxy-route.ts
|
|
190
|
-
import http from
|
|
159
|
+
import http from "node:http";
|
|
191
160
|
var LOOPBACK = /^(localhost|127(?:\.\d{1,3}){3}|::1)$/i;
|
|
192
161
|
function bypassed(host, list) {
|
|
193
|
-
return list
|
|
194
|
-
.split(',')
|
|
195
|
-
.map((s) => s.trim().toLowerCase())
|
|
196
|
-
.filter(Boolean)
|
|
197
|
-
.some(
|
|
198
|
-
(entry) =>
|
|
199
|
-
entry === '*' ||
|
|
200
|
-
entry === host ||
|
|
201
|
-
(entry.startsWith('.') && host.endsWith(entry)),
|
|
202
|
-
);
|
|
162
|
+
return list.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean).some((entry) => entry === "*" || entry === host || entry.startsWith(".") && host.endsWith(entry));
|
|
203
163
|
}
|
|
204
164
|
function proxyFor(target, env = process.env) {
|
|
205
|
-
const host = target.hostname.replace(/^\[|\]$/g,
|
|
165
|
+
const host = target.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
206
166
|
if (LOOPBACK.test(host)) return null;
|
|
207
|
-
const raw = (env.HTTPS_PROXY || env.https_proxy ||
|
|
167
|
+
const raw = (env.HTTPS_PROXY || env.https_proxy || "").trim();
|
|
208
168
|
if (!raw) return null;
|
|
209
|
-
if (bypassed(host, env.NO_PROXY || env.no_proxy ||
|
|
169
|
+
if (bypassed(host, env.NO_PROXY || env.no_proxy || "")) return null;
|
|
210
170
|
let url;
|
|
211
171
|
try {
|
|
212
|
-
url = new URL(raw.includes(
|
|
172
|
+
url = new URL(raw.includes("://") ? raw : `http://${raw}`);
|
|
213
173
|
} catch {
|
|
214
|
-
throw new Error(
|
|
174
|
+
throw new Error("HTTPS_PROXY is not a valid URL");
|
|
215
175
|
}
|
|
216
|
-
if (url.protocol !==
|
|
217
|
-
throw new Error('HTTPS_PROXY must be an http:// proxy URL');
|
|
176
|
+
if (url.protocol !== "http:") throw new Error("HTTPS_PROXY must be an http:// proxy URL");
|
|
218
177
|
const user = decodeURIComponent(url.username);
|
|
219
178
|
const pass = decodeURIComponent(url.password);
|
|
220
179
|
return {
|
|
221
|
-
host: url.hostname.replace(/^\[|\]$/g,
|
|
180
|
+
host: url.hostname.replace(/^\[|\]$/g, ""),
|
|
222
181
|
port: Number(url.port || 80),
|
|
223
|
-
...
|
|
224
|
-
? {
|
|
225
|
-
authorization: `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`,
|
|
226
|
-
}
|
|
227
|
-
: {}),
|
|
182
|
+
...user || pass ? { authorization: `Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}` } : {}
|
|
228
183
|
};
|
|
229
184
|
}
|
|
230
185
|
var ProxyRefusedError = class extends Error {
|
|
231
186
|
constructor(status) {
|
|
232
|
-
super(
|
|
233
|
-
status === 407
|
|
234
|
-
? "the network's proxy needs credentials (HTTP 407): put them in HTTPS_PROXY as http://user:password@host:port"
|
|
235
|
-
: `the network's proxy refused the connection (HTTP ${status})`,
|
|
236
|
-
);
|
|
187
|
+
super(status === 407 ? "the network's proxy needs credentials (HTTP 407): put them in HTTPS_PROXY as http://user:password@host:port" : `the network's proxy refused the connection (HTTP ${status})`);
|
|
237
188
|
this.status = status;
|
|
238
189
|
}
|
|
239
190
|
status;
|
|
240
191
|
};
|
|
241
192
|
function connectVia(route, host, port) {
|
|
242
|
-
const authority = host.includes(
|
|
243
|
-
? `[${host}]:${port}`
|
|
244
|
-
: `${host}:${port}`;
|
|
193
|
+
const authority = host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
245
194
|
const req = http.request({
|
|
246
195
|
host: route.host,
|
|
247
196
|
port: route.port,
|
|
248
|
-
method:
|
|
197
|
+
method: "CONNECT",
|
|
249
198
|
path: authority,
|
|
250
199
|
agent: false,
|
|
251
|
-
headers: {
|
|
252
|
-
Host: authority,
|
|
253
|
-
...(route.authorization
|
|
254
|
-
? { 'Proxy-Authorization': route.authorization }
|
|
255
|
-
: {}),
|
|
256
|
-
},
|
|
200
|
+
headers: { Host: authority, ...route.authorization ? { "Proxy-Authorization": route.authorization } : {} }
|
|
257
201
|
});
|
|
258
202
|
const socket = new Promise((resolve, reject) => {
|
|
259
|
-
req.on(
|
|
203
|
+
req.on("connect", (res, sock, head) => {
|
|
260
204
|
if (res.statusCode !== 200) {
|
|
261
205
|
sock.destroy();
|
|
262
206
|
reject(new ProxyRefusedError(res.statusCode ?? 0));
|
|
@@ -265,27 +209,28 @@ function connectVia(route, host, port) {
|
|
|
265
209
|
if (head.length) sock.unshift(head);
|
|
266
210
|
resolve(sock);
|
|
267
211
|
});
|
|
268
|
-
req.on(
|
|
212
|
+
req.on("response", (res) => {
|
|
269
213
|
res.resume();
|
|
270
214
|
reject(new ProxyRefusedError(res.statusCode ?? 0));
|
|
271
215
|
});
|
|
272
|
-
req.on(
|
|
216
|
+
req.on("error", reject);
|
|
273
217
|
});
|
|
274
218
|
req.end();
|
|
275
219
|
return { socket, abort: () => req.destroy() };
|
|
276
220
|
}
|
|
277
221
|
|
|
278
222
|
// src/client/ws-client.ts
|
|
279
|
-
import { createHash as createHash2, randomBytes } from
|
|
280
|
-
import http2 from
|
|
281
|
-
import https from
|
|
282
|
-
import { isIP } from
|
|
283
|
-
import { Duplex } from
|
|
284
|
-
import tls from
|
|
223
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
224
|
+
import http2 from "node:http";
|
|
225
|
+
import https from "node:https";
|
|
226
|
+
import { isIP } from "node:net";
|
|
227
|
+
import { Duplex } from "node:stream";
|
|
228
|
+
import tls from "node:tls";
|
|
285
229
|
|
|
286
230
|
// src/client/ws-frame.ts
|
|
287
231
|
var OP = { cont: 0, text: 1, binary: 2, close: 8, ping: 9, pong: 10 };
|
|
288
|
-
var WsProtocolError = class extends Error {
|
|
232
|
+
var WsProtocolError = class extends Error {
|
|
233
|
+
};
|
|
289
234
|
function encodeFrame(opcode, payload, mask, fin = true) {
|
|
290
235
|
const n = payload.length;
|
|
291
236
|
const b0 = (fin ? 128 : 0) | opcode;
|
|
@@ -313,13 +258,12 @@ var FrameParser = class {
|
|
|
313
258
|
push(chunk) {
|
|
314
259
|
this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk;
|
|
315
260
|
const out = [];
|
|
316
|
-
for (
|
|
261
|
+
for (; ; ) {
|
|
317
262
|
if (this.buf.length < 2) return out;
|
|
318
263
|
const b0 = this.buf[0];
|
|
319
264
|
const b1 = this.buf[1];
|
|
320
|
-
if (b0 & 112) throw new WsProtocolError(
|
|
321
|
-
if (b1 & 128)
|
|
322
|
-
throw new WsProtocolError('a server frame must not be masked');
|
|
265
|
+
if (b0 & 112) throw new WsProtocolError("reserved bits set");
|
|
266
|
+
if (b1 & 128) throw new WsProtocolError("a server frame must not be masked");
|
|
323
267
|
const opcode = b0 & 15;
|
|
324
268
|
const fin = (b0 & 128) !== 0;
|
|
325
269
|
let n = b1 & 127;
|
|
@@ -331,14 +275,12 @@ var FrameParser = class {
|
|
|
331
275
|
} else if (n === 127) {
|
|
332
276
|
if (this.buf.length < 10) return out;
|
|
333
277
|
const big = this.buf.readBigUInt64BE(2);
|
|
334
|
-
if (big > BigInt(this.maxPayload))
|
|
335
|
-
throw new WsProtocolError('frame too large');
|
|
278
|
+
if (big > BigInt(this.maxPayload)) throw new WsProtocolError("frame too large");
|
|
336
279
|
n = Number(big);
|
|
337
280
|
off = 10;
|
|
338
281
|
}
|
|
339
|
-
if (n > this.maxPayload) throw new WsProtocolError(
|
|
340
|
-
if (opcode >= 8 && (n > 125 || !fin))
|
|
341
|
-
throw new WsProtocolError('bad control frame');
|
|
282
|
+
if (n > this.maxPayload) throw new WsProtocolError("frame too large");
|
|
283
|
+
if (opcode >= 8 && (n > 125 || !fin)) throw new WsProtocolError("bad control frame");
|
|
342
284
|
if (this.buf.length < off + n) return out;
|
|
343
285
|
out.push({ fin, opcode, payload: this.buf.subarray(off, off + n) });
|
|
344
286
|
this.buf = this.buf.subarray(off + n);
|
|
@@ -349,27 +291,25 @@ var FrameParser = class {
|
|
|
349
291
|
// src/client/ws-client.ts
|
|
350
292
|
var TunnelBlockedError = class extends Error {
|
|
351
293
|
constructor(status, detail) {
|
|
352
|
-
super(
|
|
353
|
-
detail ? `tunnel blocked: ${detail}` : `tunnel blocked (HTTP ${status})`,
|
|
354
|
-
);
|
|
294
|
+
super(detail ? `tunnel blocked: ${detail}` : `tunnel blocked (HTTP ${status})`);
|
|
355
295
|
this.status = status;
|
|
356
296
|
}
|
|
357
297
|
status;
|
|
358
298
|
};
|
|
359
|
-
var GUID =
|
|
299
|
+
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
360
300
|
function defaultCa() {
|
|
361
301
|
const get = tls.getCACertificates;
|
|
362
302
|
if (!get) return void 0;
|
|
363
303
|
try {
|
|
364
|
-
return [...get(
|
|
304
|
+
return [...get("default"), ...get("system")];
|
|
365
305
|
} catch {
|
|
366
306
|
return void 0;
|
|
367
307
|
}
|
|
368
308
|
}
|
|
369
309
|
function openWebSocket(url, opts) {
|
|
370
|
-
const secure = url.protocol ===
|
|
371
|
-
const key = randomBytes(16).toString(
|
|
372
|
-
const hostname = url.hostname.replace(/^\[|\]$/g,
|
|
310
|
+
const secure = url.protocol === "https:";
|
|
311
|
+
const key = randomBytes(16).toString("base64");
|
|
312
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
373
313
|
const port = Number(url.port || (secure ? 443 : 80));
|
|
374
314
|
return new Promise((resolve, reject) => {
|
|
375
315
|
let done = false;
|
|
@@ -385,79 +325,48 @@ function openWebSocket(url, opts) {
|
|
|
385
325
|
const timer = setTimeout(() => {
|
|
386
326
|
connecting.abort?.();
|
|
387
327
|
req?.destroy();
|
|
388
|
-
settle(new Error(
|
|
328
|
+
settle(new Error("tunnel connect timeout"));
|
|
389
329
|
}, opts.timeoutMs ?? 15e3);
|
|
390
330
|
timer.unref();
|
|
391
331
|
const common = {
|
|
392
332
|
host: hostname,
|
|
393
333
|
port,
|
|
394
334
|
path: url.pathname,
|
|
395
|
-
method:
|
|
335
|
+
method: "GET",
|
|
396
336
|
headers: {
|
|
397
337
|
Host: url.host,
|
|
398
|
-
Connection:
|
|
399
|
-
Upgrade:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
...
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
const outerTls = {
|
|
407
|
-
...(isIP(hostname) ? {} : { servername: hostname }),
|
|
408
|
-
rejectUnauthorized: false,
|
|
409
|
-
ca: opts.ca ?? defaultCa(),
|
|
338
|
+
Connection: "Upgrade",
|
|
339
|
+
Upgrade: "websocket",
|
|
340
|
+
"Sec-WebSocket-Version": "13",
|
|
341
|
+
"Sec-WebSocket-Key": key,
|
|
342
|
+
"Sec-WebSocket-Protocol": opts.protocol,
|
|
343
|
+
...opts.userAgent ? { "User-Agent": opts.userAgent } : {}
|
|
344
|
+
}
|
|
410
345
|
};
|
|
346
|
+
const outerTls = { ...isIP(hostname) ? {} : { servername: hostname }, rejectUnauthorized: false, ca: opts.ca ?? defaultCa() };
|
|
411
347
|
const upgrade = (tunnelled) => {
|
|
412
348
|
if (done) {
|
|
413
349
|
tunnelled?.destroy();
|
|
414
350
|
return;
|
|
415
351
|
}
|
|
416
|
-
const r = tunnelled
|
|
417
|
-
? http2.request({
|
|
418
|
-
...common,
|
|
419
|
-
createConnection: () =>
|
|
420
|
-
secure
|
|
421
|
-
? tls.connect({ ...outerTls, socket: tunnelled })
|
|
422
|
-
: tunnelled,
|
|
423
|
-
})
|
|
424
|
-
: secure
|
|
425
|
-
? https.request({ ...common, ...outerTls, agent: false })
|
|
426
|
-
: http2.request({ ...common, agent: false });
|
|
352
|
+
const r = tunnelled ? http2.request({ ...common, createConnection: () => secure ? tls.connect({ ...outerTls, socket: tunnelled }) : tunnelled }) : secure ? https.request({ ...common, ...outerTls, agent: false }) : http2.request({ ...common, agent: false });
|
|
427
353
|
req = r;
|
|
428
|
-
r.on(
|
|
354
|
+
r.on("response", (res) => {
|
|
429
355
|
res.resume();
|
|
430
356
|
settle(new TunnelBlockedError(res.statusCode ?? 0));
|
|
431
357
|
});
|
|
432
|
-
r.on(
|
|
433
|
-
r.on(
|
|
434
|
-
const accept = createHash2(
|
|
435
|
-
|
|
436
|
-
.digest('base64');
|
|
437
|
-
if (
|
|
438
|
-
done ||
|
|
439
|
-
res.headers['sec-websocket-accept'] !== accept ||
|
|
440
|
-
res.headers['sec-websocket-protocol'] !== opts.protocol
|
|
441
|
-
) {
|
|
358
|
+
r.on("error", (err) => settle(err));
|
|
359
|
+
r.on("upgrade", (res, sock, head) => {
|
|
360
|
+
const accept = createHash2("sha1").update(key + GUID).digest("base64");
|
|
361
|
+
if (done || res.headers["sec-websocket-accept"] !== accept || res.headers["sec-websocket-protocol"] !== opts.protocol) {
|
|
442
362
|
sock.destroy();
|
|
443
363
|
settle(new TunnelBlockedError(101));
|
|
444
364
|
return;
|
|
445
365
|
}
|
|
446
366
|
const tlsSock = sock;
|
|
447
|
-
const outer = !secure
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
? 'verified'
|
|
451
|
-
: 'intercepted';
|
|
452
|
-
const outerError =
|
|
453
|
-
secure && !tlsSock.authorized && tlsSock.authorizationError
|
|
454
|
-
? String(tlsSock.authorizationError)
|
|
455
|
-
: void 0;
|
|
456
|
-
settle(null, {
|
|
457
|
-
stream: wrap(sock, head, opts.pingMs ?? 3e4),
|
|
458
|
-
outer,
|
|
459
|
-
...(outerError ? { outerError } : {}),
|
|
460
|
-
});
|
|
367
|
+
const outer = !secure ? "plain" : tlsSock.authorized ? "verified" : "intercepted";
|
|
368
|
+
const outerError = secure && !tlsSock.authorized && tlsSock.authorizationError ? String(tlsSock.authorizationError) : void 0;
|
|
369
|
+
settle(null, { stream: wrap(sock, head, opts.pingMs ?? 3e4), outer, ...outerError ? { outerError } : {} });
|
|
461
370
|
});
|
|
462
371
|
r.end();
|
|
463
372
|
};
|
|
@@ -474,13 +383,7 @@ function openWebSocket(url, opts) {
|
|
|
474
383
|
}
|
|
475
384
|
const via = connectVia(route, hostname, port);
|
|
476
385
|
connecting.abort = via.abort;
|
|
477
|
-
via.socket.then(upgrade, (err) =>
|
|
478
|
-
settle(
|
|
479
|
-
err instanceof ProxyRefusedError
|
|
480
|
-
? new TunnelBlockedError(err.status, err.message)
|
|
481
|
-
: err,
|
|
482
|
-
),
|
|
483
|
-
);
|
|
386
|
+
via.socket.then(upgrade, (err) => settle(err instanceof ProxyRefusedError ? new TunnelBlockedError(err.status, err.message) : err));
|
|
484
387
|
});
|
|
485
388
|
}
|
|
486
389
|
function wrap(sock, head, pingMs) {
|
|
@@ -508,7 +411,7 @@ function wrap(sock, head, pingMs) {
|
|
|
508
411
|
clearInterval(pinger);
|
|
509
412
|
sock.destroy();
|
|
510
413
|
cb(err);
|
|
511
|
-
}
|
|
414
|
+
}
|
|
512
415
|
});
|
|
513
416
|
const failFrame = (err) => {
|
|
514
417
|
setImmediate(() => duplex.destroy(err));
|
|
@@ -518,9 +421,7 @@ function wrap(sock, head, pingMs) {
|
|
|
518
421
|
try {
|
|
519
422
|
frames = parser.push(chunk);
|
|
520
423
|
} catch (err) {
|
|
521
|
-
failFrame(
|
|
522
|
-
err instanceof WsProtocolError ? err : new WsProtocolError(String(err)),
|
|
523
|
-
);
|
|
424
|
+
failFrame(err instanceof WsProtocolError ? err : new WsProtocolError(String(err)));
|
|
524
425
|
return;
|
|
525
426
|
}
|
|
526
427
|
for (const f of frames) {
|
|
@@ -536,14 +437,14 @@ function wrap(sock, head, pingMs) {
|
|
|
536
437
|
duplex.push(null);
|
|
537
438
|
sock.end();
|
|
538
439
|
} else if (f.opcode === OP.text) {
|
|
539
|
-
failFrame(new WsProtocolError(
|
|
440
|
+
failFrame(new WsProtocolError("text frame on a binary tunnel"));
|
|
540
441
|
}
|
|
541
442
|
}
|
|
542
443
|
};
|
|
543
|
-
sock.on(
|
|
544
|
-
sock.on(
|
|
545
|
-
sock.on(
|
|
546
|
-
sock.on(
|
|
444
|
+
sock.on("data", onData);
|
|
445
|
+
sock.on("end", () => duplex.push(null));
|
|
446
|
+
sock.on("error", (err) => duplex.destroy(err));
|
|
447
|
+
sock.on("close", () => {
|
|
547
448
|
clearInterval(pinger);
|
|
548
449
|
if (!duplex.destroyed) duplex.destroy();
|
|
549
450
|
});
|
|
@@ -556,7 +457,7 @@ function wrap(sock, head, pingMs) {
|
|
|
556
457
|
// src/client/poll-client.ts
|
|
557
458
|
var SessionGoneError = class extends Error {
|
|
558
459
|
constructor() {
|
|
559
|
-
super(
|
|
460
|
+
super("tunnel session gone");
|
|
560
461
|
}
|
|
561
462
|
};
|
|
562
463
|
var EMPTY = Buffer.alloc(0);
|
|
@@ -566,34 +467,22 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
566
467
|
function isBusy(r) {
|
|
567
468
|
if (r.status !== 503 || r.body.length > 4096) return false;
|
|
568
469
|
try {
|
|
569
|
-
return JSON.parse(r.body.toString(
|
|
470
|
+
return JSON.parse(r.body.toString("utf8")).error?.type === TUNNEL_BUSY;
|
|
570
471
|
} catch {
|
|
571
472
|
return false;
|
|
572
473
|
}
|
|
573
474
|
}
|
|
574
475
|
function settings(o) {
|
|
575
|
-
return {
|
|
576
|
-
timeoutMs: o.timeoutMs ?? 15e3,
|
|
577
|
-
waitMs: o.waitMs ?? POLL_WAIT_MS,
|
|
578
|
-
retryForMs: o.retryForMs ?? 3e4,
|
|
579
|
-
busyForMs: o.busyForMs ?? 3e5,
|
|
580
|
-
coalesceMs: o.coalesceMs ?? 5,
|
|
581
|
-
};
|
|
476
|
+
return { timeoutMs: o.timeoutMs ?? 15e3, waitMs: o.waitMs ?? POLL_WAIT_MS, retryForMs: o.retryForMs ?? 3e4, busyForMs: o.busyForMs ?? 3e5, coalesceMs: o.coalesceMs ?? 5 };
|
|
582
477
|
}
|
|
583
478
|
function makeExchanger(base, o) {
|
|
584
|
-
const secure = base.protocol ===
|
|
585
|
-
const hostname = base.hostname.replace(/^\[|\]$/g,
|
|
479
|
+
const secure = base.protocol === "https:";
|
|
480
|
+
const hostname = base.hostname.replace(/^\[|\]$/g, "");
|
|
586
481
|
const port = Number(base.port || (secure ? 443 : 80));
|
|
587
|
-
const outerTls = {
|
|
588
|
-
...(isIP2(hostname) ? {} : { servername: hostname }),
|
|
589
|
-
rejectUnauthorized: false,
|
|
590
|
-
ca: o.ca ?? defaultCa(),
|
|
591
|
-
};
|
|
482
|
+
const outerTls = { ...isIP2(hostname) ? {} : { servername: hostname }, rejectUnauthorized: false, ca: o.ca ?? defaultCa() };
|
|
592
483
|
const route = proxyFor(base, o.env ?? process.env);
|
|
593
484
|
const connectMs = o.timeoutMs ?? 15e3;
|
|
594
|
-
const agent = secure
|
|
595
|
-
? new https2.Agent({ keepAlive: true, maxSockets: 2, ...outerTls })
|
|
596
|
-
: new http3.Agent({ keepAlive: true, maxSockets: 2 });
|
|
485
|
+
const agent = secure ? new https2.Agent({ keepAlive: true, maxSockets: 2, ...outerTls }) : new http3.Agent({ keepAlive: true, maxSockets: 2 });
|
|
597
486
|
if (route) {
|
|
598
487
|
agent.createConnection = (_opts, cb) => {
|
|
599
488
|
const via = connectVia(route, hostname, port);
|
|
@@ -608,80 +497,51 @@ function makeExchanger(base, o) {
|
|
|
608
497
|
cb(err, sock);
|
|
609
498
|
};
|
|
610
499
|
const timer = setTimeout(() => {
|
|
611
|
-
once(new Error(
|
|
500
|
+
once(new Error("tunnel connect timeout"));
|
|
612
501
|
via.abort();
|
|
613
502
|
}, connectMs);
|
|
614
503
|
via.socket.then(
|
|
615
|
-
(sock) =>
|
|
616
|
-
|
|
617
|
-
null,
|
|
618
|
-
secure ? tls2.connect({ ...outerTls, socket: sock }) : sock,
|
|
619
|
-
),
|
|
620
|
-
(err) =>
|
|
621
|
-
once(
|
|
622
|
-
err instanceof ProxyRefusedError
|
|
623
|
-
? new TunnelBlockedError(err.status, err.message)
|
|
624
|
-
: err,
|
|
625
|
-
),
|
|
504
|
+
(sock) => once(null, secure ? tls2.connect({ ...outerTls, socket: sock }) : sock),
|
|
505
|
+
(err) => once(err instanceof ProxyRefusedError ? new TunnelBlockedError(err.status, err.message) : err)
|
|
626
506
|
);
|
|
627
507
|
return void 0;
|
|
628
508
|
};
|
|
629
509
|
}
|
|
630
|
-
const post = (body, timeoutMs) =>
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
res.on('data', (c) => {
|
|
661
|
-
size += c.length;
|
|
662
|
-
if (size > MAX_ANSWER) {
|
|
663
|
-
req.destroy(new Error('tunnel answer too large'));
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
chunks.push(c);
|
|
667
|
-
});
|
|
668
|
-
res.on('end', () =>
|
|
669
|
-
resolve({
|
|
670
|
-
status: res.statusCode ?? 0,
|
|
671
|
-
body: Buffer.concat(chunks),
|
|
672
|
-
outer,
|
|
673
|
-
...(outerError ? { outerError } : {}),
|
|
674
|
-
}),
|
|
675
|
-
);
|
|
676
|
-
res.on('error', reject);
|
|
677
|
-
},
|
|
678
|
-
);
|
|
679
|
-
req.setTimeout(timeoutMs, () =>
|
|
680
|
-
req.destroy(new Error('tunnel exchange timeout')),
|
|
681
|
-
);
|
|
682
|
-
req.on('error', reject);
|
|
683
|
-
req.end(body);
|
|
510
|
+
const post = (body, timeoutMs) => new Promise((resolve, reject) => {
|
|
511
|
+
const req = (secure ? https2 : http3).request({
|
|
512
|
+
host: hostname,
|
|
513
|
+
port,
|
|
514
|
+
path: POLL_PATH,
|
|
515
|
+
method: "POST",
|
|
516
|
+
agent,
|
|
517
|
+
...secure ? outerTls : {},
|
|
518
|
+
headers: {
|
|
519
|
+
Host: base.host,
|
|
520
|
+
"Content-Type": "application/octet-stream",
|
|
521
|
+
"Content-Length": String(body.length),
|
|
522
|
+
...o.userAgent ? { "User-Agent": o.userAgent } : {}
|
|
523
|
+
}
|
|
524
|
+
}, (res) => {
|
|
525
|
+
const sock = res.socket;
|
|
526
|
+
const outer = !secure ? "plain" : sock?.authorized ? "verified" : "intercepted";
|
|
527
|
+
const outerError = secure && !sock?.authorized && sock?.authorizationError ? String(sock.authorizationError) : void 0;
|
|
528
|
+
const chunks = [];
|
|
529
|
+
let size = 0;
|
|
530
|
+
res.on("data", (c) => {
|
|
531
|
+
size += c.length;
|
|
532
|
+
if (size > MAX_ANSWER) {
|
|
533
|
+
req.destroy(new Error("tunnel answer too large"));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
chunks.push(c);
|
|
537
|
+
});
|
|
538
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks), outer, ...outerError ? { outerError } : {} }));
|
|
539
|
+
res.on("error", reject);
|
|
684
540
|
});
|
|
541
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error("tunnel exchange timeout")));
|
|
542
|
+
req.on("error", reject);
|
|
543
|
+
req.end(body);
|
|
544
|
+
});
|
|
685
545
|
return { post, destroy: () => agent.destroy() };
|
|
686
546
|
}
|
|
687
547
|
var PollStream = class {
|
|
@@ -706,7 +566,7 @@ var PollStream = class {
|
|
|
706
566
|
destroy: (err, cb) => {
|
|
707
567
|
this.shutdown(true);
|
|
708
568
|
cb(err);
|
|
709
|
-
}
|
|
569
|
+
}
|
|
710
570
|
});
|
|
711
571
|
void this.downLoop();
|
|
712
572
|
}
|
|
@@ -733,24 +593,11 @@ var PollStream = class {
|
|
|
733
593
|
await delay(this.o.coalesceMs);
|
|
734
594
|
while (!this.closed && this.pendingSize > 0) {
|
|
735
595
|
const data = this.takePending();
|
|
736
|
-
await this.exchange(
|
|
737
|
-
{
|
|
738
|
-
session: this.session,
|
|
739
|
-
upOffset: this.sent,
|
|
740
|
-
ack: this.received,
|
|
741
|
-
close: false,
|
|
742
|
-
poll: false,
|
|
743
|
-
open: false,
|
|
744
|
-
data,
|
|
745
|
-
},
|
|
746
|
-
this.o.timeoutMs,
|
|
747
|
-
);
|
|
596
|
+
await this.exchange({ session: this.session, upOffset: this.sent, ack: this.received, close: false, poll: false, open: false, data }, this.o.timeoutMs);
|
|
748
597
|
this.sent += data.length;
|
|
749
|
-
while (this.held.length > 0 && this.pendingSize <= MAX_UP)
|
|
750
|
-
this.held.shift()();
|
|
598
|
+
while (this.held.length > 0 && this.pendingSize <= MAX_UP) this.held.shift()();
|
|
751
599
|
}
|
|
752
|
-
if (this.finishing && !this.closed && this.pendingSize === 0)
|
|
753
|
-
this.shutdown(true);
|
|
600
|
+
if (this.finishing && !this.closed && this.pendingSize === 0) this.shutdown(true);
|
|
754
601
|
} catch (err) {
|
|
755
602
|
this.fail(err);
|
|
756
603
|
} finally {
|
|
@@ -769,21 +616,9 @@ var PollStream = class {
|
|
|
769
616
|
async downLoop() {
|
|
770
617
|
try {
|
|
771
618
|
while (!this.closed) {
|
|
772
|
-
const r = await this.exchange(
|
|
773
|
-
{
|
|
774
|
-
session: this.session,
|
|
775
|
-
upOffset: this.sent,
|
|
776
|
-
ack: this.received,
|
|
777
|
-
close: false,
|
|
778
|
-
poll: true,
|
|
779
|
-
open: false,
|
|
780
|
-
data: EMPTY,
|
|
781
|
-
},
|
|
782
|
-
this.o.waitMs + this.o.timeoutMs,
|
|
783
|
-
);
|
|
619
|
+
const r = await this.exchange({ session: this.session, upOffset: this.sent, ack: this.received, close: false, poll: true, open: false, data: EMPTY }, this.o.waitMs + this.o.timeoutMs);
|
|
784
620
|
if (this.closed) return;
|
|
785
|
-
if (r.downOffset > this.received)
|
|
786
|
-
throw new Error('tunnel stream out of order');
|
|
621
|
+
if (r.downOffset > this.received) throw new Error("tunnel stream out of order");
|
|
787
622
|
const skip = this.received - r.downOffset;
|
|
788
623
|
if (skip < r.data.length) {
|
|
789
624
|
const fresh = Buffer.from(r.data.subarray(skip));
|
|
@@ -805,8 +640,8 @@ var PollStream = class {
|
|
|
805
640
|
let firstFailure = 0;
|
|
806
641
|
let firstBusy = 0;
|
|
807
642
|
let wait = 250;
|
|
808
|
-
for (
|
|
809
|
-
if (this.closed) throw new Error(
|
|
643
|
+
for (; ; ) {
|
|
644
|
+
if (this.closed) throw new Error("tunnel closed");
|
|
810
645
|
let failure;
|
|
811
646
|
let busy = false;
|
|
812
647
|
try {
|
|
@@ -817,10 +652,8 @@ var PollStream = class {
|
|
|
817
652
|
if (FATAL_STATUS.has(r.status)) throw failure;
|
|
818
653
|
busy = isBusy(r);
|
|
819
654
|
} catch (err) {
|
|
820
|
-
if (err instanceof SessionGoneError || err instanceof PollFormatError)
|
|
821
|
-
|
|
822
|
-
if (err instanceof TunnelBlockedError && FATAL_STATUS.has(err.status))
|
|
823
|
-
throw err;
|
|
655
|
+
if (err instanceof SessionGoneError || err instanceof PollFormatError) throw err;
|
|
656
|
+
if (err instanceof TunnelBlockedError && FATAL_STATUS.has(err.status)) throw err;
|
|
824
657
|
failure = err;
|
|
825
658
|
}
|
|
826
659
|
if (busy) {
|
|
@@ -849,19 +682,8 @@ var PollStream = class {
|
|
|
849
682
|
if (this.closed) return;
|
|
850
683
|
this.closed = true;
|
|
851
684
|
if (sendClose) {
|
|
852
|
-
const bye = encodeRequest({
|
|
853
|
-
|
|
854
|
-
upOffset: this.sent,
|
|
855
|
-
ack: this.received,
|
|
856
|
-
close: true,
|
|
857
|
-
poll: false,
|
|
858
|
-
open: false,
|
|
859
|
-
data: EMPTY,
|
|
860
|
-
});
|
|
861
|
-
this.ex
|
|
862
|
-
.post(bye, 5e3)
|
|
863
|
-
.catch(() => void 0)
|
|
864
|
-
.finally(() => this.ex.destroy());
|
|
685
|
+
const bye = encodeRequest({ session: this.session, upOffset: this.sent, ack: this.received, close: true, poll: false, open: false, data: EMPTY });
|
|
686
|
+
this.ex.post(bye, 5e3).catch(() => void 0).finally(() => this.ex.destroy());
|
|
865
687
|
} else {
|
|
866
688
|
this.ex.destroy();
|
|
867
689
|
}
|
|
@@ -871,15 +693,7 @@ var PollStream = class {
|
|
|
871
693
|
async function openPollStream(base, opts = {}) {
|
|
872
694
|
const ex = makeExchanger(base, opts);
|
|
873
695
|
const session = randomBytes2(SESSION_BYTES);
|
|
874
|
-
const open2 = encodeRequest({
|
|
875
|
-
session,
|
|
876
|
-
upOffset: 0,
|
|
877
|
-
ack: 0,
|
|
878
|
-
close: false,
|
|
879
|
-
poll: false,
|
|
880
|
-
open: true,
|
|
881
|
-
data: EMPTY,
|
|
882
|
-
});
|
|
696
|
+
const open2 = encodeRequest({ session, upOffset: 0, ack: 0, close: false, poll: false, open: true, data: EMPTY });
|
|
883
697
|
const o = settings(opts);
|
|
884
698
|
let first;
|
|
885
699
|
let firstFailure = 0;
|
|
@@ -890,7 +704,7 @@ async function openPollStream(base, opts = {}) {
|
|
|
890
704
|
} catch (err) {
|
|
891
705
|
const t = Date.now();
|
|
892
706
|
if (firstFailure === 0) firstFailure = t;
|
|
893
|
-
if (err.code !==
|
|
707
|
+
if (err.code !== "ECONNRESET" || t - firstFailure >= o.retryForMs) {
|
|
894
708
|
ex.destroy();
|
|
895
709
|
throw err;
|
|
896
710
|
}
|
|
@@ -905,62 +719,35 @@ async function openPollStream(base, opts = {}) {
|
|
|
905
719
|
let answered;
|
|
906
720
|
try {
|
|
907
721
|
answered = decodeResponse(first.body).session;
|
|
908
|
-
} catch {
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
909
724
|
if (!answered?.equals(session)) {
|
|
910
725
|
ex.destroy();
|
|
911
|
-
throw new TunnelBlockedError(
|
|
912
|
-
200,
|
|
913
|
-
"the answer is not the tunnel's (a filter's page?)",
|
|
914
|
-
);
|
|
726
|
+
throw new TunnelBlockedError(200, "the answer is not the tunnel's (a filter's page?)");
|
|
915
727
|
}
|
|
916
728
|
const stream = new PollStream(ex, session, opts).duplex;
|
|
917
|
-
return {
|
|
918
|
-
stream,
|
|
919
|
-
outer: first.outer,
|
|
920
|
-
...(first.outerError ? { outerError: first.outerError } : {}),
|
|
921
|
-
};
|
|
729
|
+
return { stream, outer: first.outer, ...first.outerError ? { outerError: first.outerError } : {} };
|
|
922
730
|
}
|
|
923
731
|
|
|
924
732
|
// src/client/inner.ts
|
|
925
733
|
var IdentityMismatchError = class extends Error {
|
|
926
734
|
constructor(received) {
|
|
927
|
-
super(
|
|
735
|
+
super("tunnel identity mismatch");
|
|
928
736
|
this.received = received;
|
|
929
737
|
}
|
|
930
738
|
received;
|
|
931
739
|
};
|
|
932
740
|
async function connectInner(base, pins, opts = {}) {
|
|
933
|
-
const outerOpts = {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
userAgent: opts.userAgent,
|
|
937
|
-
env: opts.env,
|
|
938
|
-
};
|
|
939
|
-
const ws =
|
|
940
|
-
opts.transport === 'post'
|
|
941
|
-
? await openPollStream(base, outerOpts)
|
|
942
|
-
: await openWebSocket(new URL(TUNNEL_PATH, base), {
|
|
943
|
-
protocol: TUNNEL_PROTOCOL,
|
|
944
|
-
...outerOpts,
|
|
945
|
-
});
|
|
946
|
-
opts.onOuter?.({
|
|
947
|
-
outer: ws.outer,
|
|
948
|
-
...(ws.outerError ? { outerError: ws.outerError } : {}),
|
|
949
|
-
});
|
|
741
|
+
const outerOpts = { timeoutMs: opts.timeoutMs, ca: opts.ca, userAgent: opts.userAgent, env: opts.env };
|
|
742
|
+
const ws = opts.transport === "post" ? await openPollStream(base, outerOpts) : await openWebSocket(new URL(TUNNEL_PATH, base), { protocol: TUNNEL_PROTOCOL, ...outerOpts });
|
|
743
|
+
opts.onOuter?.({ outer: ws.outer, ...ws.outerError ? { outerError: ws.outerError } : {} });
|
|
950
744
|
const preferred = pins.at(-1);
|
|
951
|
-
const servername =
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
rejectUnauthorized: false,
|
|
958
|
-
ALPNProtocols: ['http/1.1'],
|
|
959
|
-
});
|
|
960
|
-
ws.stream.on('close', () => socket.destroy());
|
|
961
|
-
ws.stream.on('end', () => socket.destroy());
|
|
962
|
-
ws.stream.on('error', () => socket.destroy());
|
|
963
|
-
socket.on('close', () => ws.stream.destroy());
|
|
745
|
+
const servername = preferred && isPin(preferred) ? sniOfPin(preferred) : TUNNEL_SERVERNAME;
|
|
746
|
+
const socket = tls3.connect({ socket: ws.stream, servername, minVersion: "TLSv1.3", rejectUnauthorized: false, ALPNProtocols: ["http/1.1"] });
|
|
747
|
+
ws.stream.on("close", () => socket.destroy());
|
|
748
|
+
ws.stream.on("end", () => socket.destroy());
|
|
749
|
+
ws.stream.on("error", () => socket.destroy());
|
|
750
|
+
socket.on("close", () => ws.stream.destroy());
|
|
964
751
|
try {
|
|
965
752
|
await new Promise((resolve, reject) => {
|
|
966
753
|
let settled = false;
|
|
@@ -968,37 +755,32 @@ async function connectInner(base, pins, opts = {}) {
|
|
|
968
755
|
if (settled) return;
|
|
969
756
|
settled = true;
|
|
970
757
|
clearTimeout(timer);
|
|
971
|
-
socket.off(
|
|
972
|
-
socket.off(
|
|
758
|
+
socket.off("close", onClose);
|
|
759
|
+
socket.off("secureConnect", onSecure);
|
|
973
760
|
if (err === null) {
|
|
974
|
-
socket.off(
|
|
761
|
+
socket.off("error", onError);
|
|
975
762
|
resolve();
|
|
976
763
|
return;
|
|
977
764
|
}
|
|
978
|
-
socket.once(
|
|
765
|
+
socket.once("close", () => socket.off("error", onError));
|
|
979
766
|
reject(err);
|
|
980
767
|
};
|
|
981
768
|
const onError = (err) => finish(err);
|
|
982
|
-
const onClose = () => finish(new Error(
|
|
769
|
+
const onClose = () => finish(new Error("tunnel closed during handshake"));
|
|
983
770
|
const onSecure = () => finish(null);
|
|
984
|
-
const timer = setTimeout(
|
|
985
|
-
() => finish(new Error('tunnel handshake timeout')),
|
|
986
|
-
opts.timeoutMs ?? 15e3,
|
|
987
|
-
);
|
|
771
|
+
const timer = setTimeout(() => finish(new Error("tunnel handshake timeout")), opts.timeoutMs ?? 15e3);
|
|
988
772
|
timer.unref();
|
|
989
|
-
socket.on(
|
|
990
|
-
socket.on(
|
|
991
|
-
socket.on(
|
|
773
|
+
socket.on("error", onError);
|
|
774
|
+
socket.on("close", onClose);
|
|
775
|
+
socket.on("secureConnect", onSecure);
|
|
992
776
|
});
|
|
993
777
|
} catch (err) {
|
|
994
778
|
socket.destroy();
|
|
995
779
|
throw err;
|
|
996
780
|
}
|
|
997
|
-
socket.on(
|
|
781
|
+
socket.on("error", () => socket.destroy());
|
|
998
782
|
const peer = peerCertificate(socket);
|
|
999
|
-
const received = peer
|
|
1000
|
-
? pinOfSpki(peer.publicKey.export({ type: 'spki', format: 'der' }))
|
|
1001
|
-
: 'none';
|
|
783
|
+
const received = peer ? pinOfSpki(peer.publicKey.export({ type: "spki", format: "der" })) : "none";
|
|
1002
784
|
if (!pins.includes(received)) {
|
|
1003
785
|
socket.destroy();
|
|
1004
786
|
throw new IdentityMismatchError(received);
|
|
@@ -1007,36 +789,24 @@ async function connectInner(base, pins, opts = {}) {
|
|
|
1007
789
|
}
|
|
1008
790
|
|
|
1009
791
|
// src/client/local-server.ts
|
|
1010
|
-
import {
|
|
1011
|
-
|
|
1012
|
-
randomBytes as randomBytes3,
|
|
1013
|
-
timingSafeEqual,
|
|
1014
|
-
} from 'node:crypto';
|
|
1015
|
-
import http5 from 'node:http';
|
|
792
|
+
import { createHash as createHash3, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
793
|
+
import http5 from "node:http";
|
|
1016
794
|
|
|
1017
795
|
// src/client/tunnel-agent.ts
|
|
1018
|
-
import http4 from
|
|
796
|
+
import http4 from "node:http";
|
|
1019
797
|
var TUNNEL_AGENT_MAX_SOCKETS = 4;
|
|
1020
798
|
function canSwitch(err) {
|
|
1021
|
-
if (err instanceof IdentityMismatchError || err instanceof BackoffError)
|
|
1022
|
-
|
|
1023
|
-
if (
|
|
1024
|
-
err instanceof TunnelBlockedError &&
|
|
1025
|
-
(err.status === 429 || err.status === 503)
|
|
1026
|
-
)
|
|
1027
|
-
return false;
|
|
799
|
+
if (err instanceof IdentityMismatchError || err instanceof BackoffError) return false;
|
|
800
|
+
if (err instanceof TunnelBlockedError && (err.status === 429 || err.status === 503)) return false;
|
|
1028
801
|
return true;
|
|
1029
802
|
}
|
|
1030
803
|
function reasonOf(err) {
|
|
1031
|
-
if (err instanceof TunnelBlockedError)
|
|
1032
|
-
return err.status === 101 || err.status === 200
|
|
1033
|
-
? err.message
|
|
1034
|
-
: `HTTP ${err.status}`;
|
|
804
|
+
if (err instanceof TunnelBlockedError) return err.status === 101 || err.status === 200 ? err.message : `HTTP ${err.status}`;
|
|
1035
805
|
return err instanceof Error ? err.message : String(err);
|
|
1036
806
|
}
|
|
1037
807
|
var BackoffError = class extends Error {
|
|
1038
808
|
constructor(waitMs) {
|
|
1039
|
-
super(
|
|
809
|
+
super("tunnel identity mismatch - waiting before retry");
|
|
1040
810
|
this.waitMs = waitMs;
|
|
1041
811
|
}
|
|
1042
812
|
waitMs;
|
|
@@ -1044,19 +814,16 @@ var BackoffError = class extends Error {
|
|
|
1044
814
|
var baseAddRequest = http4.Agent.prototype.addRequest;
|
|
1045
815
|
var TunnelAgent = class extends http4.Agent {
|
|
1046
816
|
constructor(target, backoff, events = {}) {
|
|
1047
|
-
super({
|
|
1048
|
-
keepAlive: true,
|
|
1049
|
-
maxSockets: TUNNEL_AGENT_MAX_SOCKETS,
|
|
1050
|
-
maxFreeSockets: TUNNEL_AGENT_MAX_SOCKETS,
|
|
1051
|
-
});
|
|
817
|
+
super({ keepAlive: true, maxSockets: TUNNEL_AGENT_MAX_SOCKETS, maxFreeSockets: TUNNEL_AGENT_MAX_SOCKETS });
|
|
1052
818
|
this.target = target;
|
|
1053
819
|
this.backoff = backoff;
|
|
1054
820
|
this.events = events;
|
|
1055
|
-
this.mode = target.transport ??
|
|
1056
|
-
this.using = this.mode ===
|
|
821
|
+
this.mode = target.transport ?? "auto";
|
|
822
|
+
this.using = this.mode === "post" ? "post" : "ws";
|
|
1057
823
|
this.connect = target.connect ?? connectInner;
|
|
1058
824
|
this.pins = [...target.pins];
|
|
1059
|
-
this.
|
|
825
|
+
this.targets = [target.url, ...target.urls ?? []];
|
|
826
|
+
this.on("free", () => this.drain());
|
|
1060
827
|
}
|
|
1061
828
|
target;
|
|
1062
829
|
backoff;
|
|
@@ -1069,6 +836,9 @@ var TunnelAgent = class extends http4.Agent {
|
|
|
1069
836
|
mode;
|
|
1070
837
|
connect;
|
|
1071
838
|
pins;
|
|
839
|
+
/** Every host the code names, first one first; `preferred` indexes it. */
|
|
840
|
+
targets;
|
|
841
|
+
preferred = 0;
|
|
1072
842
|
/** Destroyed: no tunnel is opened any more, and one still opening is closed when it arrives. */
|
|
1073
843
|
closed = false;
|
|
1074
844
|
/**
|
|
@@ -1089,9 +859,7 @@ var TunnelAgent = class extends http4.Agent {
|
|
|
1089
859
|
return n;
|
|
1090
860
|
}
|
|
1091
861
|
hasFree() {
|
|
1092
|
-
return Object.values(this.freeSockets).some((list) =>
|
|
1093
|
-
list?.some((s) => !s.destroyed),
|
|
1094
|
-
);
|
|
862
|
+
return Object.values(this.freeSockets).some((list) => list?.some((s) => !s.destroyed));
|
|
1095
863
|
}
|
|
1096
864
|
canServe() {
|
|
1097
865
|
return this.hasFree() || this.live() < TUNNEL_AGENT_MAX_SOCKETS;
|
|
@@ -1100,7 +868,7 @@ var TunnelAgent = class extends http4.Agent {
|
|
|
1100
868
|
addRequest(req, options) {
|
|
1101
869
|
if (!this.canServe()) {
|
|
1102
870
|
this.waiting.push({ req, options });
|
|
1103
|
-
req.once(
|
|
871
|
+
req.once("close", () => {
|
|
1104
872
|
const i = this.waiting.findIndex((w) => w.req === req);
|
|
1105
873
|
if (i >= 0) this.waiting.splice(i, 1);
|
|
1106
874
|
});
|
|
@@ -1122,9 +890,9 @@ var TunnelAgent = class extends http4.Agent {
|
|
|
1122
890
|
destroy() {
|
|
1123
891
|
this.closed = true;
|
|
1124
892
|
for (const { req } of this.waiting.splice(0)) {
|
|
1125
|
-
const err = new Error(
|
|
893
|
+
const err = new Error("tunnel agent closed");
|
|
1126
894
|
req.destroy(err);
|
|
1127
|
-
process.nextTick(() => req.emit(
|
|
895
|
+
process.nextTick(() => req.emit("error", err));
|
|
1128
896
|
}
|
|
1129
897
|
super.destroy();
|
|
1130
898
|
}
|
|
@@ -1133,7 +901,7 @@ var TunnelAgent = class extends http4.Agent {
|
|
|
1133
901
|
createConnection(_options, callback) {
|
|
1134
902
|
const done = callback ?? (() => void 0);
|
|
1135
903
|
if (this.closed) {
|
|
1136
|
-
done(new Error(
|
|
904
|
+
done(new Error("tunnel agent closed"), void 0);
|
|
1137
905
|
return void 0;
|
|
1138
906
|
}
|
|
1139
907
|
const wait = this.backoff.blockedMs();
|
|
@@ -1142,143 +910,109 @@ var TunnelAgent = class extends http4.Agent {
|
|
|
1142
910
|
return void 0;
|
|
1143
911
|
}
|
|
1144
912
|
this.opening += 1;
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const first = this.using;
|
|
1154
|
-
attempt(first)
|
|
1155
|
-
.catch((err) => {
|
|
1156
|
-
if (this.mode !== 'auto' || first !== 'ws' || !canSwitch(err))
|
|
1157
|
-
throw err;
|
|
1158
|
-
return attempt('post').then(
|
|
1159
|
-
(socket) => {
|
|
1160
|
-
if (this.using !== 'post') {
|
|
1161
|
-
this.using = 'post';
|
|
1162
|
-
this.events.onTransport?.(
|
|
1163
|
-
'post',
|
|
1164
|
-
`websocket blocked: ${reasonOf(err)}`,
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
|
-
return socket;
|
|
1168
|
-
},
|
|
1169
|
-
(postErr) => {
|
|
1170
|
-
throw postErr instanceof IdentityMismatchError ? postErr : err;
|
|
1171
|
-
},
|
|
1172
|
-
);
|
|
1173
|
-
})
|
|
1174
|
-
.then(
|
|
913
|
+
const dial = (host, transport) => this.connect(this.targets[host], this.pins, { ca: this.target.ca, userAgent: this.target.userAgent, env: this.target.env, transport, onOuter: (i) => this.events.onOuter?.(i) });
|
|
914
|
+
const hosts = this.targets.map((_, i) => (this.preferred + i) % this.targets.length);
|
|
915
|
+
const transports = this.mode === "auto" ? this.using === "ws" ? ["ws", "post"] : ["post"] : [this.using === "post" ? "post" : "ws"];
|
|
916
|
+
const chain = transports.flatMap((transport) => hosts.map((host) => ({ host, transport })));
|
|
917
|
+
let firstErr = null;
|
|
918
|
+
const run = (i) => {
|
|
919
|
+
const at = chain[i];
|
|
920
|
+
return dial(at.host, at.transport).then(
|
|
1175
921
|
(socket) => {
|
|
1176
|
-
this.
|
|
1177
|
-
if (this.
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
return;
|
|
922
|
+
if (this.preferred !== at.host) this.preferred = at.host;
|
|
923
|
+
if (this.mode === "auto" && this.using !== "post" && at.transport === "post") {
|
|
924
|
+
this.using = "post";
|
|
925
|
+
this.events.onTransport?.("post", firstErr === null ? null : `websocket blocked: ${reasonOf(firstErr)}`);
|
|
1181
926
|
}
|
|
1182
|
-
|
|
1183
|
-
this.events.onConnected?.();
|
|
1184
|
-
socket.once('close', () => process.nextTick(() => this.drain()));
|
|
1185
|
-
done(null, socket);
|
|
927
|
+
return socket;
|
|
1186
928
|
},
|
|
1187
929
|
(err) => {
|
|
1188
|
-
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
done(err, void 0);
|
|
1194
|
-
this.drain();
|
|
1195
|
-
},
|
|
930
|
+
if (!(err instanceof Error)) throw err;
|
|
931
|
+
if (firstErr === null) firstErr = err;
|
|
932
|
+
if (i + 1 < chain.length && canSwitch(err)) return run(i + 1);
|
|
933
|
+
throw err instanceof IdentityMismatchError ? err : firstErr;
|
|
934
|
+
}
|
|
1196
935
|
);
|
|
936
|
+
};
|
|
937
|
+
run(0).then(
|
|
938
|
+
(socket) => {
|
|
939
|
+
this.opening -= 1;
|
|
940
|
+
if (this.closed) {
|
|
941
|
+
socket.destroy();
|
|
942
|
+
done(new Error("tunnel agent closed"), void 0);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
this.backoff.success();
|
|
946
|
+
this.events.onConnected?.();
|
|
947
|
+
socket.once("close", () => process.nextTick(() => this.drain()));
|
|
948
|
+
done(null, socket);
|
|
949
|
+
},
|
|
950
|
+
(err) => {
|
|
951
|
+
this.opening -= 1;
|
|
952
|
+
if (err instanceof IdentityMismatchError) {
|
|
953
|
+
this.backoff.failure();
|
|
954
|
+
this.events.onMismatch?.(err.received);
|
|
955
|
+
}
|
|
956
|
+
done(err, void 0);
|
|
957
|
+
this.drain();
|
|
958
|
+
}
|
|
959
|
+
);
|
|
1197
960
|
return void 0;
|
|
1198
961
|
}
|
|
1199
962
|
};
|
|
1200
963
|
|
|
1201
964
|
// src/client/local-server.ts
|
|
1202
965
|
function newLocalKey() {
|
|
1203
|
-
return `o1gwl_${randomBytes3(32).toString(
|
|
966
|
+
return `o1gwl_${randomBytes3(32).toString("base64url")}`;
|
|
1204
967
|
}
|
|
1205
968
|
function hashLocalKey(key) {
|
|
1206
|
-
return createHash3(
|
|
969
|
+
return createHash3("sha256").update(key, "utf8").digest("hex");
|
|
1207
970
|
}
|
|
1208
|
-
var HOP = /* @__PURE__ */ new Set([
|
|
1209
|
-
|
|
1210
|
-
'keep-alive',
|
|
1211
|
-
'proxy-authorization',
|
|
1212
|
-
'proxy-connection',
|
|
1213
|
-
'te',
|
|
1214
|
-
'trailer',
|
|
1215
|
-
'transfer-encoding',
|
|
1216
|
-
'upgrade',
|
|
1217
|
-
'host',
|
|
1218
|
-
'authorization',
|
|
1219
|
-
'x-api-key',
|
|
1220
|
-
]);
|
|
1221
|
-
var LOOPBACK2 = /* @__PURE__ */ new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
971
|
+
var HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-authorization", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade", "host", "authorization", "x-api-key"]);
|
|
972
|
+
var LOOPBACK2 = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
1222
973
|
function send(res, status, message) {
|
|
1223
|
-
const body = JSON.stringify({
|
|
1224
|
-
|
|
1225
|
-
error: { type: 'tunnel_error', message },
|
|
1226
|
-
});
|
|
1227
|
-
res
|
|
1228
|
-
.writeHead(status, {
|
|
1229
|
-
'content-type': 'application/json',
|
|
1230
|
-
'content-length': Buffer.byteLength(body),
|
|
1231
|
-
})
|
|
1232
|
-
.end(body);
|
|
974
|
+
const body = JSON.stringify({ type: "error", error: { type: "tunnel_error", message } });
|
|
975
|
+
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) }).end(body);
|
|
1233
976
|
}
|
|
1234
977
|
function keyOf(req) {
|
|
1235
|
-
const bearer = /^Bearer\s+(.+)$/i.exec(
|
|
1236
|
-
String(req.headers.authorization ?? '').trim(),
|
|
1237
|
-
);
|
|
978
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(String(req.headers.authorization ?? "").trim());
|
|
1238
979
|
if (bearer) return bearer[1].trim();
|
|
1239
|
-
const k = req.headers[
|
|
1240
|
-
return typeof k ===
|
|
980
|
+
const k = req.headers["x-api-key"];
|
|
981
|
+
return typeof k === "string" && k.trim() ? k.trim() : void 0;
|
|
1241
982
|
}
|
|
1242
983
|
function sameHash(a, b) {
|
|
1243
|
-
const x = Buffer.from(a,
|
|
1244
|
-
const y = Buffer.from(b,
|
|
984
|
+
const x = Buffer.from(a, "hex");
|
|
985
|
+
const y = Buffer.from(b, "hex");
|
|
1245
986
|
return x.length === y.length && x.length > 0 && timingSafeEqual(x, y);
|
|
1246
987
|
}
|
|
1247
988
|
var REPLAY_MAX_BYTES = 10 * 1024 * 1024;
|
|
1248
989
|
function deadTunnel(err, reusedSocket) {
|
|
1249
990
|
if (err instanceof SessionGoneError) return true;
|
|
1250
991
|
const code = err?.code;
|
|
1251
|
-
return reusedSocket && (code ===
|
|
992
|
+
return reusedSocket && (code === "ECONNRESET" || code === "EPIPE");
|
|
1252
993
|
}
|
|
1253
994
|
function describeError(err) {
|
|
1254
|
-
if (err instanceof BackoffError)
|
|
1255
|
-
|
|
1256
|
-
if (err instanceof IdentityMismatchError) return 'tunnel identity mismatch';
|
|
995
|
+
if (err instanceof BackoffError) return `tunnel identity mismatch - waiting before retry (${Math.ceil(err.waitMs / 6e4)} min)`;
|
|
996
|
+
if (err instanceof IdentityMismatchError) return "tunnel identity mismatch";
|
|
1257
997
|
if (err instanceof TunnelBlockedError) return err.message;
|
|
1258
|
-
return
|
|
998
|
+
return "tunnel unavailable";
|
|
1259
999
|
}
|
|
1260
1000
|
function startLocalServer(o) {
|
|
1261
1001
|
const server = http5.createServer((req, res) => {
|
|
1262
1002
|
const started = Date.now();
|
|
1263
|
-
const route = (req.url ??
|
|
1264
|
-
const hostname = String(req.headers.host ??
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
if (!LOOPBACK2.has(hostname))
|
|
1268
|
-
return send(res, 403, 'this endpoint answers on 127.0.0.1 only');
|
|
1269
|
-
if (req.headers.origin !== void 0)
|
|
1270
|
-
return send(res, 403, 'browser requests are not accepted');
|
|
1003
|
+
const route = (req.url ?? "/").split("?")[0].slice(0, 200);
|
|
1004
|
+
const hostname = String(req.headers.host ?? "").toLowerCase().replace(/:\d+$/, "");
|
|
1005
|
+
if (!LOOPBACK2.has(hostname)) return send(res, 403, "this endpoint answers on 127.0.0.1 only");
|
|
1006
|
+
if (req.headers.origin !== void 0) return send(res, 403, "browser requests are not accepted");
|
|
1271
1007
|
const key = keyOf(req);
|
|
1272
|
-
if (!key || !sameHash(hashLocalKey(key), o.localKeyHash))
|
|
1273
|
-
return send(res, 401, 'invalid or missing local key');
|
|
1008
|
+
if (!key || !sameHash(hashLocalKey(key), o.localKeyHash)) return send(res, 401, "invalid or missing local key");
|
|
1274
1009
|
const headers = {};
|
|
1275
|
-
for (const [name, value] of Object.entries(req.headers))
|
|
1276
|
-
if (!HOP.has(name) && value !== void 0) headers[name] = value;
|
|
1010
|
+
for (const [name, value] of Object.entries(req.headers)) if (!HOP.has(name) && value !== void 0) headers[name] = value;
|
|
1277
1011
|
headers.host = o.target.host;
|
|
1278
1012
|
headers.authorization = `Bearer ${o.deviceToken}`;
|
|
1279
1013
|
let kept = [];
|
|
1280
1014
|
let keptBytes = 0;
|
|
1281
|
-
req.on(
|
|
1015
|
+
req.on("data", (c) => {
|
|
1282
1016
|
if (!kept) return;
|
|
1283
1017
|
keptBytes += c.length;
|
|
1284
1018
|
if (keptBytes > REPLAY_MAX_BYTES) kept = null;
|
|
@@ -1286,33 +1020,18 @@ function startLocalServer(o) {
|
|
|
1286
1020
|
});
|
|
1287
1021
|
let upstream;
|
|
1288
1022
|
const forward = (replay) => {
|
|
1289
|
-
const via = replay
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
(
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
res.writeHead(up.statusCode ?? 502, out);
|
|
1299
|
-
up.pipe(res);
|
|
1300
|
-
up.on('error', () => res.destroy());
|
|
1301
|
-
up.on('end', () =>
|
|
1302
|
-
o.log?.(
|
|
1303
|
-
`${req.method} ${route} status=${up.statusCode} total=${Date.now() - started}ms`,
|
|
1304
|
-
),
|
|
1305
|
-
);
|
|
1306
|
-
},
|
|
1307
|
-
);
|
|
1023
|
+
const via = replay ? { createConnection: (opts, cb) => o.agent.createConnection(opts, cb) } : { agent: o.agent };
|
|
1024
|
+
const current = http5.request({ ...via, method: req.method, path: req.url, headers, setHost: false }, (up) => {
|
|
1025
|
+
const out = {};
|
|
1026
|
+
for (const [name, value] of Object.entries(up.headers)) if (!HOP.has(name) && value !== void 0) out[name] = value;
|
|
1027
|
+
res.writeHead(up.statusCode ?? 502, out);
|
|
1028
|
+
up.pipe(res);
|
|
1029
|
+
up.on("error", () => res.destroy());
|
|
1030
|
+
up.on("end", () => o.log?.(`${req.method} ${route} status=${up.statusCode} total=${Date.now() - started}ms`));
|
|
1031
|
+
});
|
|
1308
1032
|
upstream = current;
|
|
1309
|
-
current.on(
|
|
1310
|
-
if (
|
|
1311
|
-
!replay &&
|
|
1312
|
-
!res.headersSent &&
|
|
1313
|
-
kept &&
|
|
1314
|
-
deadTunnel(err, current.reusedSocket)
|
|
1315
|
-
) {
|
|
1033
|
+
current.on("error", (err) => {
|
|
1034
|
+
if (!replay && !res.headersSent && kept && deadTunnel(err, current.reusedSocket)) {
|
|
1316
1035
|
o.log?.(`${req.method} ${route} retry: the tunnel was gone`);
|
|
1317
1036
|
const again = () => {
|
|
1318
1037
|
if (kept && !res.destroyed) forward(true);
|
|
@@ -1320,30 +1039,29 @@ function startLocalServer(o) {
|
|
|
1320
1039
|
};
|
|
1321
1040
|
if (req.readableEnded) again();
|
|
1322
1041
|
else {
|
|
1323
|
-
req.once(
|
|
1042
|
+
req.once("end", again);
|
|
1324
1043
|
req.resume();
|
|
1325
1044
|
}
|
|
1326
1045
|
return;
|
|
1327
1046
|
}
|
|
1328
1047
|
o.log?.(`${req.method} ${route} failed: ${describeError(err)}`);
|
|
1329
1048
|
if (res.headersSent) res.destroy();
|
|
1330
|
-
else if (err instanceof TunnelBlockedError && err.status === 429)
|
|
1331
|
-
send(res, 429, 'Too many attempts');
|
|
1049
|
+
else if (err instanceof TunnelBlockedError && err.status === 429) send(res, 429, "Too many attempts");
|
|
1332
1050
|
else send(res, 502, describeError(err));
|
|
1333
1051
|
});
|
|
1334
1052
|
if (replay) current.end(Buffer.concat(kept ?? []));
|
|
1335
1053
|
else req.pipe(current);
|
|
1336
1054
|
};
|
|
1337
1055
|
forward(false);
|
|
1338
|
-
res.on(
|
|
1056
|
+
res.on("close", () => {
|
|
1339
1057
|
if (!res.writableFinished) upstream.destroy();
|
|
1340
1058
|
});
|
|
1341
|
-
req.on(
|
|
1059
|
+
req.on("error", () => upstream.destroy());
|
|
1342
1060
|
});
|
|
1343
1061
|
return new Promise((resolve, reject) => {
|
|
1344
|
-
server.once(
|
|
1345
|
-
server.listen(o.port,
|
|
1346
|
-
server.off(
|
|
1062
|
+
server.once("error", reject);
|
|
1063
|
+
server.listen(o.port, "127.0.0.1", () => {
|
|
1064
|
+
server.off("error", reject);
|
|
1347
1065
|
resolve(server);
|
|
1348
1066
|
});
|
|
1349
1067
|
});
|
|
@@ -1380,27 +1098,17 @@ var IdentityBackoff = class {
|
|
|
1380
1098
|
this.since = null;
|
|
1381
1099
|
}
|
|
1382
1100
|
snapshot() {
|
|
1383
|
-
return {
|
|
1384
|
-
mismatches: this.count,
|
|
1385
|
-
since: this.since,
|
|
1386
|
-
waitMs: this.blockedMs(),
|
|
1387
|
-
};
|
|
1101
|
+
return { mismatches: this.count, since: this.since, waitMs: this.blockedMs() };
|
|
1388
1102
|
}
|
|
1389
1103
|
};
|
|
1390
1104
|
|
|
1391
1105
|
// src/client/pin-sync.ts
|
|
1392
|
-
import http6 from
|
|
1393
|
-
import tls4 from
|
|
1106
|
+
import http6 from "node:http";
|
|
1107
|
+
import tls4 from "node:tls";
|
|
1394
1108
|
function mergePins(own, listed, via) {
|
|
1395
1109
|
const unchanged = { pins: [...own], added: [], dropped: [] };
|
|
1396
1110
|
if (via === null || !own.includes(via)) return unchanged;
|
|
1397
|
-
if (
|
|
1398
|
-
!Array.isArray(listed) ||
|
|
1399
|
-
listed.length === 0 ||
|
|
1400
|
-
listed.length > MAX_PINS ||
|
|
1401
|
-
!listed.every((p) => typeof p === 'string' && isPin(p))
|
|
1402
|
-
)
|
|
1403
|
-
return unchanged;
|
|
1111
|
+
if (!Array.isArray(listed) || listed.length === 0 || listed.length > MAX_PINS || !listed.every((p) => typeof p === "string" && isPin(p))) return unchanged;
|
|
1404
1112
|
const list = [...new Set(listed)];
|
|
1405
1113
|
const added = list.filter((p) => !own.includes(p));
|
|
1406
1114
|
if (list.includes(via)) {
|
|
@@ -1413,9 +1121,7 @@ function mergePins(own, listed, via) {
|
|
|
1413
1121
|
function pinOfSocket(socket) {
|
|
1414
1122
|
if (!(socket instanceof tls4.TLSSocket)) return null;
|
|
1415
1123
|
const peer = peerCertificate(socket);
|
|
1416
|
-
return peer
|
|
1417
|
-
? pinOfSpki(peer.publicKey.export({ type: 'spki', format: 'der' }))
|
|
1418
|
-
: null;
|
|
1124
|
+
return peer ? pinOfSpki(peer.publicKey.export({ type: "spki", format: "der" })) : null;
|
|
1419
1125
|
}
|
|
1420
1126
|
var MAX_BODY = 16 * 1024;
|
|
1421
1127
|
var short = (pin) => fingerprint(pin).slice(0, 19);
|
|
@@ -1447,44 +1153,28 @@ var PinSync = class {
|
|
|
1447
1153
|
const now = (this.o.now ?? Date.now)();
|
|
1448
1154
|
if (this.running || now - this.last < (this.o.intervalMs ?? 36e5)) return;
|
|
1449
1155
|
this.last = now;
|
|
1450
|
-
this.syncNow().catch((err) =>
|
|
1451
|
-
this.o.log?.(
|
|
1452
|
-
`pin check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1453
|
-
),
|
|
1454
|
-
);
|
|
1156
|
+
this.syncNow().catch((err) => this.o.log?.(`pin check failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1455
1157
|
}
|
|
1456
1158
|
async run() {
|
|
1457
1159
|
this.last = (this.o.now ?? Date.now)();
|
|
1458
1160
|
const { listed, via } = await this.fetch();
|
|
1459
1161
|
const merged = mergePins(this.current, listed, via);
|
|
1460
|
-
const changed =
|
|
1461
|
-
merged.pins.length !== this.current.length ||
|
|
1462
|
-
merged.pins.some((p, i) => p !== this.current[i]);
|
|
1162
|
+
const changed = merged.pins.length !== this.current.length || merged.pins.some((p, i) => p !== this.current[i]);
|
|
1463
1163
|
if (changed) {
|
|
1464
1164
|
this.current = merged.pins;
|
|
1465
1165
|
this.o.onChange?.(this.pins);
|
|
1466
|
-
this.o.log?.(
|
|
1467
|
-
`proxy keys updated through the pinned tunnel: ${merged.added
|
|
1468
|
-
.map((p) => `+${short(p)}`)
|
|
1469
|
-
.concat(merged.dropped.map((p) => `-${short(p)}`))
|
|
1470
|
-
.join(' ')}`,
|
|
1471
|
-
);
|
|
1166
|
+
this.o.log?.(`proxy keys updated through the pinned tunnel: ${merged.added.map((p) => `+${short(p)}`).concat(merged.dropped.map((p) => `-${short(p)}`)).join(" ")}`);
|
|
1472
1167
|
}
|
|
1473
1168
|
if ((changed || this.unsaved) && !this.saveStopped) {
|
|
1474
1169
|
try {
|
|
1475
|
-
if (
|
|
1170
|
+
if (await this.o.save(this.pins) === false) {
|
|
1476
1171
|
this.saveStopped = true;
|
|
1477
|
-
this.o.log?.(
|
|
1478
|
-
this.o.stoppedLine ??
|
|
1479
|
-
'config.bin now holds another configuration (setup ran since): the proxy keys learned by this run are no longer saved',
|
|
1480
|
-
);
|
|
1172
|
+
this.o.log?.(this.o.stoppedLine ?? "config.bin now holds another configuration (setup ran since): the proxy keys learned by this run are no longer saved");
|
|
1481
1173
|
}
|
|
1482
1174
|
this.unsaved = false;
|
|
1483
1175
|
} catch (err) {
|
|
1484
1176
|
this.unsaved = true;
|
|
1485
|
-
this.o.log?.(
|
|
1486
|
-
`could not save the proxy keys (tried again at the next check): ${err instanceof Error ? err.message : String(err)}`,
|
|
1487
|
-
);
|
|
1177
|
+
this.o.log?.(`could not save the proxy keys (tried again at the next check): ${err instanceof Error ? err.message : String(err)}`);
|
|
1488
1178
|
}
|
|
1489
1179
|
}
|
|
1490
1180
|
return { ...merged, via };
|
|
@@ -1492,52 +1182,35 @@ var PinSync = class {
|
|
|
1492
1182
|
fetch() {
|
|
1493
1183
|
const { target } = this.o;
|
|
1494
1184
|
return new Promise((resolve, reject) => {
|
|
1495
|
-
const req = http6.request(
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
),
|
|
1525
|
-
);
|
|
1526
|
-
try {
|
|
1527
|
-
resolve({
|
|
1528
|
-
listed: JSON.parse(Buffer.concat(chunks).toString('utf8')).pins,
|
|
1529
|
-
via,
|
|
1530
|
-
});
|
|
1531
|
-
} catch {
|
|
1532
|
-
reject(new Error("the proxy's pin list is not JSON"));
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
},
|
|
1536
|
-
);
|
|
1537
|
-
req.setTimeout(this.o.timeoutMs ?? 3e4, () =>
|
|
1538
|
-
req.destroy(new Error('pin check timeout')),
|
|
1539
|
-
);
|
|
1540
|
-
req.on('error', reject);
|
|
1185
|
+
const req = http6.request({
|
|
1186
|
+
host: target.hostname,
|
|
1187
|
+
port: target.port,
|
|
1188
|
+
method: "GET",
|
|
1189
|
+
path: TUNNEL_PINS_PATH,
|
|
1190
|
+
agent: this.o.agent,
|
|
1191
|
+
setHost: false,
|
|
1192
|
+
headers: { host: target.host, authorization: `Bearer ${this.o.deviceToken}`, accept: "application/json" }
|
|
1193
|
+
}, (res) => {
|
|
1194
|
+
const via = pinOfSocket(res.socket);
|
|
1195
|
+
const chunks = [];
|
|
1196
|
+
let size = 0;
|
|
1197
|
+
res.on("data", (c) => {
|
|
1198
|
+
size += c.length;
|
|
1199
|
+
if (size > MAX_BODY) req.destroy(new Error("pin list too large"));
|
|
1200
|
+
else chunks.push(c);
|
|
1201
|
+
});
|
|
1202
|
+
res.on("error", reject);
|
|
1203
|
+
res.on("end", () => {
|
|
1204
|
+
if (res.statusCode !== 200) return reject(new Error(`the proxy answered ${res.statusCode ?? 0} for its pins`));
|
|
1205
|
+
try {
|
|
1206
|
+
resolve({ listed: JSON.parse(Buffer.concat(chunks).toString("utf8")).pins, via });
|
|
1207
|
+
} catch {
|
|
1208
|
+
reject(new Error("the proxy's pin list is not JSON"));
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
req.setTimeout(this.o.timeoutMs ?? 3e4, () => req.destroy(new Error("pin check timeout")));
|
|
1213
|
+
req.on("error", reject);
|
|
1541
1214
|
req.end();
|
|
1542
1215
|
});
|
|
1543
1216
|
}
|
|
@@ -1546,27 +1219,19 @@ var PinSync = class {
|
|
|
1546
1219
|
// src/client/session.ts
|
|
1547
1220
|
async function startTunnelSession(o) {
|
|
1548
1221
|
const target = new URL(o.url);
|
|
1222
|
+
const failover = (o.urls ?? []).map((u) => new URL(u));
|
|
1549
1223
|
const backoff = o.backoff ?? new IdentityBackoff();
|
|
1550
|
-
const agent = new TunnelAgent(
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
backoff,
|
|
1558
|
-
{
|
|
1559
|
-
onOuter: o.onOuter,
|
|
1560
|
-
onConnected: () => {
|
|
1561
|
-
setImmediate(() => {
|
|
1562
|
-
if (!agent.destroyed) pinSync.maybeSync();
|
|
1563
|
-
});
|
|
1564
|
-
o.onConnected?.();
|
|
1565
|
-
},
|
|
1566
|
-
onMismatch: o.onMismatch,
|
|
1567
|
-
onTransport: o.onTransport,
|
|
1224
|
+
const agent = new TunnelAgent({ url: target, urls: failover, pins: o.pins, userAgent: o.userAgent, transport: o.transport }, backoff, {
|
|
1225
|
+
onOuter: o.onOuter,
|
|
1226
|
+
onConnected: () => {
|
|
1227
|
+
setImmediate(() => {
|
|
1228
|
+
if (!agent.destroyed) pinSync.maybeSync();
|
|
1229
|
+
});
|
|
1230
|
+
o.onConnected?.();
|
|
1568
1231
|
},
|
|
1569
|
-
|
|
1232
|
+
onMismatch: o.onMismatch,
|
|
1233
|
+
onTransport: o.onTransport
|
|
1234
|
+
});
|
|
1570
1235
|
const pinSync = new PinSync({
|
|
1571
1236
|
agent,
|
|
1572
1237
|
target,
|
|
@@ -1575,18 +1240,11 @@ async function startTunnelSession(o) {
|
|
|
1575
1240
|
log: o.log,
|
|
1576
1241
|
onChange: (pins) => agent.setPins(pins),
|
|
1577
1242
|
save: o.savePins,
|
|
1578
|
-
stoppedLine: o.savePinsStoppedLine
|
|
1243
|
+
stoppedLine: o.savePinsStoppedLine
|
|
1579
1244
|
});
|
|
1580
1245
|
let server;
|
|
1581
1246
|
try {
|
|
1582
|
-
server = await startLocalServer({
|
|
1583
|
-
port: o.port,
|
|
1584
|
-
localKeyHash: o.localKeyHash,
|
|
1585
|
-
deviceToken: o.deviceToken,
|
|
1586
|
-
target,
|
|
1587
|
-
agent,
|
|
1588
|
-
log: o.log,
|
|
1589
|
-
});
|
|
1247
|
+
server = await startLocalServer({ port: o.port, localKeyHash: o.localKeyHash, deviceToken: o.deviceToken, target, agent, log: o.log });
|
|
1590
1248
|
} catch (err) {
|
|
1591
1249
|
agent.destroy();
|
|
1592
1250
|
throw err;
|
|
@@ -1605,7 +1263,7 @@ async function startTunnelSession(o) {
|
|
|
1605
1263
|
}
|
|
1606
1264
|
|
|
1607
1265
|
// src/client/version.ts
|
|
1608
|
-
var VERSION = true ?
|
|
1266
|
+
var VERSION = true ? "9.2.0" : "dev";
|
|
1609
1267
|
|
|
1610
1268
|
// src/client/lib.ts
|
|
1611
1269
|
var version = VERSION;
|
|
@@ -1613,27 +1271,21 @@ var O1ConnectError = class extends Error {
|
|
|
1613
1271
|
code;
|
|
1614
1272
|
constructor(code, message, options) {
|
|
1615
1273
|
super(message, options);
|
|
1616
|
-
this.name =
|
|
1274
|
+
this.name = "O1ConnectError";
|
|
1617
1275
|
this.code = code;
|
|
1618
1276
|
}
|
|
1619
1277
|
};
|
|
1620
|
-
var DEFAULT_NAME =
|
|
1621
|
-
var TRANSPORTS = [
|
|
1278
|
+
var DEFAULT_NAME = "aipp-connect";
|
|
1279
|
+
var TRANSPORTS = ["auto", "websocket", "post"];
|
|
1622
1280
|
function entryName(name) {
|
|
1623
1281
|
if (name === void 0) return DEFAULT_NAME;
|
|
1624
|
-
if (typeof name !==
|
|
1625
|
-
throw new TypeError('name must be a non-empty string');
|
|
1282
|
+
if (typeof name !== "string" || name.trim() === "") throw new TypeError("name must be a non-empty string");
|
|
1626
1283
|
return name;
|
|
1627
1284
|
}
|
|
1628
1285
|
function checkStore(store) {
|
|
1629
1286
|
const s = store;
|
|
1630
|
-
if (
|
|
1631
|
-
|
|
1632
|
-
typeof s.get !== 'function' ||
|
|
1633
|
-
typeof s.set !== 'function' ||
|
|
1634
|
-
typeof s.delete !== 'function'
|
|
1635
|
-
) {
|
|
1636
|
-
throw new TypeError('store must implement get, set and delete');
|
|
1287
|
+
if (!s || typeof s.get !== "function" || typeof s.set !== "function" || typeof s.delete !== "function") {
|
|
1288
|
+
throw new TypeError("store must implement get, set and delete");
|
|
1637
1289
|
}
|
|
1638
1290
|
return s;
|
|
1639
1291
|
}
|
|
@@ -1646,10 +1298,7 @@ function withEntry(store, name, fn) {
|
|
|
1646
1298
|
}
|
|
1647
1299
|
const map = byName;
|
|
1648
1300
|
const run = (map.get(name) ?? Promise.resolve()).then(fn);
|
|
1649
|
-
const tail = run.then(
|
|
1650
|
-
() => void 0,
|
|
1651
|
-
() => void 0,
|
|
1652
|
-
);
|
|
1301
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
1653
1302
|
map.set(name, tail);
|
|
1654
1303
|
void tail.then(() => {
|
|
1655
1304
|
if (map.get(name) === tail) map.delete(name);
|
|
@@ -1660,11 +1309,7 @@ async function viaStore(what, call) {
|
|
|
1660
1309
|
try {
|
|
1661
1310
|
return await call();
|
|
1662
1311
|
} catch (err) {
|
|
1663
|
-
throw new O1ConnectError(
|
|
1664
|
-
'store_failed',
|
|
1665
|
-
`the secret store failed to ${what}`,
|
|
1666
|
-
{ cause: err },
|
|
1667
|
-
);
|
|
1312
|
+
throw new O1ConnectError("store_failed", `the secret store failed to ${what}`, { cause: err });
|
|
1668
1313
|
}
|
|
1669
1314
|
}
|
|
1670
1315
|
function parseSaved(raw) {
|
|
@@ -1672,95 +1317,60 @@ function parseSaved(raw) {
|
|
|
1672
1317
|
try {
|
|
1673
1318
|
o = JSON.parse(String(raw));
|
|
1674
1319
|
} catch {
|
|
1675
|
-
throw new O1ConnectError(
|
|
1676
|
-
'invalid_config',
|
|
1677
|
-
'the saved configuration is not JSON: run setup again',
|
|
1678
|
-
);
|
|
1320
|
+
throw new O1ConnectError("invalid_config", "the saved configuration is not JSON: run setup again");
|
|
1679
1321
|
}
|
|
1680
|
-
if (!o || typeof o !==
|
|
1681
|
-
throw new O1ConnectError(
|
|
1682
|
-
'invalid_config',
|
|
1683
|
-
'the saved configuration is not one this library knows: run setup again',
|
|
1684
|
-
);
|
|
1322
|
+
if (!o || typeof o !== "object" || o.v !== 1) throw new O1ConnectError("invalid_config", "the saved configuration is not one this library knows: run setup again");
|
|
1685
1323
|
let code;
|
|
1686
1324
|
try {
|
|
1687
1325
|
code = validateCode(o);
|
|
1688
1326
|
} catch (err) {
|
|
1689
|
-
throw new O1ConnectError(
|
|
1690
|
-
'invalid_config',
|
|
1691
|
-
`the saved configuration is not valid (${err instanceof Error ? err.message : String(err)}): run setup again`,
|
|
1692
|
-
);
|
|
1327
|
+
throw new O1ConnectError("invalid_config", `the saved configuration is not valid (${err instanceof Error ? err.message : String(err)}): run setup again`);
|
|
1693
1328
|
}
|
|
1694
|
-
const transport = o.transport ??
|
|
1695
|
-
if (!TRANSPORTS.includes(transport))
|
|
1696
|
-
throw new O1ConnectError(
|
|
1697
|
-
'invalid_config',
|
|
1698
|
-
'the saved transport is not auto, websocket or post: run setup again',
|
|
1699
|
-
);
|
|
1329
|
+
const transport = o.transport ?? "auto";
|
|
1330
|
+
if (!TRANSPORTS.includes(transport)) throw new O1ConnectError("invalid_config", "the saved transport is not auto, websocket or post: run setup again");
|
|
1700
1331
|
return { v: 1, ...code, transport };
|
|
1701
1332
|
}
|
|
1702
1333
|
async function setup(code, opts) {
|
|
1703
1334
|
const store = checkStore(opts?.store);
|
|
1704
|
-
if (typeof opts.onFingerprint !==
|
|
1705
|
-
throw new TypeError(
|
|
1706
|
-
"onFingerprint is required: every key's fingerprint must be confirmed by the user",
|
|
1707
|
-
);
|
|
1335
|
+
if (typeof opts.onFingerprint !== "function") throw new TypeError("onFingerprint is required: every key's fingerprint must be confirmed by the user");
|
|
1708
1336
|
const name = entryName(opts.name);
|
|
1709
|
-
const transport = opts.transport ??
|
|
1710
|
-
if (!TRANSPORTS.includes(transport))
|
|
1711
|
-
throw new TypeError('transport must be auto, websocket or post');
|
|
1337
|
+
const transport = opts.transport ?? "auto";
|
|
1338
|
+
if (!TRANSPORTS.includes(transport)) throw new TypeError("transport must be auto, websocket or post");
|
|
1712
1339
|
let decoded;
|
|
1713
1340
|
try {
|
|
1714
|
-
decoded = decodeConnectionCode(String(code ??
|
|
1341
|
+
decoded = decodeConnectionCode(String(code ?? ""));
|
|
1715
1342
|
} catch (err) {
|
|
1716
|
-
throw new O1ConnectError(
|
|
1717
|
-
'invalid_code',
|
|
1718
|
-
err instanceof ConnectionCodeError
|
|
1719
|
-
? err.message
|
|
1720
|
-
: 'the connection code is not valid',
|
|
1721
|
-
);
|
|
1343
|
+
throw new O1ConnectError("invalid_code", err instanceof ConnectionCodeError ? err.message : "the connection code is not valid");
|
|
1722
1344
|
}
|
|
1723
1345
|
const fingerprints = decoded.pins.map((p) => fingerprint(p));
|
|
1724
|
-
const info = { device: decoded.device, url: decoded.url };
|
|
1725
|
-
if (
|
|
1726
|
-
throw new O1ConnectError(
|
|
1727
|
-
'fingerprint_rejected',
|
|
1728
|
-
'the proxy keys were not confirmed: nothing was saved',
|
|
1729
|
-
);
|
|
1346
|
+
const info = { device: decoded.device, url: decoded.url, ...decoded.urls ? { urls: [...decoded.urls] } : {} };
|
|
1347
|
+
if (await opts.onFingerprint([...fingerprints], { ...info }) !== true) {
|
|
1348
|
+
throw new O1ConnectError("fingerprint_rejected", "the proxy keys were not confirmed: nothing was saved");
|
|
1730
1349
|
}
|
|
1731
1350
|
const saved = { v: 1, ...decoded, transport };
|
|
1732
|
-
await withEntry(store, name, () =>
|
|
1733
|
-
viaStore('save the configuration', () =>
|
|
1734
|
-
store.set(name, JSON.stringify(saved)),
|
|
1735
|
-
),
|
|
1736
|
-
);
|
|
1351
|
+
await withEntry(store, name, () => viaStore("save the configuration", () => store.set(name, JSON.stringify(saved))));
|
|
1737
1352
|
return { ...info, fingerprints };
|
|
1738
1353
|
}
|
|
1739
1354
|
async function open(opts) {
|
|
1740
1355
|
const store = checkStore(opts?.store);
|
|
1741
1356
|
const name = entryName(opts.name);
|
|
1742
1357
|
const port = opts.port ?? 0;
|
|
1743
|
-
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
if (raw === null || raw === void 0)
|
|
1747
|
-
throw new O1ConnectError(
|
|
1748
|
-
'not_set_up',
|
|
1749
|
-
'no o1-connect configuration in the store: run setup with a connection code',
|
|
1750
|
-
);
|
|
1358
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new TypeError("port must be an integer from 0 to 65535");
|
|
1359
|
+
const raw = await viaStore("read the configuration", () => store.get(name));
|
|
1360
|
+
if (raw === null || raw === void 0) throw new O1ConnectError("not_set_up", "no o1-connect configuration in the store: run setup with a connection code");
|
|
1751
1361
|
const cfg = parseSaved(raw);
|
|
1752
|
-
const log = opts.log
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
: void 0;
|
|
1362
|
+
const log = opts.log ? (line) => {
|
|
1363
|
+
try {
|
|
1364
|
+
opts.log(line);
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
} : void 0;
|
|
1759
1368
|
const apiKey = newLocalKey();
|
|
1760
1369
|
let session;
|
|
1761
1370
|
try {
|
|
1762
1371
|
session = await startTunnelSession({
|
|
1763
1372
|
url: cfg.url,
|
|
1373
|
+
urls: cfg.urls,
|
|
1764
1374
|
pins: cfg.pins,
|
|
1765
1375
|
deviceToken: cfg.token,
|
|
1766
1376
|
transport: cfg.transport,
|
|
@@ -1769,53 +1379,35 @@ async function open(opts) {
|
|
|
1769
1379
|
userAgent: `o1-connect-lib/${VERSION}`,
|
|
1770
1380
|
log,
|
|
1771
1381
|
onMismatch: (received) => {
|
|
1772
|
-
log?.(
|
|
1773
|
-
`TUNNEL IDENTITY MISMATCH: received ${received}; possible interception`,
|
|
1774
|
-
);
|
|
1382
|
+
log?.(`TUNNEL IDENTITY MISMATCH: received ${received}; possible interception`);
|
|
1775
1383
|
try {
|
|
1776
1384
|
opts.onMismatch?.(received);
|
|
1777
|
-
} catch {
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1778
1387
|
},
|
|
1779
|
-
onTransport: (t, reason) => log?.(`${reason ??
|
|
1388
|
+
onTransport: (t, reason) => log?.(`${reason ?? "transport"}, using ${t}`),
|
|
1780
1389
|
// Pins learned during a key rotation: saved only while the store still holds this
|
|
1781
1390
|
// configuration (same proxy, device and token), under the entry's lock; false (never
|
|
1782
1391
|
// tried again by this connection) once setup replaced it or forget deleted it.
|
|
1783
|
-
savePinsStoppedLine:
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
);
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
if (
|
|
1798
|
-
current.url !== cfg.url ||
|
|
1799
|
-
current.device !== cfg.device ||
|
|
1800
|
-
current.token !== cfg.token
|
|
1801
|
-
)
|
|
1802
|
-
return false;
|
|
1803
|
-
await viaStore('save the configuration', () =>
|
|
1804
|
-
store.set(name, JSON.stringify({ ...current, pins })),
|
|
1805
|
-
);
|
|
1806
|
-
return true;
|
|
1807
|
-
}),
|
|
1392
|
+
savePinsStoppedLine: "the store now holds another configuration, or none (setup or forget ran since): the proxy keys learned by this connection are no longer saved",
|
|
1393
|
+
savePins: (pins) => withEntry(store, name, async () => {
|
|
1394
|
+
const raw2 = await viaStore("read the configuration", () => store.get(name));
|
|
1395
|
+
if (raw2 === null || raw2 === void 0) return false;
|
|
1396
|
+
let current;
|
|
1397
|
+
try {
|
|
1398
|
+
current = parseSaved(raw2);
|
|
1399
|
+
} catch {
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
if (current.url !== cfg.url || current.device !== cfg.device || current.token !== cfg.token) return false;
|
|
1403
|
+
await viaStore("save the configuration", () => store.set(name, JSON.stringify({ ...current, pins })));
|
|
1404
|
+
return true;
|
|
1405
|
+
})
|
|
1808
1406
|
});
|
|
1809
1407
|
} catch (err) {
|
|
1810
1408
|
const code = err.code;
|
|
1811
|
-
if (code ===
|
|
1812
|
-
|
|
1813
|
-
if (code === 'EACCES' || code === 'EADDRNOTAVAIL')
|
|
1814
|
-
throw new O1ConnectError(
|
|
1815
|
-
'port_unavailable',
|
|
1816
|
-
`cannot listen on 127.0.0.1:${port} (${code})`,
|
|
1817
|
-
{ cause: err },
|
|
1818
|
-
);
|
|
1409
|
+
if (code === "EADDRINUSE") throw new O1ConnectError("port_in_use", `port ${port} is in use`);
|
|
1410
|
+
if (code === "EACCES" || code === "EADDRNOTAVAIL") throw new O1ConnectError("port_unavailable", `cannot listen on 127.0.0.1:${port} (${code})`, { cause: err });
|
|
1819
1411
|
throw err;
|
|
1820
1412
|
}
|
|
1821
1413
|
if (opts.check !== false) {
|
|
@@ -1824,36 +1416,19 @@ async function open(opts) {
|
|
|
1824
1416
|
} catch (err) {
|
|
1825
1417
|
await session.close();
|
|
1826
1418
|
if (err instanceof IdentityMismatchError || err instanceof BackoffError) {
|
|
1827
|
-
throw new O1ConnectError(
|
|
1828
|
-
'identity_mismatch',
|
|
1829
|
-
'the proxy presented a key that is not pinned: possible interception (nothing was sent)',
|
|
1830
|
-
);
|
|
1419
|
+
throw new O1ConnectError("identity_mismatch", "the proxy presented a key that is not pinned: possible interception (nothing was sent)");
|
|
1831
1420
|
}
|
|
1832
|
-
throw new O1ConnectError(
|
|
1833
|
-
'tunnel_unavailable',
|
|
1834
|
-
`the tunnel did not answer: ${err instanceof Error ? err.message : String(err)}`,
|
|
1835
|
-
);
|
|
1421
|
+
throw new O1ConnectError("tunnel_unavailable", `the tunnel did not answer: ${err instanceof Error ? err.message : String(err)}`);
|
|
1836
1422
|
}
|
|
1837
1423
|
}
|
|
1838
1424
|
const baseUrl = `http://127.0.0.1:${session.port}`;
|
|
1839
|
-
log?.(
|
|
1840
|
-
|
|
1841
|
-
);
|
|
1842
|
-
return {
|
|
1843
|
-
baseUrl,
|
|
1844
|
-
openaiBaseUrl: `${baseUrl}/v1`,
|
|
1845
|
-
apiKey,
|
|
1846
|
-
port: session.port,
|
|
1847
|
-
device: cfg.device,
|
|
1848
|
-
close: session.close,
|
|
1849
|
-
};
|
|
1425
|
+
log?.(`o1-connect library ${VERSION} for ${cfg.device} listening on ${baseUrl} (transport ${cfg.transport})`);
|
|
1426
|
+
return { baseUrl, openaiBaseUrl: `${baseUrl}/v1`, apiKey, port: session.port, device: cfg.device, close: session.close };
|
|
1850
1427
|
}
|
|
1851
1428
|
async function forget(opts) {
|
|
1852
1429
|
const store = checkStore(opts?.store);
|
|
1853
1430
|
const name = entryName(opts.name);
|
|
1854
|
-
await withEntry(store, name, () =>
|
|
1855
|
-
viaStore('delete the configuration', () => store.delete(name)),
|
|
1856
|
-
);
|
|
1431
|
+
await withEntry(store, name, () => viaStore("delete the configuration", () => store.delete(name)));
|
|
1857
1432
|
}
|
|
1858
1433
|
var MemorySecretStore = class {
|
|
1859
1434
|
#values = /* @__PURE__ */ new Map();
|
|
@@ -1867,4 +1442,11 @@ var MemorySecretStore = class {
|
|
|
1867
1442
|
this.#values.delete(name);
|
|
1868
1443
|
}
|
|
1869
1444
|
};
|
|
1870
|
-
export {
|
|
1445
|
+
export {
|
|
1446
|
+
MemorySecretStore,
|
|
1447
|
+
O1ConnectError,
|
|
1448
|
+
forget,
|
|
1449
|
+
open,
|
|
1450
|
+
setup,
|
|
1451
|
+
version
|
|
1452
|
+
};
|