strikethroo 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -1
- package/dist/export-profile.d.ts +6 -0
- package/dist/export-profile.d.ts.map +1 -1
- package/dist/export-profile.js +46 -4
- package/dist/export-profile.js.map +1 -1
- package/dist/metadata.d.ts +11 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +75 -11
- package/dist/metadata.js.map +1 -1
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +52 -7
- package/dist/profiles.js.map +1 -1
- package/dist/serve/config-write.d.ts +6 -3
- package/dist/serve/config-write.d.ts.map +1 -1
- package/dist/serve/config-write.js +20 -21
- package/dist/serve/config-write.js.map +1 -1
- package/dist/serve/events.d.ts +37 -8
- package/dist/serve/events.d.ts.map +1 -1
- package/dist/serve/events.js +114 -28
- package/dist/serve/events.js.map +1 -1
- package/dist/serve/self-review.d.ts +37 -7
- package/dist/serve/self-review.d.ts.map +1 -1
- package/dist/serve/self-review.js +93 -33
- package/dist/serve/self-review.js.map +1 -1
- package/dist/serve/server.d.ts +107 -0
- package/dist/serve/server.d.ts.map +1 -1
- package/dist/serve/server.js +502 -135
- package/dist/serve/server.js.map +1 -1
- package/dist/serve/workspace-model.d.ts.map +1 -1
- package/dist/serve/workspace-model.js +36 -34
- package/dist/serve/workspace-model.js.map +1 -1
- package/dist/skill-scripts/shared/safe-fs.d.ts +89 -0
- package/dist/skill-scripts/shared/safe-fs.d.ts.map +1 -0
- package/dist/skill-scripts/shared/safe-fs.js +324 -0
- package/dist/skill-scripts/shared/safe-fs.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +13 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +15 -1
- package/dist/update.js.map +1 -1
- package/dist-web/assets/abnfDiagram-VCTEODGH-BCoAyQ8y.js +1 -0
- package/dist-web/assets/apl-gfMMiTNf.js +1 -0
- package/dist-web/assets/arc-Pry-7G1_.js +1 -0
- package/dist-web/assets/architecture-7GRP2DOG-C2Oj1AcH.js +1 -0
- package/dist-web/assets/architectureDiagram-5GKGNRK7-BbYvyste.js +36 -0
- package/dist-web/assets/array-BifhSqXX.js +1 -0
- package/dist-web/assets/asciiarmor-C_eisQCp.js +1 -0
- package/dist-web/assets/asn1-DCl_8MCW.js +1 -0
- package/dist-web/assets/asterisk-BEn0Cwpx.js +1 -0
- package/dist-web/assets/blockDiagram-I7D4REHJ-CGYIKoI0.js +129 -0
- package/dist-web/assets/brainfuck-D5EjA2JK.js +1 -0
- package/dist-web/assets/c4Diagram-7LVT6UL2-Cpat9Q1R.js +38 -0
- package/dist-web/assets/channel-Da0Eg5hZ.js +1 -0
- package/dist-web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist-web/assets/chunk-4HAMMTFA-D1PXshWJ.js +62 -0
- package/dist-web/assets/{chunk-FMBD7UC4-zW3dYoIJ.js → chunk-5VM5RSS4-ZNzvKenW.js} +2 -2
- package/dist-web/assets/chunk-75Z2AOVW-C19f30iG.js +2 -0
- package/dist-web/assets/chunk-DU6HZSFF-CV-KOPc1.js +125 -0
- package/dist-web/assets/chunk-F27PBJKO-DMfX40T1.js +1 -0
- package/dist-web/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
- package/dist-web/assets/chunk-GMAD6QVW-Nd9kACgc.js +72 -0
- package/dist-web/assets/chunk-GVQU2GXP-3G8ojGFe.js +1 -0
- package/dist-web/assets/chunk-IMKFNOWR-CmhGyHNd.js +231 -0
- package/dist-web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist-web/assets/chunk-L3NEJ4N5-Dsa1ER8Q.js +1 -0
- package/dist-web/assets/chunk-OSK3NFVY-ObCj9P3B.js +10 -0
- package/dist-web/assets/chunk-P2QGCYS3-CfNHqVGX.js +1 -0
- package/dist-web/assets/chunk-POPQ4Y6H-Cg5tk8by.js +1 -0
- package/dist-web/assets/chunk-PWAF6VOD-e6XMS-JR.js +1 -0
- package/dist-web/assets/chunk-SHT3W25Y-B1M0HI76.js +168 -0
- package/dist-web/assets/chunk-SVP7TREG-XLNHY4Z2.js +88 -0
- package/dist-web/assets/chunk-TICWLB2K-Bpm0S7rI.js +206 -0
- package/dist-web/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
- package/dist-web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist-web/assets/classDiagram-ZZMXUADV-CZI086hK.js +1 -0
- package/dist-web/assets/classDiagram-v2-VYDZK3BY-CZI086hK.js +1 -0
- package/dist-web/assets/clike-D_6L3tnO.js +1 -0
- package/dist-web/assets/clojure-BflJGmDX.js +1 -0
- package/dist-web/assets/cmake-84nbDH0I.js +1 -0
- package/dist-web/assets/cobol-DHLQUbN7.js +1 -0
- package/dist-web/assets/coffeescript-De_zI4J0.js +1 -0
- package/dist-web/assets/commonlisp-DgAKBe0r.js +1 -0
- package/dist-web/assets/cose-bilkent-JH36ORCC-BEpcWcmK.js +1 -0
- package/dist-web/assets/crystal-Bsdphhtm.js +1 -0
- package/dist-web/assets/css-CnaYoQib.js +1 -0
- package/dist-web/assets/cynefin-OW5HDTMX-B5ohibc7.js +1 -0
- package/dist-web/assets/cynefinDiagram-5FMLGOSQ-C1202n-x.js +62 -0
- package/dist-web/assets/cypher-p9eYesGn.js +1 -0
- package/dist-web/assets/cytoscape.esm-Yq6u8L66.js +321 -0
- package/dist-web/assets/d-DWkQyNnU.js +1 -0
- package/dist-web/assets/dagre-C-rnJpZF.js +1 -0
- package/dist-web/assets/dagre-GXQ25YYZ-CQlKKJXg.js +4 -0
- package/dist-web/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist-web/assets/diagram-S7CK7UJ4-DRf7iTH9.js +30 -0
- package/dist-web/assets/diagram-UQ7AKVKN-CxSX3b2_.js +41 -0
- package/dist-web/assets/diagram-VSXAHHWV-D9vrB5je.js +3 -0
- package/dist-web/assets/diagram-VX7I27RA-DTOqcpBV.js +24 -0
- package/dist-web/assets/diagram-Z3DM3KII-mlhRA4lR.js +24 -0
- package/dist-web/assets/diff-ChtP43wD.js +1 -0
- package/dist-web/assets/dist-B-O4eZD_.js +1 -0
- package/dist-web/assets/dist-B27L6oW0.js +1 -0
- package/dist-web/assets/dist-BKXbBuAc.js +1 -0
- package/dist-web/assets/dist-BPcC0hnc.js +9 -0
- package/dist-web/assets/dist-BgwxV8e_.js +1 -0
- package/dist-web/assets/dist-BlG40nRw.js +1 -0
- package/dist-web/assets/dist-Bqc6e86z.js +1 -0
- package/dist-web/assets/dist-BsvASXHG.js +1 -0
- package/dist-web/assets/dist-C15GN054.js +1 -0
- package/dist-web/assets/dist-C4QRmcTO.js +1 -0
- package/dist-web/assets/dist-C4lZy0XG.js +6 -0
- package/dist-web/assets/dist-CFLbFzpB.js +23 -0
- package/dist-web/assets/dist-CP6AUVtv.js +1 -0
- package/dist-web/assets/dist-CpYnHHz6.js +1 -0
- package/dist-web/assets/dist-DHnGmK79.js +1 -0
- package/dist-web/assets/dist-DYmIHv0n.js +13 -0
- package/dist-web/assets/dist-DauRO0RO.js +1 -0
- package/dist-web/assets/dist-Day7i26p.js +1 -0
- package/dist-web/assets/dist-Do_t70vA.js +1 -0
- package/dist-web/assets/dist-DqTG_rhR.js +2 -0
- package/dist-web/assets/dist-G8nWWSie.js +11 -0
- package/dist-web/assets/dist-QfYLz_N1.js +1 -0
- package/dist-web/assets/dist-TZhb2tj2.js +1 -0
- package/dist-web/assets/dist-Wr2Wo_KE.js +1 -0
- package/dist-web/assets/dist-kYRo7Db1.js +1 -0
- package/dist-web/assets/dist-kfqV1Lu-.js +2 -0
- package/dist-web/assets/dist-v5Q1xZ2K.js +1 -0
- package/dist-web/assets/dockerfile-DJkHQgkl.js +1 -0
- package/dist-web/assets/dtd-DTF72YNO.js +1 -0
- package/dist-web/assets/dylan-BCDoL_-p.js +1 -0
- package/dist-web/assets/ebnf-D4c0_ac3.js +1 -0
- package/dist-web/assets/ebnfDiagram-PWID7BFC-BhB_RJPM.js +1 -0
- package/dist-web/assets/ecl-BVIeWmwY.js +1 -0
- package/dist-web/assets/eiffel-BEjRio4Q.js +1 -0
- package/dist-web/assets/elm-Cshzl8qu.js +1 -0
- package/dist-web/assets/erDiagram-RLTQ6QDP-0wE1kPYz.js +99 -0
- package/dist-web/assets/erlang-BV0c4Hdn.js +1 -0
- package/dist-web/assets/esm-COEoxUhE.js +2 -0
- package/dist-web/assets/eventmodeling-NTZA5JFV-DzkXb9nA.js +1 -0
- package/dist-web/assets/factor-LKH5gN8L.js +1 -0
- package/dist-web/assets/fcl-ClOhbFR7.js +1 -0
- package/dist-web/assets/flowDiagram-HODETNUW-cgl8ipSt.js +1 -0
- package/dist-web/assets/forth-D6RzAl64.js +1 -0
- package/dist-web/assets/fortran-Bt6PBEDR.js +1 -0
- package/dist-web/assets/ganttDiagram-EL5Y4UJY-BsvlpHPt.js +292 -0
- package/dist-web/assets/gas-BHEdbvp9.js +1 -0
- package/dist-web/assets/gherkin-oBAE_ms0.js +1 -0
- package/dist-web/assets/gitGraph-4MIJSDKK-Dm82FCqy.js +1 -0
- package/dist-web/assets/gitGraphDiagram-WWUBYQGX-OPit_3vA.js +106 -0
- package/dist-web/assets/graphlib-DS17s2tU.js +1 -0
- package/dist-web/assets/groovy-CUpZ7dEl.js +1 -0
- package/dist-web/assets/haskell-BRsoo5mP.js +1 -0
- package/dist-web/assets/haxe-DjVOVvNP.js +1 -0
- package/dist-web/assets/http-CLVgA2GD.js +1 -0
- package/dist-web/assets/idl-BUZw3wgd.js +1 -0
- package/dist-web/assets/index-6XzOqB6r.js +103 -0
- package/dist-web/assets/index-CgIHUWsX.css +2 -0
- package/dist-web/assets/info-A6RAGUB7-8sl1XQWq.js +1 -0
- package/dist-web/assets/infoDiagram-27XIBGKW-DIpdnB7L.js +2 -0
- package/dist-web/assets/init-C-OQMol4.js +1 -0
- package/dist-web/assets/ishikawaDiagram-5VMMS53U-DpoLDWpF.js +70 -0
- package/dist-web/assets/javascript-Bu8mWwtz.js +1 -0
- package/dist-web/assets/journeyDiagram-3NMN7TZE-BAEbJ33U.js +139 -0
- package/dist-web/assets/julia-CQfgDRfP.js +1 -0
- package/dist-web/assets/kanban-definition-UXKFOSKX-CqzyUA0g.js +89 -0
- package/dist-web/assets/katex-ZlcWpGUi.js +257 -0
- package/dist-web/assets/line-CQRHrtV_.js +1 -0
- package/dist-web/assets/linear-SzVmGSw4.js +1 -0
- package/dist-web/assets/livescript-BXxG0Yva.js +1 -0
- package/dist-web/assets/lua-DPCEkfD_.js +1 -0
- package/dist-web/assets/mathematica-u8YMmbU0.js +1 -0
- package/dist-web/assets/mbox-BEMVcqjs.js +1 -0
- package/dist-web/assets/mermaid-parser.core-XlEvAPi2.js +7 -0
- package/dist-web/assets/mermaid.core-CvFTFi5O.js +44 -0
- package/dist-web/assets/mindmap-definition-YA3MSWOX-Ch0vW4Mc.js +96 -0
- package/dist-web/assets/{mirc-CjQqDB4T.js → mirc-C9zpzAZU.js} +1 -1
- package/dist-web/assets/mllike-B45xgp2S.js +1 -0
- package/dist-web/assets/modelica-CqHI_q-D.js +1 -0
- package/dist-web/assets/mscgen-qgSLujhx.js +1 -0
- package/dist-web/assets/mumps-BSzj6Xyz.js +1 -0
- package/dist-web/assets/{nginx-DdIZxoE0.js → nginx-8H7HVH4w.js} +1 -1
- package/dist-web/assets/nsis-CfSuhXs5.js +1 -0
- package/dist-web/assets/ntriples-B9h1VB_g.js +1 -0
- package/dist-web/assets/octave-D2Q8cUp1.js +1 -0
- package/dist-web/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist-web/assets/oz-BwTct_5T.js +1 -0
- package/dist-web/assets/packet-AYTQ26CC-lk-QU-qZ.js +1 -0
- package/dist-web/assets/pascal-Dp_KdFgX.js +1 -0
- package/dist-web/assets/path-fybaL0A-.js +1 -0
- package/dist-web/assets/pegDiagram-XKGWAZYB-Cgf4CYg-.js +1 -0
- package/dist-web/assets/perl-BkSkiI9m.js +1 -0
- package/dist-web/assets/pie-WAS4IAKB-dWyVjg9O.js +1 -0
- package/dist-web/assets/pieDiagram-E7YTZNPT-BDZxWO8n.js +39 -0
- package/dist-web/assets/pig-AF4Hwhju.js +1 -0
- package/dist-web/assets/powershell-BcIto6Sf.js +1 -0
- package/dist-web/assets/properties-C357ku5U.js +1 -0
- package/dist-web/assets/protobuf-D1ij_kNL.js +1 -0
- package/dist-web/assets/pug-3K-JMW4g.js +1 -0
- package/dist-web/assets/puppet-BZyAS7I4.js +1 -0
- package/dist-web/assets/python-B_1Jd8HM.js +1 -0
- package/dist-web/assets/q-B9NhS7L_.js +1 -0
- package/dist-web/assets/quadrantDiagram-AXDQQJYC-DCu9cx1_.js +7 -0
- package/dist-web/assets/r-CxDmU1I4.js +1 -0
- package/dist-web/assets/radar-RG4KPBEZ-CphwvESj.js +1 -0
- package/dist-web/assets/railroad-74A4TZTK-COTrvtXu.js +1 -0
- package/dist-web/assets/railroad-abnf-HS5TGJTU-CYfpugMj.js +1 -0
- package/dist-web/assets/railroad-ebnf-LZEXJU2U--Ui_4jm6.js +1 -0
- package/dist-web/assets/railroad-peg-WCYAUIDC-DzcveMan.js +1 -0
- package/dist-web/assets/railroadDiagram-O6MQD6OU-S9ZI1Adi.js +1 -0
- package/dist-web/assets/requirementDiagram-BXWQKSXE-a_3FElqS.js +84 -0
- package/dist-web/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist-web/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist-web/assets/rpm-gpDK5sbt.js +1 -0
- package/dist-web/assets/ruby-BjXV7-Qa.js +1 -0
- package/dist-web/assets/sankeyDiagram-P5KCCOFB-D-i6r8wy.js +40 -0
- package/dist-web/assets/{sas-B4kiWyti.js → sas-C2OyD2Y6.js} +1 -1
- package/dist-web/assets/scheme-D9qYN0V5.js +1 -0
- package/dist-web/assets/sequenceDiagram-WJ2MYXX4-D8hA2m0U.js +162 -0
- package/dist-web/assets/shell-DwuoZtxw.js +1 -0
- package/dist-web/assets/sieve-tLWDfEkZ.js +1 -0
- package/dist-web/assets/simple-mode-DXiLM1-G.js +1 -0
- package/dist-web/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist-web/assets/smalltalk-BLp5-jwC.js +1 -0
- package/dist-web/assets/solr-B78wSQJQ.js +1 -0
- package/dist-web/assets/sparql-Dz-mlCAC.js +1 -0
- package/dist-web/assets/spreadsheet-CTIncYxj.js +1 -0
- package/dist-web/assets/sql-DuQMFDvy.js +1 -0
- package/dist-web/assets/src-B6xuSHsQ.js +1 -0
- package/dist-web/assets/stateDiagram-D77RDMKH-C14W-8zT.js +1 -0
- package/dist-web/assets/stateDiagram-v2-MP3YSRHH-Dhuhsub3.js +1 -0
- package/dist-web/assets/stex-DNzd62Q_.js +1 -0
- package/dist-web/assets/stylus-BOVgKnng.js +1 -0
- package/dist-web/assets/swift-DKB6_1j6.js +1 -0
- package/dist-web/assets/swimlanes-42K2YHIH-FMv8YLkC.js +1 -0
- package/dist-web/assets/swimlanesDiagram-VR7AAH4N-O94a9jW3.js +8 -0
- package/dist-web/assets/tcl-BGfruO1u.js +1 -0
- package/dist-web/assets/textile-C4jVoHG1.js +1 -0
- package/dist-web/assets/tiddlywiki-VlUiK86J.js +1 -0
- package/dist-web/assets/tiki-AK6FwT8q.js +1 -0
- package/dist-web/assets/timeline-definition-24CTP7MA-DmBN45fY.js +120 -0
- package/dist-web/assets/toml-BPTmHmyx.js +1 -0
- package/dist-web/assets/treeView-Q6P3EWNA-C5-PUnPN.js +1 -0
- package/dist-web/assets/treemap-WGGIJYW6-C_veVYMi.js +1 -0
- package/dist-web/assets/troff-C2dAgh7P.js +1 -0
- package/dist-web/assets/ttcn-PPSTk1ow.js +1 -0
- package/dist-web/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
- package/dist-web/assets/turtle-Cx3rYX2g.js +1 -0
- package/dist-web/assets/vb-BthdM_4N.js +1 -0
- package/dist-web/assets/vbscript-D9f6rSsU.js +1 -0
- package/dist-web/assets/velocity-CqftRsjg.js +1 -0
- package/dist-web/assets/vennDiagram-4TSXK5OY-CiWHjSVc.js +34 -0
- package/dist-web/assets/verilog-CjI9Y6RI.js +1 -0
- package/dist-web/assets/vhdl-kS471soi.js +1 -0
- package/dist-web/assets/wardley-WFR3VGLG-FOCO1NlU.js +1 -0
- package/dist-web/assets/wardleyDiagram-VM6X3IG4-Da3kkvxV.js +78 -0
- package/dist-web/assets/webidl-D5jiJvOU.js +1 -0
- package/dist-web/assets/xquery-BKJx4mf9.js +1 -0
- package/dist-web/assets/xychartDiagram-S5SC5T6Z-DPxie1fH.js +7 -0
- package/dist-web/assets/yacas-CXXgtw0p.js +1 -0
- package/dist-web/assets/z80-Co3PbHdE.js +1 -0
- package/dist-web/index.html +6 -18
- package/dist-web/theme-guard.js +21 -0
- package/package.json +29 -24
- package/templates/strikethroo/config/config.yaml +2 -2
- package/dist-web/assets/apl-B4CMkyY2.js +0 -1
- package/dist-web/assets/arc-DA6tr0lU.js +0 -1
- package/dist-web/assets/architectureDiagram-3BPJPVTR-BEH2YihV.js +0 -36
- package/dist-web/assets/asciiarmor-Df11BRmG.js +0 -1
- package/dist-web/assets/asn1-EdZsLKOL.js +0 -1
- package/dist-web/assets/asterisk-B-8jnY81.js +0 -1
- package/dist-web/assets/blockDiagram-GPEHLZMM-jgUyFGxx.js +0 -132
- package/dist-web/assets/brainfuck-C4LP7Hcl.js +0 -1
- package/dist-web/assets/c4Diagram-AAUBKEIU-Do-n7I5O.js +0 -10
- package/dist-web/assets/channel-DJ33z8Wx.js +0 -1
- package/dist-web/assets/chunk-2J33WTMH-CgMvCn22.js +0 -1
- package/dist-web/assets/chunk-4BX2VUAB-FO8RQkkL.js +0 -1
- package/dist-web/assets/chunk-55IACEB6-DL_6pZjw.js +0 -1
- package/dist-web/assets/chunk-727SXJPM-DW7J3hiD.js +0 -206
- package/dist-web/assets/chunk-AQP2D5EJ-TPWnvFSC.js +0 -231
- package/dist-web/assets/chunk-ND2GUHAM-D0Nmkz3P.js +0 -1
- package/dist-web/assets/chunk-QZHKN3VN-BxgxRNWB.js +0 -1
- package/dist-web/assets/classDiagram-4FO5ZUOK--tqQjy0s.js +0 -1
- package/dist-web/assets/classDiagram-v2-Q7XG4LA2--tqQjy0s.js +0 -1
- package/dist-web/assets/clike-B9uivgTg.js +0 -1
- package/dist-web/assets/clojure-BMjYHr_A.js +0 -1
- package/dist-web/assets/cmake-BQqOBYOt.js +0 -1
- package/dist-web/assets/cobol-CWcv1MsR.js +0 -1
- package/dist-web/assets/coffeescript-S37ZYGWr.js +0 -1
- package/dist-web/assets/commonlisp-DBKNyK5s.js +0 -1
- package/dist-web/assets/cose-bilkent-S5V4N54A-ysHXC5GT.js +0 -1
- package/dist-web/assets/crystal-SjHAIU92.js +0 -1
- package/dist-web/assets/css-BnMrqG3P.js +0 -1
- package/dist-web/assets/cypher-C_CwsFkJ.js +0 -1
- package/dist-web/assets/cytoscape.esm-BHYC38rz.js +0 -331
- package/dist-web/assets/d-pRatUO7H.js +0 -1
- package/dist-web/assets/dagre-BM42HDAG-G_XIk8Dj.js +0 -4
- package/dist-web/assets/defaultLocale-CrowFXzY.js +0 -1
- package/dist-web/assets/diagram-2AECGRRQ-7HG2PLhr.js +0 -43
- package/dist-web/assets/diagram-5GNKFQAL-BdS1bF_R.js +0 -10
- package/dist-web/assets/diagram-KO2AKTUF-AFaiXGZQ.js +0 -3
- package/dist-web/assets/diagram-LMA3HP47-CTWrauj7.js +0 -24
- package/dist-web/assets/diagram-OG6HWLK6-BmBckGLa.js +0 -24
- package/dist-web/assets/diff-DbItnlRl.js +0 -1
- package/dist-web/assets/dockerfile-BKs6k2Af.js +0 -1
- package/dist-web/assets/dtd-DF_7sFjM.js +0 -1
- package/dist-web/assets/dylan-DwRh75JA.js +0 -1
- package/dist-web/assets/ebnf-CDyGwa7X.js +0 -1
- package/dist-web/assets/ecl-Cabwm37j.js +0 -1
- package/dist-web/assets/eiffel-CnydiIhH.js +0 -1
- package/dist-web/assets/elm-vLlmbW-K.js +0 -1
- package/dist-web/assets/erDiagram-TEJ5UH35-C0B2cXE-.js +0 -85
- package/dist-web/assets/erlang-BNw1qcRV.js +0 -1
- package/dist-web/assets/factor-kuTfRLto.js +0 -1
- package/dist-web/assets/fcl-Kvtd6kyn.js +0 -1
- package/dist-web/assets/flowDiagram-I6XJVG4X-CZ70tn_Q.js +0 -162
- package/dist-web/assets/forth-Ffai-XNe.js +0 -1
- package/dist-web/assets/fortran-DYz_wnZ1.js +0 -1
- package/dist-web/assets/ganttDiagram-6RSMTGT7-h7O-Xh-D.js +0 -292
- package/dist-web/assets/gas-Bneqetm1.js +0 -1
- package/dist-web/assets/gherkin-heZmZLOM.js +0 -1
- package/dist-web/assets/gitGraphDiagram-PVQCEYII-BDZixURw.js +0 -106
- package/dist-web/assets/graph-Dqkl27Ch.js +0 -1
- package/dist-web/assets/groovy-D9Dt4D0W.js +0 -1
- package/dist-web/assets/haskell-BWDZoCOh.js +0 -1
- package/dist-web/assets/haxe-H-WmDvRZ.js +0 -1
- package/dist-web/assets/http-DBlCnlav.js +0 -1
- package/dist-web/assets/idl-BEugSyMb.js +0 -1
- package/dist-web/assets/index-B-KlYMEI.js +0 -1
- package/dist-web/assets/index-B0a4C4Lb.js +0 -1
- package/dist-web/assets/index-B90tvIxG.js +0 -3
- package/dist-web/assets/index-BLXIwDvb.js +0 -1
- package/dist-web/assets/index-BjZYUODU.js +0 -1
- package/dist-web/assets/index-BmsczLPZ.js +0 -1
- package/dist-web/assets/index-BpU7RpGz.js +0 -1
- package/dist-web/assets/index-BwFIiA6J.js +0 -2
- package/dist-web/assets/index-C53NKwQ5.js +0 -1
- package/dist-web/assets/index-C73riuQN.js +0 -1
- package/dist-web/assets/index-C9RcHSPW.js +0 -1
- package/dist-web/assets/index-CEBV-m_B.js +0 -1
- package/dist-web/assets/index-CG1O2Vjg.js +0 -1
- package/dist-web/assets/index-CRL66wiY.js +0 -169
- package/dist-web/assets/index-CddJufqj.js +0 -1
- package/dist-web/assets/index-CrzBaw1K.css +0 -1
- package/dist-web/assets/index-CuBsNWzH.js +0 -13
- package/dist-web/assets/index-CuCAyRTw.js +0 -7
- package/dist-web/assets/index-CuUh3dvS.js +0 -1
- package/dist-web/assets/index-CwPWc9jQ.js +0 -1
- package/dist-web/assets/index-DHBbud_j.js +0 -1
- package/dist-web/assets/index-LLaxWDbg.js +0 -1
- package/dist-web/assets/index-XOpAvr0E.js +0 -13
- package/dist-web/assets/index-aX67gnlS.js +0 -2
- package/dist-web/assets/index-lkaAPBmM.js +0 -6
- package/dist-web/assets/index-pUkuW0EP.js +0 -1
- package/dist-web/assets/index-sbfqG9cy.js +0 -1
- package/dist-web/assets/index-udFbjSlZ.js +0 -2
- package/dist-web/assets/index-yjEPVwyN.js +0 -1
- package/dist-web/assets/infoDiagram-5YYISTIA-CKXHxMh_.js +0 -2
- package/dist-web/assets/init-Gi6I4Gst.js +0 -1
- package/dist-web/assets/ishikawaDiagram-YF4QCWOH-DGancylI.js +0 -70
- package/dist-web/assets/javascript-qCveANmP.js +0 -1
- package/dist-web/assets/journeyDiagram-JHISSGLW-DifvKy-w.js +0 -139
- package/dist-web/assets/julia-DuME0IfC.js +0 -1
- package/dist-web/assets/kanban-definition-UN3LZRKU-ByC-4YyY.js +0 -89
- package/dist-web/assets/katex-C5jXJg4s.js +0 -257
- package/dist-web/assets/layout-DO9UvE1P.js +0 -1
- package/dist-web/assets/linear-Cm0Y2OWD.js +0 -1
- package/dist-web/assets/livescript-BwQOo05w.js +0 -1
- package/dist-web/assets/lua-VAEuO923.js +0 -1
- package/dist-web/assets/mathematica-DTrFuWx2.js +0 -1
- package/dist-web/assets/mbox-CNhZ1qSd.js +0 -1
- package/dist-web/assets/mermaid.core-DBlLt9sa.js +0 -307
- package/dist-web/assets/mindmap-definition-RKZ34NQL-tkThiQWd.js +0 -96
- package/dist-web/assets/mllike-CXdrOF99.js +0 -1
- package/dist-web/assets/modelica-Dc1JOy9r.js +0 -1
- package/dist-web/assets/mscgen-BA5vi2Kp.js +0 -1
- package/dist-web/assets/mumps-BT43cFF4.js +0 -1
- package/dist-web/assets/nsis-LdVXkNf5.js +0 -1
- package/dist-web/assets/ntriples-BfvgReVJ.js +0 -1
- package/dist-web/assets/octave-Ck1zUtKM.js +0 -1
- package/dist-web/assets/ordinal-Cboi1Yqb.js +0 -1
- package/dist-web/assets/oz-BzwKVEFT.js +0 -1
- package/dist-web/assets/pascal--L3eBynH.js +0 -1
- package/dist-web/assets/perl-CdXCOZ3F.js +0 -1
- package/dist-web/assets/pieDiagram-4H26LBE5-BXGBVjcj.js +0 -30
- package/dist-web/assets/pig-CevX1Tat.js +0 -1
- package/dist-web/assets/powershell-CFHJl5sT.js +0 -1
- package/dist-web/assets/properties-C78fOPTZ.js +0 -1
- package/dist-web/assets/protobuf-ChK-085T.js +0 -1
- package/dist-web/assets/pug-DukmZTjD.js +0 -1
- package/dist-web/assets/puppet-DMA9R1ak.js +0 -1
- package/dist-web/assets/python-BuPzkPfP.js +0 -1
- package/dist-web/assets/q-pXgVlZs6.js +0 -1
- package/dist-web/assets/quadrantDiagram-W4KKPZXB-DnF0iEBo.js +0 -7
- package/dist-web/assets/r-DUYO_cvP.js +0 -1
- package/dist-web/assets/requirementDiagram-4Y6WPE33-BooGjmPb.js +0 -84
- package/dist-web/assets/rpm-CTu-6PCP.js +0 -1
- package/dist-web/assets/ruby-B2Rjki9n.js +0 -1
- package/dist-web/assets/sankeyDiagram-5OEKKPKP-7le5uaC7.js +0 -40
- package/dist-web/assets/scheme-C41bIUwD.js +0 -1
- package/dist-web/assets/sequenceDiagram-3UESZ5HK-B9GCKMDd.js +0 -162
- package/dist-web/assets/shell-CjFT_Tl9.js +0 -1
- package/dist-web/assets/sieve-C3Gn_uJK.js +0 -1
- package/dist-web/assets/simple-mode-GW_nhZxv.js +0 -1
- package/dist-web/assets/smalltalk-CnHTOXQT.js +0 -1
- package/dist-web/assets/solr-DehyRSwq.js +0 -1
- package/dist-web/assets/sparql-DkYu6x3z.js +0 -1
- package/dist-web/assets/spreadsheet-BCZA_wO0.js +0 -1
- package/dist-web/assets/sql-D0XecflT.js +0 -1
- package/dist-web/assets/stateDiagram-AJRCARHV-CO2qL8R6.js +0 -1
- package/dist-web/assets/stateDiagram-v2-BHNVJYJU-DUkgqBTd.js +0 -1
- package/dist-web/assets/stex-C3f8Ysf7.js +0 -1
- package/dist-web/assets/stylus-B533Al4x.js +0 -1
- package/dist-web/assets/swift-BzpIVaGY.js +0 -1
- package/dist-web/assets/tcl-DVfN8rqt.js +0 -1
- package/dist-web/assets/textile-CnDTJFAw.js +0 -1
- package/dist-web/assets/tiddlywiki-DO-Gjzrf.js +0 -1
- package/dist-web/assets/tiki-DGYXhP31.js +0 -1
- package/dist-web/assets/timeline-definition-PNZ67QCA-FLzWM85F.js +0 -120
- package/dist-web/assets/toml-Bm5Em-hy.js +0 -1
- package/dist-web/assets/troff-wAsdV37c.js +0 -1
- package/dist-web/assets/ttcn-CfJYG6tj.js +0 -1
- package/dist-web/assets/ttcn-cfg-B9xdYoR4.js +0 -1
- package/dist-web/assets/turtle-B1tBg_DP.js +0 -1
- package/dist-web/assets/vb-CmGdzxic.js +0 -1
- package/dist-web/assets/vbscript-BuJXcnF6.js +0 -1
- package/dist-web/assets/velocity-D8B20fx6.js +0 -1
- package/dist-web/assets/vennDiagram-CIIHVFJN-Dt6p6Ib1.js +0 -34
- package/dist-web/assets/verilog-C6RDOZhf.js +0 -1
- package/dist-web/assets/vhdl-lSbBsy5d.js +0 -1
- package/dist-web/assets/wardley-L42UT6IY-D6k_B5It.js +0 -173
- package/dist-web/assets/wardleyDiagram-YWT4CUSO-jT3ItNLi.js +0 -78
- package/dist-web/assets/webidl-ZXfAyPTL.js +0 -1
- package/dist-web/assets/xquery-CQfU5ijd.js +0 -1
- package/dist-web/assets/xychartDiagram-2RQKCTM6-CpM6UhUO.js +0 -7
- package/dist-web/assets/yacas-BJ4BC0dw.js +0 -1
- package/dist-web/assets/z80-Hz9HOZM7.js +0 -1
package/dist/serve/server.js
CHANGED
|
@@ -14,6 +14,23 @@
|
|
|
14
14
|
* external self-review binary for a validated in-workspace plan path;
|
|
15
15
|
* - platform-aware browser auto-open on startup.
|
|
16
16
|
*
|
|
17
|
+
* The server binds loopback unless the caller passes another IP address in
|
|
18
|
+
* `host`, and every request — static, API, and the `apiHandlers` extension
|
|
19
|
+
* point — passes through {@link guardRequest} (Host authority, request-target
|
|
20
|
+
* parsing) and a single
|
|
21
|
+
* exception boundary before any route runs. Routes are matched by path first
|
|
22
|
+
* and method second, so a wrong method answers `405` and a mutation path never
|
|
23
|
+
* reaches a read handler. Body-bearing mutations require `application/json`.
|
|
24
|
+
*
|
|
25
|
+
* Mutations are additionally gated on a per-start capability: a random token
|
|
26
|
+
* minted in {@link startServer}, handed out only by `GET /api/session` to a
|
|
27
|
+
* same-origin request, and required in {@link CAPABILITY_HEADER}. Together with
|
|
28
|
+
* the Origin/fetch-metadata check this closes CSRF and DNS-rebinding paths from
|
|
29
|
+
* a browser. It is a browser/request safeguard only: it does not isolate the
|
|
30
|
+
* server from another local process that can already read the user's files
|
|
31
|
+
* (and so the token). No cookies, no accounts, no CORS — the token lives in
|
|
32
|
+
* the page's memory and is never persisted, logged, or placed in a URL.
|
|
33
|
+
*
|
|
17
34
|
* The SSE change stream (`GET /api/events`) is added by a separate module that
|
|
18
35
|
* hooks into the `apiHandlers` extension point below, so this module stays free
|
|
19
36
|
* of file-watching concerns. Node built-ins only — no runtime dependency, no
|
|
@@ -53,10 +70,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
53
70
|
};
|
|
54
71
|
})();
|
|
55
72
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
-
exports.startServer = exports.defaultAssetsDir = void 0;
|
|
73
|
+
exports.startServer = exports.hostAddressesFor = exports.isLoopbackAddress = exports.CONTENT_SECURITY_POLICY = exports.guardRequest = exports.defaultAssetsDir = exports.CAPABILITY_HEADER = void 0;
|
|
57
74
|
const http = __importStar(require("http"));
|
|
58
75
|
const fs = __importStar(require("fs"));
|
|
59
76
|
const path = __importStar(require("path"));
|
|
77
|
+
const crypto = __importStar(require("crypto"));
|
|
78
|
+
const os = __importStar(require("os"));
|
|
60
79
|
const child_process_1 = require("child_process");
|
|
61
80
|
const url_1 = require("url");
|
|
62
81
|
const workspace_model_1 = require("./workspace-model");
|
|
@@ -64,6 +83,8 @@ const events_1 = require("./events");
|
|
|
64
83
|
const self_review_1 = require("./self-review");
|
|
65
84
|
const archive_1 = require("./archive");
|
|
66
85
|
const config_write_1 = require("./config-write");
|
|
86
|
+
/** Request header carrying the mutation capability. */
|
|
87
|
+
exports.CAPABILITY_HEADER = 'X-Strikethroo-Capability';
|
|
67
88
|
const MIME_TYPES = {
|
|
68
89
|
'.html': 'text/html; charset=utf-8',
|
|
69
90
|
'.js': 'text/javascript; charset=utf-8',
|
|
@@ -108,14 +129,117 @@ const deriveProject = (root) => {
|
|
|
108
129
|
*/
|
|
109
130
|
const defaultAssetsDir = () => path.resolve(__dirname, '..', '..', 'dist-web');
|
|
110
131
|
exports.defaultAssetsDir = defaultAssetsDir;
|
|
111
|
-
const sendJson = (res, status, body) => {
|
|
132
|
+
const sendJson = (res, status, body, headers = {}) => {
|
|
112
133
|
const payload = JSON.stringify(body);
|
|
113
134
|
res.writeHead(status, {
|
|
114
135
|
'Content-Type': 'application/json; charset=utf-8',
|
|
115
136
|
'Content-Length': Buffer.byteLength(payload),
|
|
137
|
+
...headers,
|
|
116
138
|
});
|
|
117
139
|
res.end(payload);
|
|
118
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* Sends an error response if one can still be sent. Once headers are out the
|
|
143
|
+
* only honest option is to drop the connection, never a half-written body.
|
|
144
|
+
*/
|
|
145
|
+
const sendError = (res, status, body) => {
|
|
146
|
+
if (res.headersSent) {
|
|
147
|
+
res.destroy();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
sendJson(res, status, body);
|
|
151
|
+
};
|
|
152
|
+
const reject = (status, error) => ({
|
|
153
|
+
ok: false,
|
|
154
|
+
status,
|
|
155
|
+
body: { error },
|
|
156
|
+
});
|
|
157
|
+
const BAD_REQUEST = reject(400, 'Bad request.');
|
|
158
|
+
const MISDIRECTED = reject(421, 'Misdirected request.');
|
|
159
|
+
const FORBIDDEN = reject(403, 'Forbidden.');
|
|
160
|
+
/**
|
|
161
|
+
* Runs before routing, for every request. Refuses any `Host` that is not one
|
|
162
|
+
* of the server's own loopback authorities (DNS-rebinding defense), then parses
|
|
163
|
+
* the request target: origin-form (`/path`) or absolute-form naming an allowed
|
|
164
|
+
* authority; anything else, including a target whose path does not
|
|
165
|
+
* percent-decode, is a `400`. The returned `pathname` is the only thing routing
|
|
166
|
+
* consumes.
|
|
167
|
+
*/
|
|
168
|
+
const guardRequest = (req, ctx) => {
|
|
169
|
+
const host = (req.headers.host ?? '').toLowerCase();
|
|
170
|
+
if (!ctx.allowedHosts.has(host))
|
|
171
|
+
return MISDIRECTED;
|
|
172
|
+
const target = req.url ?? '';
|
|
173
|
+
let url;
|
|
174
|
+
try {
|
|
175
|
+
if (target.startsWith('/')) {
|
|
176
|
+
// `//authority/path` is scheme-relative, not origin-form; refuse it so the
|
|
177
|
+
// routed path is never taken from a smuggled authority.
|
|
178
|
+
if (target.startsWith('//'))
|
|
179
|
+
return BAD_REQUEST;
|
|
180
|
+
url = new url_1.URL(target, 'http://127.0.0.1');
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
url = new url_1.URL(target);
|
|
184
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
185
|
+
return BAD_REQUEST;
|
|
186
|
+
if (!ctx.allowedHosts.has(url.host.toLowerCase()))
|
|
187
|
+
return MISDIRECTED;
|
|
188
|
+
}
|
|
189
|
+
decodeURIComponent(url.pathname);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return BAD_REQUEST;
|
|
193
|
+
}
|
|
194
|
+
return { ok: true, pathname: url.pathname };
|
|
195
|
+
};
|
|
196
|
+
exports.guardRequest = guardRequest;
|
|
197
|
+
/**
|
|
198
|
+
* Same-origin policy for the session bootstrap and every mutation. The request
|
|
199
|
+
* must carry no cross-site fetch metadata (`Sec-Fetch-Site` absent, `same-origin`
|
|
200
|
+
* or `none` — a page we served, or a direct navigation) and any `Origin` present
|
|
201
|
+
* must be `http://<allowed authority>`. `same-site`, `cross-site`, `null`, and a
|
|
202
|
+
* foreign or https Origin are all refused. Both headers are forbidden request
|
|
203
|
+
* headers in browsers, so a page cannot forge them; non-browser callers are
|
|
204
|
+
* what the capability is for.
|
|
205
|
+
*/
|
|
206
|
+
const guardSameOrigin = (req, ctx) => {
|
|
207
|
+
const site = req.headers['sec-fetch-site'];
|
|
208
|
+
if (site !== undefined && site !== 'same-origin' && site !== 'none')
|
|
209
|
+
return FORBIDDEN;
|
|
210
|
+
const origin = req.headers.origin;
|
|
211
|
+
if (origin === undefined)
|
|
212
|
+
return null;
|
|
213
|
+
const lower = origin.toLowerCase();
|
|
214
|
+
for (const host of ctx.allowedHosts) {
|
|
215
|
+
if (lower === `http://${host}`)
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return FORBIDDEN;
|
|
219
|
+
};
|
|
220
|
+
/** Constant-time check that {@link CAPABILITY_HEADER} carries this instance's token. */
|
|
221
|
+
const guardCapability = (req, ctx) => {
|
|
222
|
+
const header = req.headers[exports.CAPABILITY_HEADER.toLowerCase()];
|
|
223
|
+
if (typeof header !== 'string')
|
|
224
|
+
return FORBIDDEN;
|
|
225
|
+
const presented = Buffer.from(header);
|
|
226
|
+
const expected = Buffer.from(ctx.capability);
|
|
227
|
+
if (presented.length !== expected.length || !crypto.timingSafeEqual(presented, expected)) {
|
|
228
|
+
return FORBIDDEN;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
};
|
|
232
|
+
/** True when `Content-Type` is `application/json`, any case, parameters allowed. */
|
|
233
|
+
const isJson = (req) => {
|
|
234
|
+
const header = req.headers['content-type'];
|
|
235
|
+
if (typeof header !== 'string')
|
|
236
|
+
return false;
|
|
237
|
+
const mediaType = header.split(';', 1)[0]?.trim().toLowerCase();
|
|
238
|
+
return mediaType === 'application/json';
|
|
239
|
+
};
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Route handlers
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
119
243
|
/** The composite plan key grammar: `{id}--{slug}`, e.g. `28--plan-name`. */
|
|
120
244
|
const COMPOSITE_KEY = /^[0-9]+--[a-z0-9-]+$/;
|
|
121
245
|
/**
|
|
@@ -160,130 +284,162 @@ const parseArchivePlanId = (pathname) => {
|
|
|
160
284
|
return null;
|
|
161
285
|
return validateCompositeKey(match[1]);
|
|
162
286
|
};
|
|
163
|
-
/**
|
|
164
|
-
* Handles `POST /api/plans/:id/archive`: delegates entirely to the {@link
|
|
165
|
-
* archivePlan} operation and maps its discriminated result to status codes
|
|
166
|
-
* without duplicating validation. Returns `true` once it owns the response.
|
|
167
|
-
*/
|
|
168
|
-
const handleArchive = (res, root, pathname) => {
|
|
169
|
-
const key = parseArchivePlanId(pathname);
|
|
170
|
-
if (key === null) {
|
|
171
|
-
sendJson(res, 400, { error: 'Invalid plan id.' });
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
(0, archive_1.archivePlan)(root, key)
|
|
175
|
-
.then(result => {
|
|
176
|
-
if (result.ok) {
|
|
177
|
-
sendJson(res, 200, result.plan);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
switch (result.reason) {
|
|
181
|
-
case 'not-found':
|
|
182
|
-
sendJson(res, 404, { error: result.message });
|
|
183
|
-
return;
|
|
184
|
-
case 'not-done':
|
|
185
|
-
case 'already-archived':
|
|
186
|
-
case 'destination-exists':
|
|
187
|
-
sendJson(res, 409, { error: result.message });
|
|
188
|
-
return;
|
|
189
|
-
default:
|
|
190
|
-
// fs-error: a safe, fixed message — never leak internals.
|
|
191
|
-
sendJson(res, 500, { error: 'Failed to archive plan.' });
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
.catch(() => {
|
|
195
|
-
sendJson(res, 500, { error: 'Failed to archive plan.' });
|
|
196
|
-
});
|
|
197
|
-
return true;
|
|
198
|
-
};
|
|
199
|
-
/** Handles the built-in read-only API. Returns `true` when the request matched. */
|
|
200
|
-
const handleApi = (res, root, pathname) => {
|
|
201
|
-
try {
|
|
202
|
-
if (pathname === '/api/plans' || pathname === '/api/plans/') {
|
|
203
|
-
sendJson(res, 200, (0, workspace_model_1.getWorkspaceModel)(root).plans);
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
if (pathname === '/api/config' || pathname === '/api/config/') {
|
|
207
|
-
sendJson(res, 200, (0, workspace_model_1.getConfig)(root));
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
if (pathname === '/api/capabilities' || pathname === '/api/capabilities/') {
|
|
211
|
-
sendJson(res, 200, { selfReview: (0, self_review_1.isSelfReviewAvailable)(), project: deriveProject(root) });
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
if (/^\/api\/plans\/[^/]+\/?$/.test(pathname)) {
|
|
215
|
-
const key = parsePlanId(pathname);
|
|
216
|
-
if (key === null) {
|
|
217
|
-
sendJson(res, 404, { error: 'Invalid plan id' });
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
const detail = (0, workspace_model_1.getPlanDetail)(root, key);
|
|
221
|
-
if (!detail) {
|
|
222
|
-
sendJson(res, 404, { error: `Plan ${key} not found` });
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
sendJson(res, 200, detail);
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
// Malformed workspace data: concise message, never a raw stack trace.
|
|
232
|
-
sendJson(res, 500, {
|
|
233
|
-
error: `Failed to read workspace: ${err instanceof Error ? err.message : String(err)}`,
|
|
234
|
-
});
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
287
|
/**
|
|
239
288
|
* Max accepted request-body size (bytes). The self-review body is tiny JSON,
|
|
240
289
|
* but the config-write route carries whole markdown files, which can exceed
|
|
241
290
|
* 64 KiB once edited — so the cap is 1 MiB.
|
|
242
291
|
*/
|
|
243
292
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
244
|
-
/**
|
|
293
|
+
/** A body-reading failure that already knows its HTTP status. */
|
|
294
|
+
class BodyError extends Error {
|
|
295
|
+
constructor(status, message) {
|
|
296
|
+
super(message);
|
|
297
|
+
this.status = status;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Reads and JSON-parses a request body. Rejects with a {@link BodyError}: `413`
|
|
302
|
+
* when the declared or actual size exceeds {@link MAX_BODY_BYTES}, `400` when
|
|
303
|
+
* the body is empty or not JSON. An oversized body is answered, not reset, so
|
|
304
|
+
* the client sees the `413`; Node drains the remainder after the response.
|
|
305
|
+
*/
|
|
245
306
|
const readJsonBody = (req) => new Promise((resolve, reject) => {
|
|
307
|
+
const tooLarge = new BodyError(413, 'Request body too large');
|
|
308
|
+
const declared = Number(req.headers['content-length']);
|
|
309
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
310
|
+
reject(tooLarge);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
246
313
|
let size = 0;
|
|
314
|
+
let failed = false;
|
|
247
315
|
const chunks = [];
|
|
248
316
|
req.on('data', (chunk) => {
|
|
317
|
+
if (failed)
|
|
318
|
+
return;
|
|
249
319
|
size += chunk.length;
|
|
250
320
|
if (size > MAX_BODY_BYTES) {
|
|
251
|
-
|
|
252
|
-
|
|
321
|
+
failed = true;
|
|
322
|
+
chunks.length = 0;
|
|
323
|
+
reject(tooLarge);
|
|
253
324
|
return;
|
|
254
325
|
}
|
|
255
326
|
chunks.push(chunk);
|
|
256
327
|
});
|
|
257
328
|
req.on('end', () => {
|
|
329
|
+
if (failed)
|
|
330
|
+
return;
|
|
258
331
|
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
259
332
|
if (raw === '') {
|
|
260
|
-
|
|
333
|
+
reject(new BodyError(400, 'Request body must be a JSON document'));
|
|
261
334
|
return;
|
|
262
335
|
}
|
|
263
336
|
try {
|
|
264
337
|
resolve(JSON.parse(raw));
|
|
265
338
|
}
|
|
266
339
|
catch {
|
|
267
|
-
reject(new
|
|
340
|
+
reject(new BodyError(400, 'Invalid JSON body'));
|
|
268
341
|
}
|
|
269
342
|
});
|
|
270
343
|
req.on('error', reject);
|
|
271
344
|
});
|
|
345
|
+
const bodyErrorStatus = (err) => (err instanceof BodyError ? err.status : 500);
|
|
346
|
+
const READ_METHODS = ['GET', 'HEAD'];
|
|
347
|
+
/** Malformed workspace data is a concise `500`, never a stack trace. */
|
|
348
|
+
const sendReadFailure = (res, err) => {
|
|
349
|
+
sendJson(res, 500, {
|
|
350
|
+
error: `Failed to read workspace: ${err instanceof Error ? err.message : String(err)}`,
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
/** Wraps a synchronous workspace read as a `200` JSON route. */
|
|
354
|
+
const readRoute = (read) => (_req, res, ctx) => {
|
|
355
|
+
let body;
|
|
356
|
+
try {
|
|
357
|
+
body = read(ctx);
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
sendReadFailure(res, err);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
sendJson(res, 200, body);
|
|
364
|
+
};
|
|
365
|
+
const handlePlanDetail = (_req, res, ctx) => {
|
|
366
|
+
const key = parsePlanId(ctx.pathname);
|
|
367
|
+
if (key === null) {
|
|
368
|
+
sendJson(res, 404, { error: 'Invalid plan id' });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
let detail;
|
|
372
|
+
try {
|
|
373
|
+
detail = (0, workspace_model_1.getPlanDetail)(ctx.root, key);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
sendReadFailure(res, err);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (!detail) {
|
|
380
|
+
sendJson(res, 404, { error: `Plan ${key} not found` });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
sendJson(res, 200, detail);
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Handles `POST /api/plans/:id/archive`: delegates entirely to the {@link
|
|
387
|
+
* archivePlan} operation and maps its discriminated result to status codes
|
|
388
|
+
* without duplicating validation. A JSON body is required; `{}` is enough.
|
|
389
|
+
*/
|
|
390
|
+
const handleArchive = (req, res, ctx) => {
|
|
391
|
+
const key = parseArchivePlanId(ctx.pathname);
|
|
392
|
+
if (key === null) {
|
|
393
|
+
sendJson(res, 400, { error: 'Invalid plan id.' });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
readJsonBody(req)
|
|
397
|
+
.then(() => (0, archive_1.archivePlan)(ctx.root, key))
|
|
398
|
+
.then(result => {
|
|
399
|
+
if (result.ok) {
|
|
400
|
+
sendJson(res, 200, result.plan);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
switch (result.reason) {
|
|
404
|
+
case 'not-found':
|
|
405
|
+
sendJson(res, 404, { error: result.message });
|
|
406
|
+
return;
|
|
407
|
+
case 'not-done':
|
|
408
|
+
case 'already-archived':
|
|
409
|
+
case 'destination-exists':
|
|
410
|
+
sendJson(res, 409, { error: result.message });
|
|
411
|
+
return;
|
|
412
|
+
default:
|
|
413
|
+
// fs-error: a safe, fixed message — never leak internals.
|
|
414
|
+
sendJson(res, 500, { error: 'Failed to archive plan.' });
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
.catch((err) => {
|
|
418
|
+
const status = bodyErrorStatus(err);
|
|
419
|
+
sendError(res, status, {
|
|
420
|
+
error: err instanceof BodyError ? err.message : 'Failed to archive plan.',
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
};
|
|
272
424
|
/**
|
|
273
425
|
* Handles `POST /api/self-review`: launches the external self-review binary for
|
|
274
|
-
* the requested plan path.
|
|
426
|
+
* the requested plan path.
|
|
275
427
|
*/
|
|
276
|
-
const handleSelfReview = (req, res,
|
|
428
|
+
const handleSelfReview = (req, res, ctx) => {
|
|
277
429
|
readJsonBody(req)
|
|
278
430
|
.then(body => {
|
|
279
431
|
const clientPath = body.path;
|
|
280
|
-
|
|
281
|
-
|
|
432
|
+
return (0, self_review_1.launchSelfReview)(ctx.root, typeof clientPath === 'string' ? clientPath : '', ctx.selfReviewDeps);
|
|
433
|
+
})
|
|
434
|
+
.then(result => {
|
|
435
|
+
sendJson(res, result.status, result.body, result.headers);
|
|
282
436
|
})
|
|
283
437
|
.catch((err) => {
|
|
284
|
-
|
|
438
|
+
sendError(res, bodyErrorStatus(err), {
|
|
439
|
+
ok: false,
|
|
440
|
+
error: err instanceof Error ? err.message : 'Failed to launch self-review.',
|
|
441
|
+
});
|
|
285
442
|
});
|
|
286
|
-
return true;
|
|
287
443
|
};
|
|
288
444
|
/** Parses `/api/config/:kind/:id` -> `{ kind, id }`, or `null` if malformed. */
|
|
289
445
|
const parseConfigTarget = (pathname) => {
|
|
@@ -301,14 +457,13 @@ const parseConfigTarget = (pathname) => {
|
|
|
301
457
|
* Handles `PUT /api/config/:kind/:id`: reads `{ content }` from the JSON body,
|
|
302
458
|
* delegates to the {@link writeConfigFile} guard, and maps its discriminated
|
|
303
459
|
* result to status codes. On success returns the refreshed config slice so the
|
|
304
|
-
* client can update without a second fetch.
|
|
305
|
-
* response.
|
|
460
|
+
* client can update without a second fetch.
|
|
306
461
|
*/
|
|
307
|
-
const handleConfigWrite = (req, res,
|
|
308
|
-
const target = parseConfigTarget(pathname);
|
|
462
|
+
const handleConfigWrite = (req, res, ctx) => {
|
|
463
|
+
const target = parseConfigTarget(ctx.pathname);
|
|
309
464
|
if (!target) {
|
|
310
465
|
sendJson(res, 400, { error: 'Invalid config path.' });
|
|
311
|
-
return
|
|
466
|
+
return;
|
|
312
467
|
}
|
|
313
468
|
readJsonBody(req)
|
|
314
469
|
.then(body => {
|
|
@@ -317,9 +472,9 @@ const handleConfigWrite = (req, res, root, pathname) => {
|
|
|
317
472
|
sendJson(res, 400, { error: 'Request body must include string "content".' });
|
|
318
473
|
return;
|
|
319
474
|
}
|
|
320
|
-
return (0, config_write_1.writeConfigFile)(root, target.kind, target.id, content).then(result => {
|
|
475
|
+
return (0, config_write_1.writeConfigFile)(ctx.root, target.kind, target.id, content).then(result => {
|
|
321
476
|
if (result.ok) {
|
|
322
|
-
sendJson(res, 200, (0, workspace_model_1.getConfig)(root));
|
|
477
|
+
sendJson(res, 200, (0, workspace_model_1.getConfig)(ctx.root));
|
|
323
478
|
return;
|
|
324
479
|
}
|
|
325
480
|
switch (result.reason) {
|
|
@@ -337,13 +492,135 @@ const handleConfigWrite = (req, res, root, pathname) => {
|
|
|
337
492
|
});
|
|
338
493
|
})
|
|
339
494
|
.catch((err) => {
|
|
340
|
-
|
|
495
|
+
sendError(res, bodyErrorStatus(err), {
|
|
496
|
+
error: err instanceof BodyError ? err.message : 'Failed to write config file.',
|
|
497
|
+
});
|
|
341
498
|
});
|
|
342
|
-
return true;
|
|
343
499
|
};
|
|
344
|
-
/**
|
|
500
|
+
/**
|
|
501
|
+
* The built-in API, most specific paths first. A path is matched before its
|
|
502
|
+
* method is checked, so a known path with the wrong method is a `405` and a
|
|
503
|
+
* mutation path can never fall through to a read handler.
|
|
504
|
+
*/
|
|
505
|
+
const API_ROUTES = [
|
|
506
|
+
{
|
|
507
|
+
pattern: /^\/api\/plans\/?$/,
|
|
508
|
+
methods: READ_METHODS,
|
|
509
|
+
handle: readRoute(ctx => (0, workspace_model_1.getWorkspaceModel)(ctx.root).plans),
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
pattern: /^\/api\/config\/?$/,
|
|
513
|
+
methods: READ_METHODS,
|
|
514
|
+
handle: readRoute(ctx => (0, workspace_model_1.getConfig)(ctx.root)),
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
pattern: /^\/api\/capabilities\/?$/,
|
|
518
|
+
methods: READ_METHODS,
|
|
519
|
+
handle: readRoute(ctx => ({
|
|
520
|
+
selfReview: (0, self_review_1.isSelfReviewAvailable)(),
|
|
521
|
+
project: deriveProject(ctx.root),
|
|
522
|
+
})),
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
pattern: /^\/api\/events\/?$/,
|
|
526
|
+
methods: ['GET'],
|
|
527
|
+
handle: (req, res, ctx) => {
|
|
528
|
+
ctx.events.apiHandler(req, res, { pathname: ctx.pathname });
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
// Capability bootstrap: the only response that ever carries the token. A
|
|
533
|
+
// cross-origin page cannot read it (no CORS headers), a cross-site fetch is
|
|
534
|
+
// refused outright, and the Host guard already stopped DNS rebinding.
|
|
535
|
+
pattern: /^\/api\/session\/?$/,
|
|
536
|
+
methods: ['GET'],
|
|
537
|
+
sameOrigin: true,
|
|
538
|
+
handle: (_req, res, ctx) => {
|
|
539
|
+
sendJson(res, 200, { token: ctx.guard.capability }, { 'Cache-Control': 'no-store' });
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
pattern: /^\/api\/plans\/[^/]+\/archive\/?$/,
|
|
544
|
+
methods: ['POST'],
|
|
545
|
+
mutation: true,
|
|
546
|
+
requiresJson: true,
|
|
547
|
+
handle: handleArchive,
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
pattern: /^\/api\/self-review\/?$/,
|
|
551
|
+
methods: ['POST'],
|
|
552
|
+
mutation: true,
|
|
553
|
+
requiresJson: true,
|
|
554
|
+
handle: handleSelfReview,
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
pattern: /^\/api\/config\/[^/]+\/[^/]+\/?$/,
|
|
558
|
+
methods: ['PUT'],
|
|
559
|
+
mutation: true,
|
|
560
|
+
requiresJson: true,
|
|
561
|
+
handle: handleConfigWrite,
|
|
562
|
+
},
|
|
563
|
+
{ pattern: /^\/api\/plans\/[^/]+\/?$/, methods: READ_METHODS, handle: handlePlanDetail },
|
|
564
|
+
];
|
|
565
|
+
/**
|
|
566
|
+
* Routes a guarded `/api/*` request through {@link API_ROUTES}. Check order,
|
|
567
|
+
* after the Host guard that already ran: path, method, Origin and fetch
|
|
568
|
+
* metadata, capability, media type, then the handler — which is the first thing
|
|
569
|
+
* to read the body, touch a file, or launch a process.
|
|
570
|
+
*/
|
|
571
|
+
const routeApi = (req, res, ctx) => {
|
|
572
|
+
const route = API_ROUTES.find(candidate => candidate.pattern.test(ctx.pathname));
|
|
573
|
+
if (!route) {
|
|
574
|
+
sendJson(res, 404, { error: `Unknown API route: ${ctx.pathname}` });
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (!route.methods.includes(req.method ?? '')) {
|
|
578
|
+
sendJson(res, 405, { error: 'Method not allowed.' }, { Allow: route.methods.join(', ') });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const rejection = (route.sameOrigin || route.mutation ? guardSameOrigin(req, ctx.guard) : null) ??
|
|
582
|
+
(route.mutation ? guardCapability(req, ctx.guard) : null);
|
|
583
|
+
if (rejection && !rejection.ok) {
|
|
584
|
+
sendJson(res, rejection.status, rejection.body, rejection.headers);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (route.requiresJson && !isJson(req)) {
|
|
588
|
+
sendJson(res, 415, { error: 'Content-Type must be application/json.' });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
route.handle(req, res, ctx);
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* The Content Security Policy for the SPA, sent on every static response (the
|
|
595
|
+
* HTML document and its assets; a non-HTML asset ignores it, which is harmless).
|
|
596
|
+
* Everything the page loads is self-hosted from `dist-web/`, so the baseline is
|
|
597
|
+
* `'self'`. The exceptions, and why:
|
|
598
|
+
*
|
|
599
|
+
* - `style-src 'unsafe-inline'`: CodeMirror 6 injects its `<style>` elements at
|
|
600
|
+
* runtime and Mermaid's rendered SVG embeds a `<style>` block, neither of
|
|
601
|
+
* which can be hashed ahead of time. Scripts have no such exception: the
|
|
602
|
+
* pre-paint theme guard is the self-hosted `/theme-guard.js`, not an inline
|
|
603
|
+
* block, so `script-src` stays `'self'` with no hash and no `unsafe-eval`.
|
|
604
|
+
* - `img-src data:`: the markdown sanitizer admits bounded `data:image/*`
|
|
605
|
+
* images; remote image sources are already stripped there.
|
|
606
|
+
* - `connect-src 'self'`: the `/api/*` fetches and the `/api/events`
|
|
607
|
+
* EventSource are same-origin.
|
|
608
|
+
* - `object-src`, `base-uri`, `form-action`: `'none'` — the page embeds no
|
|
609
|
+
* plugins, sets no base URL, and submits no forms (the sanitizer removes
|
|
610
|
+
* authored `<form>`s; this makes a surviving one unable to post anywhere).
|
|
611
|
+
* - `frame-ancestors 'none'`: the viewer is never embedded. This directive only
|
|
612
|
+
* works in the header form, which is why the policy is a header and not a
|
|
613
|
+
* `<meta http-equiv>`.
|
|
614
|
+
*/
|
|
615
|
+
exports.CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
|
|
616
|
+
"img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; " +
|
|
617
|
+
"base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
618
|
+
/** Streams a static file with an appropriate content type and the CSP. */
|
|
345
619
|
const sendFile = (res, filePath) => {
|
|
346
|
-
res.writeHead(200, {
|
|
620
|
+
res.writeHead(200, {
|
|
621
|
+
'Content-Type': mimeFor(filePath),
|
|
622
|
+
'Content-Security-Policy': exports.CONTENT_SECURITY_POLICY,
|
|
623
|
+
});
|
|
347
624
|
const stream = fs.createReadStream(filePath);
|
|
348
625
|
stream.on('error', () => {
|
|
349
626
|
if (!res.headersSent)
|
|
@@ -356,7 +633,8 @@ const ASSETS_MISSING_MESSAGE = 'Web assets not found. Build the web app first (e
|
|
|
356
633
|
/** Serves the SPA: a real file under the assets root, else the index fallback. */
|
|
357
634
|
const handleStatic = (res, assetsDir, pathname) => {
|
|
358
635
|
const indexFile = path.join(assetsDir, 'index.html');
|
|
359
|
-
// Resolve the request path under assetsDir and guard against traversal.
|
|
636
|
+
// Resolve the request path under assetsDir and guard against traversal. The
|
|
637
|
+
// guard already proved `pathname` decodes; a throw here is a 400 upstream.
|
|
360
638
|
const relative = decodeURIComponent(pathname).replace(/^\/+/, '');
|
|
361
639
|
const resolved = path.resolve(assetsDir, relative);
|
|
362
640
|
const assetsRoot = path.resolve(assetsDir);
|
|
@@ -417,60 +695,149 @@ const openBrowser = (url) => {
|
|
|
417
695
|
/* non-fatal */
|
|
418
696
|
}
|
|
419
697
|
};
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// Server
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
702
|
+
const IPV6_LOOPBACK = '::1';
|
|
703
|
+
/** `host:port` as a browser writes it in `Host`, bracketing IPv6 literals. */
|
|
704
|
+
const authorityOf = (address, port) => `${address.includes(':') ? `[${address}]` : address}:${port}`;
|
|
705
|
+
const WILDCARD_ADDRESSES = new Set(['0.0.0.0', '::']);
|
|
706
|
+
const isLoopbackAddress = (address) => address === IPV6_LOOPBACK || address.startsWith('127.') || address.startsWith('::ffff:127.');
|
|
707
|
+
exports.isLoopbackAddress = isLoopbackAddress;
|
|
708
|
+
/**
|
|
709
|
+
* IP addresses a browser may name in `Host` for a server bound to `bound`. A
|
|
710
|
+
* wildcard bind accepts the machine's interface addresses as read at startup
|
|
711
|
+
* (IPv4 only for `0.0.0.0`). Link-local IPv6 is skipped: a browser cannot put a
|
|
712
|
+
* zone id in `Host`. Only IP literals are accepted because they cannot be
|
|
713
|
+
* DNS-rebound; `localhost` is added by the caller.
|
|
714
|
+
*/
|
|
715
|
+
const hostAddressesFor = (bound, interfaces = os.networkInterfaces()) => {
|
|
716
|
+
if (!WILDCARD_ADDRESSES.has(bound))
|
|
717
|
+
return [bound];
|
|
718
|
+
const addresses = new Set();
|
|
719
|
+
for (const infos of Object.values(interfaces)) {
|
|
720
|
+
for (const info of infos ?? []) {
|
|
721
|
+
if (info.family === 'IPv6' && (bound === '0.0.0.0' || info.address.startsWith('fe80:'))) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
addresses.add(info.address);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return [...addresses];
|
|
728
|
+
};
|
|
729
|
+
exports.hostAddressesFor = hostAddressesFor;
|
|
420
730
|
/**
|
|
421
731
|
* Creates and starts the HTTP server, resolving with the bound URL once it is
|
|
422
732
|
* listening. Rejects only on a genuine listen/bind error.
|
|
423
733
|
*/
|
|
424
|
-
const startServer = (opts) => {
|
|
734
|
+
const startServer = async (opts) => {
|
|
425
735
|
const extraHandlers = opts.apiHandlers ?? [];
|
|
426
736
|
const events = new events_1.EventsHub(opts.root, opts.debounceMs);
|
|
427
|
-
|
|
428
|
-
|
|
737
|
+
// One capability per start; it lives in this closure and leaves the process
|
|
738
|
+
// only through `GET /api/session` (and the test-only handle field).
|
|
739
|
+
const capability = crypto.randomBytes(32).toString('base64url');
|
|
740
|
+
// `allowedHosts` is filled in once `listen` resolves (port 0 is ephemeral);
|
|
741
|
+
// empty until then, which refuses everything — no request can arrive before
|
|
742
|
+
// that anyway.
|
|
743
|
+
const guardContext = { allowedHosts: new Set(), capability };
|
|
744
|
+
// One launch registry per instance, so self-review admission is bounded for
|
|
745
|
+
// this server's lifetime; a test-supplied registry still wins.
|
|
746
|
+
const selfReviewDeps = { registry: (0, self_review_1.createLaunchRegistry)(), ...opts.selfReviewDeps };
|
|
747
|
+
const handleRequest = (req, res) => {
|
|
748
|
+
const guard = (0, exports.guardRequest)(req, guardContext);
|
|
749
|
+
if (!guard.ok) {
|
|
750
|
+
sendJson(res, guard.status, guard.body, guard.headers);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const { pathname } = guard;
|
|
429
754
|
if (pathname.startsWith('/api/')) {
|
|
430
755
|
for (const handler of extraHandlers) {
|
|
431
756
|
if (handler(req, res, { root: opts.root, pathname }))
|
|
432
757
|
return;
|
|
433
758
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (req.method === 'PUT' && /^\/api\/config\/[^/]+\/[^/]+\/?$/.test(pathname)) {
|
|
446
|
-
if (handleConfigWrite(req, res, opts.root, pathname))
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
// Built-in SSE change stream.
|
|
450
|
-
if (events.apiHandler(req, res, { pathname }))
|
|
451
|
-
return;
|
|
452
|
-
if (handleApi(res, opts.root, pathname))
|
|
453
|
-
return;
|
|
454
|
-
sendJson(res, 404, { error: `Unknown API route: ${pathname}` });
|
|
759
|
+
routeApi(req, res, {
|
|
760
|
+
root: opts.root,
|
|
761
|
+
pathname,
|
|
762
|
+
events,
|
|
763
|
+
guard: guardContext,
|
|
764
|
+
selfReviewDeps,
|
|
765
|
+
});
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (!READ_METHODS.includes(req.method)) {
|
|
769
|
+
sendJson(res, 405, { error: 'Method not allowed.' }, { Allow: READ_METHODS.join(', ') });
|
|
455
770
|
return;
|
|
456
771
|
}
|
|
457
772
|
handleStatic(res, opts.assetsDir, pathname);
|
|
773
|
+
};
|
|
774
|
+
// The one exception boundary: a throw anywhere in the synchronous path is a
|
|
775
|
+
// bounded error response, never a dead process. Async handlers own their own
|
|
776
|
+
// `.catch`, each of which ends in `sendError`.
|
|
777
|
+
const server = http.createServer((req, res) => {
|
|
778
|
+
try {
|
|
779
|
+
handleRequest(req, res);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
sendError(res, err instanceof URIError ? 400 : 500, {
|
|
783
|
+
error: err instanceof URIError ? 'Bad request.' : 'Internal server error.',
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
// Requests the HTTP parser itself rejects get a minimal 400 and a closed socket.
|
|
788
|
+
server.on('clientError', (err, socket) => {
|
|
789
|
+
if (err.code === 'ECONNRESET' || !socket.writable) {
|
|
790
|
+
socket.destroy();
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
458
794
|
});
|
|
459
795
|
// Tear the watcher and open client streams down with the server.
|
|
460
796
|
server.on('close', () => events.close());
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
server.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const url = `http://localhost:${boundPort}`;
|
|
468
|
-
events.start();
|
|
469
|
-
if (opts.open)
|
|
470
|
-
openBrowser(url);
|
|
471
|
-
resolve({ url, server, port: boundPort, events });
|
|
797
|
+
const listenOn = (host) => new Promise((resolve, reject) => {
|
|
798
|
+
const onError = (err) => reject(err);
|
|
799
|
+
server.once('error', onError);
|
|
800
|
+
server.listen(opts.port, host, () => {
|
|
801
|
+
server.removeListener('error', onError);
|
|
802
|
+
resolve();
|
|
472
803
|
});
|
|
473
804
|
});
|
|
805
|
+
const preferred = opts.host ?? DEFAULT_HOST;
|
|
806
|
+
try {
|
|
807
|
+
await listenOn(preferred);
|
|
808
|
+
}
|
|
809
|
+
catch (err) {
|
|
810
|
+
const code = err.code;
|
|
811
|
+
if (preferred !== DEFAULT_HOST || code !== 'EADDRNOTAVAIL')
|
|
812
|
+
throw err;
|
|
813
|
+
await listenOn(IPV6_LOOPBACK);
|
|
814
|
+
}
|
|
815
|
+
const address = server.address();
|
|
816
|
+
if (!address || typeof address === 'string') {
|
|
817
|
+
server.close();
|
|
818
|
+
throw new Error('Server did not bind a TCP address.');
|
|
819
|
+
}
|
|
820
|
+
const boundPort = address.port;
|
|
821
|
+
const authorities = (0, exports.hostAddressesFor)(address.address).map(a => authorityOf(a, boundPort));
|
|
822
|
+
// `127.0.0.1` and `localhost` are always allowed; IPv6 and non-loopback
|
|
823
|
+
// authorities only where the server listens. Lower-cased to match the guard.
|
|
824
|
+
guardContext.allowedHosts = new Set([...authorities, `${DEFAULT_HOST}:${boundPort}`, `localhost:${boundPort}`].map(a => a.toLowerCase()));
|
|
825
|
+
// A wildcard bind is opened locally through IPv4 loopback.
|
|
826
|
+
const url = WILDCARD_ADDRESSES.has(address.address)
|
|
827
|
+
? `http://${authorityOf(DEFAULT_HOST, boundPort)}`
|
|
828
|
+
: `http://${authorityOf(address.address, boundPort)}`;
|
|
829
|
+
events.start();
|
|
830
|
+
if (opts.open)
|
|
831
|
+
openBrowser(url);
|
|
832
|
+
return {
|
|
833
|
+
url,
|
|
834
|
+
server,
|
|
835
|
+
port: boundPort,
|
|
836
|
+
exposed: !(0, exports.isLoopbackAddress)(address.address),
|
|
837
|
+
reachableUrls: authorities.map(a => `http://${a}`),
|
|
838
|
+
events,
|
|
839
|
+
capability,
|
|
840
|
+
};
|
|
474
841
|
};
|
|
475
842
|
exports.startServer = startServer;
|
|
476
843
|
//# sourceMappingURL=server.js.map
|