strikethroo 4.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (479) hide show
  1. package/README.md +16 -2
  2. package/dist/__tests__/fixtures/blueprint-trailing-summary.d.ts +14 -0
  3. package/dist/__tests__/fixtures/blueprint-trailing-summary.d.ts.map +1 -0
  4. package/dist/__tests__/fixtures/blueprint-trailing-summary.js +54 -0
  5. package/dist/__tests__/fixtures/blueprint-trailing-summary.js.map +1 -0
  6. package/dist/cli.js +23 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/conflict-detector.d.ts +28 -12
  9. package/dist/conflict-detector.d.ts.map +1 -1
  10. package/dist/conflict-detector.js +45 -53
  11. package/dist/conflict-detector.js.map +1 -1
  12. package/dist/export-profile.d.ts +6 -0
  13. package/dist/export-profile.d.ts.map +1 -1
  14. package/dist/export-profile.js +46 -4
  15. package/dist/export-profile.js.map +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +97 -86
  18. package/dist/index.js.map +1 -1
  19. package/dist/metadata.d.ts +12 -1
  20. package/dist/metadata.d.ts.map +1 -1
  21. package/dist/metadata.js +76 -11
  22. package/dist/metadata.js.map +1 -1
  23. package/dist/profiles.d.ts.map +1 -1
  24. package/dist/profiles.js +52 -7
  25. package/dist/profiles.js.map +1 -1
  26. package/dist/prompts.d.ts +6 -0
  27. package/dist/prompts.d.ts.map +1 -1
  28. package/dist/prompts.js +47 -3
  29. package/dist/prompts.js.map +1 -1
  30. package/dist/serve/config-write.d.ts +6 -3
  31. package/dist/serve/config-write.d.ts.map +1 -1
  32. package/dist/serve/config-write.js +20 -21
  33. package/dist/serve/config-write.js.map +1 -1
  34. package/dist/serve/derivation.d.ts +17 -19
  35. package/dist/serve/derivation.d.ts.map +1 -1
  36. package/dist/serve/derivation.js +43 -82
  37. package/dist/serve/derivation.js.map +1 -1
  38. package/dist/serve/events.d.ts +37 -8
  39. package/dist/serve/events.d.ts.map +1 -1
  40. package/dist/serve/events.js +114 -28
  41. package/dist/serve/events.js.map +1 -1
  42. package/dist/serve/self-review.d.ts +37 -7
  43. package/dist/serve/self-review.d.ts.map +1 -1
  44. package/dist/serve/self-review.js +93 -33
  45. package/dist/serve/self-review.js.map +1 -1
  46. package/dist/serve/server.d.ts +107 -0
  47. package/dist/serve/server.d.ts.map +1 -1
  48. package/dist/serve/server.js +502 -135
  49. package/dist/serve/server.js.map +1 -1
  50. package/dist/serve/workspace-model.d.ts +11 -17
  51. package/dist/serve/workspace-model.d.ts.map +1 -1
  52. package/dist/serve/workspace-model.js +84 -83
  53. package/dist/serve/workspace-model.js.map +1 -1
  54. package/dist/skill-scripts/shared/blueprint-parse.d.ts +16 -0
  55. package/dist/skill-scripts/shared/blueprint-parse.d.ts.map +1 -0
  56. package/dist/skill-scripts/shared/blueprint-parse.js +66 -0
  57. package/dist/skill-scripts/shared/blueprint-parse.js.map +1 -0
  58. package/dist/skill-scripts/shared/root.d.ts +6 -0
  59. package/dist/skill-scripts/shared/root.d.ts.map +1 -1
  60. package/dist/skill-scripts/shared/root.js +10 -4
  61. package/dist/skill-scripts/shared/root.js.map +1 -1
  62. package/dist/skill-scripts/shared/safe-fs.d.ts +89 -0
  63. package/dist/skill-scripts/shared/safe-fs.d.ts.map +1 -0
  64. package/dist/skill-scripts/shared/safe-fs.js +324 -0
  65. package/dist/skill-scripts/shared/safe-fs.js.map +1 -0
  66. package/dist/types.d.ts +8 -0
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/types.js +13 -1
  69. package/dist/types.js.map +1 -1
  70. package/dist/update.d.ts.map +1 -1
  71. package/dist/update.js +15 -1
  72. package/dist/update.js.map +1 -1
  73. package/dist/validation/graph-checks.d.ts +6 -14
  74. package/dist/validation/graph-checks.d.ts.map +1 -1
  75. package/dist/validation/graph-checks.js +8 -15
  76. package/dist/validation/graph-checks.js.map +1 -1
  77. package/dist/validation/metadata-gate.d.ts +2 -6
  78. package/dist/validation/metadata-gate.d.ts.map +1 -1
  79. package/dist/validation/metadata-gate.js +17 -6
  80. package/dist/validation/metadata-gate.js.map +1 -1
  81. package/dist/validation/strict-pass.d.ts.map +1 -1
  82. package/dist/validation/strict-pass.js +50 -6
  83. package/dist/validation/strict-pass.js.map +1 -1
  84. package/dist-web/assets/abnfDiagram-VCTEODGH-CY8QEBrj.js +1 -0
  85. package/dist-web/assets/apl-gfMMiTNf.js +1 -0
  86. package/dist-web/assets/arc-Pry-7G1_.js +1 -0
  87. package/dist-web/assets/architecture-7GRP2DOG-drIky86B.js +1 -0
  88. package/dist-web/assets/architectureDiagram-5GKGNRK7-YZUSf-Fx.js +36 -0
  89. package/dist-web/assets/array-BifhSqXX.js +1 -0
  90. package/dist-web/assets/asciiarmor-C_eisQCp.js +1 -0
  91. package/dist-web/assets/asn1-DCl_8MCW.js +1 -0
  92. package/dist-web/assets/asterisk-BEn0Cwpx.js +1 -0
  93. package/dist-web/assets/blockDiagram-I7D4REHJ-ByyeaYP7.js +129 -0
  94. package/dist-web/assets/brainfuck-D5EjA2JK.js +1 -0
  95. package/dist-web/assets/c4Diagram-7LVT6UL2-YSA0IA7A.js +38 -0
  96. package/dist-web/assets/channel-DEeX8XvW.js +1 -0
  97. package/dist-web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  98. package/dist-web/assets/chunk-4HAMMTFA-8VzDhfwy.js +62 -0
  99. package/dist-web/assets/{chunk-FMBD7UC4-zW3dYoIJ.js → chunk-5VM5RSS4-ZNzvKenW.js} +2 -2
  100. package/dist-web/assets/chunk-75Z2AOVW-D3fZto5-.js +2 -0
  101. package/dist-web/assets/chunk-DU6HZSFF-vtUmBAxv.js +125 -0
  102. package/dist-web/assets/chunk-F27PBJKO-BMcZVmbf.js +1 -0
  103. package/dist-web/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
  104. package/dist-web/assets/chunk-GMAD6QVW-C5gwfNkR.js +72 -0
  105. package/dist-web/assets/chunk-GVQU2GXP-CqFZMp6I.js +1 -0
  106. package/dist-web/assets/chunk-IMKFNOWR-CJDfPbiX.js +231 -0
  107. package/dist-web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  108. package/dist-web/assets/chunk-L3NEJ4N5-CHeJ2XSW.js +1 -0
  109. package/dist-web/assets/chunk-OSK3NFVY-CXOClbf5.js +10 -0
  110. package/dist-web/assets/chunk-P2QGCYS3-C9Wc2Z2I.js +1 -0
  111. package/dist-web/assets/chunk-POPQ4Y6H-CfiiGKfO.js +1 -0
  112. package/dist-web/assets/chunk-PWAF6VOD-CtZ4gx65.js +1 -0
  113. package/dist-web/assets/chunk-SHT3W25Y-C50-QCrR.js +168 -0
  114. package/dist-web/assets/chunk-SVP7TREG-EIHQbPfX.js +88 -0
  115. package/dist-web/assets/chunk-TICWLB2K-DOB5NZ4R.js +206 -0
  116. package/dist-web/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
  117. package/dist-web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  118. package/dist-web/assets/classDiagram-ZZMXUADV-H5H_txsg.js +1 -0
  119. package/dist-web/assets/classDiagram-v2-VYDZK3BY-H5H_txsg.js +1 -0
  120. package/dist-web/assets/clike-D_6L3tnO.js +1 -0
  121. package/dist-web/assets/clojure-BflJGmDX.js +1 -0
  122. package/dist-web/assets/cmake-84nbDH0I.js +1 -0
  123. package/dist-web/assets/cobol-DHLQUbN7.js +1 -0
  124. package/dist-web/assets/coffeescript-De_zI4J0.js +1 -0
  125. package/dist-web/assets/commonlisp-DgAKBe0r.js +1 -0
  126. package/dist-web/assets/cose-bilkent-JH36ORCC-BEpcWcmK.js +1 -0
  127. package/dist-web/assets/crystal-Bsdphhtm.js +1 -0
  128. package/dist-web/assets/css-CnaYoQib.js +1 -0
  129. package/dist-web/assets/cynefin-OW5HDTMX-mR6pfDSH.js +1 -0
  130. package/dist-web/assets/cynefinDiagram-5FMLGOSQ-6-2oQdEg.js +62 -0
  131. package/dist-web/assets/cypher-p9eYesGn.js +1 -0
  132. package/dist-web/assets/cytoscape.esm-Yq6u8L66.js +321 -0
  133. package/dist-web/assets/d-DWkQyNnU.js +1 -0
  134. package/dist-web/assets/dagre-C-rnJpZF.js +1 -0
  135. package/dist-web/assets/dagre-GXQ25YYZ-Cm1zvIWj.js +4 -0
  136. package/dist-web/assets/defaultLocale-BFoDCU3G.js +1 -0
  137. package/dist-web/assets/diagram-S7CK7UJ4-D6mECnyP.js +30 -0
  138. package/dist-web/assets/diagram-UQ7AKVKN-CzmXABVS.js +41 -0
  139. package/dist-web/assets/diagram-VSXAHHWV-CpEauecU.js +3 -0
  140. package/dist-web/assets/diagram-VX7I27RA-CXPYrCgX.js +24 -0
  141. package/dist-web/assets/diagram-Z3DM3KII-RcMIMVWZ.js +24 -0
  142. package/dist-web/assets/diff-ChtP43wD.js +1 -0
  143. package/dist-web/assets/dist--eYrBgi0.js +2 -0
  144. package/dist-web/assets/dist-B-O4eZD_.js +1 -0
  145. package/dist-web/assets/dist-B27L6oW0.js +1 -0
  146. package/dist-web/assets/dist-BKXbBuAc.js +1 -0
  147. package/dist-web/assets/dist-BPcC0hnc.js +9 -0
  148. package/dist-web/assets/dist-BgwxV8e_.js +1 -0
  149. package/dist-web/assets/dist-BlG40nRw.js +1 -0
  150. package/dist-web/assets/dist-Bqc6e86z.js +1 -0
  151. package/dist-web/assets/dist-BsvASXHG.js +1 -0
  152. package/dist-web/assets/dist-C15GN054.js +1 -0
  153. package/dist-web/assets/dist-C4QRmcTO.js +1 -0
  154. package/dist-web/assets/dist-C4lZy0XG.js +6 -0
  155. package/dist-web/assets/dist-CFLbFzpB.js +23 -0
  156. package/dist-web/assets/dist-CP6AUVtv.js +1 -0
  157. package/dist-web/assets/dist-CpYnHHz6.js +1 -0
  158. package/dist-web/assets/dist-DHnGmK79.js +1 -0
  159. package/dist-web/assets/dist-DYmIHv0n.js +13 -0
  160. package/dist-web/assets/dist-DauRO0RO.js +1 -0
  161. package/dist-web/assets/dist-Day7i26p.js +1 -0
  162. package/dist-web/assets/dist-Do_t70vA.js +1 -0
  163. package/dist-web/assets/dist-G8nWWSie.js +11 -0
  164. package/dist-web/assets/dist-QfYLz_N1.js +1 -0
  165. package/dist-web/assets/dist-TZhb2tj2.js +1 -0
  166. package/dist-web/assets/dist-Wr2Wo_KE.js +1 -0
  167. package/dist-web/assets/dist-kYRo7Db1.js +1 -0
  168. package/dist-web/assets/dist-kfqV1Lu-.js +2 -0
  169. package/dist-web/assets/dist-v5Q1xZ2K.js +1 -0
  170. package/dist-web/assets/dockerfile-DJkHQgkl.js +1 -0
  171. package/dist-web/assets/dtd-DTF72YNO.js +1 -0
  172. package/dist-web/assets/dylan-BCDoL_-p.js +1 -0
  173. package/dist-web/assets/ebnf-D4c0_ac3.js +1 -0
  174. package/dist-web/assets/ebnfDiagram-PWID7BFC-MZpZ_VnU.js +1 -0
  175. package/dist-web/assets/ecl-BVIeWmwY.js +1 -0
  176. package/dist-web/assets/eiffel-BEjRio4Q.js +1 -0
  177. package/dist-web/assets/elm-Cshzl8qu.js +1 -0
  178. package/dist-web/assets/erDiagram-RLTQ6QDP-BcZ7Ctxy.js +99 -0
  179. package/dist-web/assets/erlang-BV0c4Hdn.js +1 -0
  180. package/dist-web/assets/esm-CQW0DO89.js +2 -0
  181. package/dist-web/assets/eventmodeling-NTZA5JFV-BwLlQZcI.js +1 -0
  182. package/dist-web/assets/factor-LKH5gN8L.js +1 -0
  183. package/dist-web/assets/fcl-ClOhbFR7.js +1 -0
  184. package/dist-web/assets/flowDiagram-HODETNUW-DGqUYaRC.js +1 -0
  185. package/dist-web/assets/forth-D6RzAl64.js +1 -0
  186. package/dist-web/assets/fortran-Bt6PBEDR.js +1 -0
  187. package/dist-web/assets/ganttDiagram-EL5Y4UJY-CcJ72sCU.js +292 -0
  188. package/dist-web/assets/gas-BHEdbvp9.js +1 -0
  189. package/dist-web/assets/gherkin-oBAE_ms0.js +1 -0
  190. package/dist-web/assets/gitGraph-4MIJSDKK-BheSjO6S.js +1 -0
  191. package/dist-web/assets/gitGraphDiagram-WWUBYQGX-Qv13SyYh.js +106 -0
  192. package/dist-web/assets/graphlib-DS17s2tU.js +1 -0
  193. package/dist-web/assets/groovy-CUpZ7dEl.js +1 -0
  194. package/dist-web/assets/haskell-BRsoo5mP.js +1 -0
  195. package/dist-web/assets/haxe-DjVOVvNP.js +1 -0
  196. package/dist-web/assets/http-CLVgA2GD.js +1 -0
  197. package/dist-web/assets/idl-BUZw3wgd.js +1 -0
  198. package/dist-web/assets/index-D0npIBvI.css +2 -0
  199. package/dist-web/assets/index-DsQxKERJ.js +103 -0
  200. package/dist-web/assets/info-A6RAGUB7-BemnwZ_B.js +1 -0
  201. package/dist-web/assets/infoDiagram-27XIBGKW-otpjj8PV.js +2 -0
  202. package/dist-web/assets/init-C-OQMol4.js +1 -0
  203. package/dist-web/assets/ishikawaDiagram-5VMMS53U-BuZBZkYN.js +70 -0
  204. package/dist-web/assets/javascript-Bu8mWwtz.js +1 -0
  205. package/dist-web/assets/journeyDiagram-3NMN7TZE-D-uflaQS.js +139 -0
  206. package/dist-web/assets/julia-CQfgDRfP.js +1 -0
  207. package/dist-web/assets/kanban-definition-UXKFOSKX-BI643rxH.js +89 -0
  208. package/dist-web/assets/katex-ZlcWpGUi.js +257 -0
  209. package/dist-web/assets/line-fay7lQM4.js +1 -0
  210. package/dist-web/assets/linear-SzVmGSw4.js +1 -0
  211. package/dist-web/assets/livescript-BXxG0Yva.js +1 -0
  212. package/dist-web/assets/lua-DPCEkfD_.js +1 -0
  213. package/dist-web/assets/mathematica-u8YMmbU0.js +1 -0
  214. package/dist-web/assets/mbox-BEMVcqjs.js +1 -0
  215. package/dist-web/assets/mermaid-parser.core-CI9Bt8lf.js +7 -0
  216. package/dist-web/assets/mermaid.core-CpvTzp9Q.js +44 -0
  217. package/dist-web/assets/mindmap-definition-YA3MSWOX-Dw-DFKfU.js +96 -0
  218. package/dist-web/assets/{mirc-CjQqDB4T.js → mirc-C9zpzAZU.js} +1 -1
  219. package/dist-web/assets/mllike-B45xgp2S.js +1 -0
  220. package/dist-web/assets/modelica-CqHI_q-D.js +1 -0
  221. package/dist-web/assets/mscgen-qgSLujhx.js +1 -0
  222. package/dist-web/assets/mumps-BSzj6Xyz.js +1 -0
  223. package/dist-web/assets/{nginx-DdIZxoE0.js → nginx-8H7HVH4w.js} +1 -1
  224. package/dist-web/assets/nsis-CfSuhXs5.js +1 -0
  225. package/dist-web/assets/ntriples-B9h1VB_g.js +1 -0
  226. package/dist-web/assets/octave-D2Q8cUp1.js +1 -0
  227. package/dist-web/assets/ordinal-BDEzSJ7C.js +1 -0
  228. package/dist-web/assets/oz-BwTct_5T.js +1 -0
  229. package/dist-web/assets/packet-AYTQ26CC-CsBrScsn.js +1 -0
  230. package/dist-web/assets/pascal-Dp_KdFgX.js +1 -0
  231. package/dist-web/assets/path-fybaL0A-.js +1 -0
  232. package/dist-web/assets/pegDiagram-XKGWAZYB-BVal7Apj.js +1 -0
  233. package/dist-web/assets/perl-BkSkiI9m.js +1 -0
  234. package/dist-web/assets/pie-WAS4IAKB-DZFMDceO.js +1 -0
  235. package/dist-web/assets/pieDiagram-E7YTZNPT-B3QEKixf.js +39 -0
  236. package/dist-web/assets/pig-AF4Hwhju.js +1 -0
  237. package/dist-web/assets/powershell-BcIto6Sf.js +1 -0
  238. package/dist-web/assets/properties-C357ku5U.js +1 -0
  239. package/dist-web/assets/protobuf-D1ij_kNL.js +1 -0
  240. package/dist-web/assets/pug-3K-JMW4g.js +1 -0
  241. package/dist-web/assets/puppet-BZyAS7I4.js +1 -0
  242. package/dist-web/assets/python-B_1Jd8HM.js +1 -0
  243. package/dist-web/assets/q-B9NhS7L_.js +1 -0
  244. package/dist-web/assets/quadrantDiagram-AXDQQJYC-CRgAf_p-.js +7 -0
  245. package/dist-web/assets/r-CxDmU1I4.js +1 -0
  246. package/dist-web/assets/radar-RG4KPBEZ-BkOoPAbJ.js +1 -0
  247. package/dist-web/assets/railroad-74A4TZTK-BgIna4sT.js +1 -0
  248. package/dist-web/assets/railroad-abnf-HS5TGJTU-CVlFeN1b.js +1 -0
  249. package/dist-web/assets/railroad-ebnf-LZEXJU2U-DCEa0WVT.js +1 -0
  250. package/dist-web/assets/railroad-peg-WCYAUIDC-BHX1T-4x.js +1 -0
  251. package/dist-web/assets/railroadDiagram-O6MQD6OU-BZwRaQM3.js +1 -0
  252. package/dist-web/assets/requirementDiagram-BXWQKSXE-BN3nmFpY.js +84 -0
  253. package/dist-web/assets/rolldown-runtime-hePW80VL.js +1 -0
  254. package/dist-web/assets/rough.esm-Dy-Kn_BL.js +1 -0
  255. package/dist-web/assets/rpm-gpDK5sbt.js +1 -0
  256. package/dist-web/assets/ruby-BjXV7-Qa.js +1 -0
  257. package/dist-web/assets/sankeyDiagram-P5KCCOFB-DBhFOKcq.js +40 -0
  258. package/dist-web/assets/{sas-B4kiWyti.js → sas-C2OyD2Y6.js} +1 -1
  259. package/dist-web/assets/scheme-D9qYN0V5.js +1 -0
  260. package/dist-web/assets/sequenceDiagram-WJ2MYXX4-CBc9y9xq.js +162 -0
  261. package/dist-web/assets/shell-DwuoZtxw.js +1 -0
  262. package/dist-web/assets/sieve-tLWDfEkZ.js +1 -0
  263. package/dist-web/assets/simple-mode-DXiLM1-G.js +1 -0
  264. package/dist-web/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  265. package/dist-web/assets/smalltalk-BLp5-jwC.js +1 -0
  266. package/dist-web/assets/solr-B78wSQJQ.js +1 -0
  267. package/dist-web/assets/sparql-Dz-mlCAC.js +1 -0
  268. package/dist-web/assets/spreadsheet-CTIncYxj.js +1 -0
  269. package/dist-web/assets/sql-DuQMFDvy.js +1 -0
  270. package/dist-web/assets/src-B6xuSHsQ.js +1 -0
  271. package/dist-web/assets/stateDiagram-D77RDMKH-BDdsXTTr.js +1 -0
  272. package/dist-web/assets/stateDiagram-v2-MP3YSRHH-C04o4SeT.js +1 -0
  273. package/dist-web/assets/stex-DNzd62Q_.js +1 -0
  274. package/dist-web/assets/stylus-BOVgKnng.js +1 -0
  275. package/dist-web/assets/swift-DKB6_1j6.js +1 -0
  276. package/dist-web/assets/swimlanes-42K2YHIH-1QXV5L5H.js +1 -0
  277. package/dist-web/assets/swimlanesDiagram-VR7AAH4N-CQ46EU90.js +8 -0
  278. package/dist-web/assets/tcl-BGfruO1u.js +1 -0
  279. package/dist-web/assets/textile-C4jVoHG1.js +1 -0
  280. package/dist-web/assets/tiddlywiki-VlUiK86J.js +1 -0
  281. package/dist-web/assets/tiki-AK6FwT8q.js +1 -0
  282. package/dist-web/assets/timeline-definition-24CTP7MA-D9CdAGEV.js +120 -0
  283. package/dist-web/assets/toml-BPTmHmyx.js +1 -0
  284. package/dist-web/assets/treeView-Q6P3EWNA-CxKfpZsz.js +1 -0
  285. package/dist-web/assets/treemap-WGGIJYW6-Bg5-PJ6G.js +1 -0
  286. package/dist-web/assets/troff-C2dAgh7P.js +1 -0
  287. package/dist-web/assets/ttcn-PPSTk1ow.js +1 -0
  288. package/dist-web/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
  289. package/dist-web/assets/turtle-Cx3rYX2g.js +1 -0
  290. package/dist-web/assets/vb-BthdM_4N.js +1 -0
  291. package/dist-web/assets/vbscript-D9f6rSsU.js +1 -0
  292. package/dist-web/assets/velocity-CqftRsjg.js +1 -0
  293. package/dist-web/assets/vennDiagram-4TSXK5OY-DIlZEQm_.js +34 -0
  294. package/dist-web/assets/verilog-CjI9Y6RI.js +1 -0
  295. package/dist-web/assets/vhdl-kS471soi.js +1 -0
  296. package/dist-web/assets/wardley-WFR3VGLG-DCKFxH5V.js +1 -0
  297. package/dist-web/assets/wardleyDiagram-VM6X3IG4-D8O8WtAP.js +78 -0
  298. package/dist-web/assets/webidl-D5jiJvOU.js +1 -0
  299. package/dist-web/assets/xquery-BKJx4mf9.js +1 -0
  300. package/dist-web/assets/xychartDiagram-S5SC5T6Z-Dz9bYgtE.js +7 -0
  301. package/dist-web/assets/yacas-CXXgtw0p.js +1 -0
  302. package/dist-web/assets/z80-Co3PbHdE.js +1 -0
  303. package/dist-web/index.html +6 -18
  304. package/dist-web/theme-guard.js +21 -0
  305. package/package.json +34 -26
  306. package/templates/strikethroo/config/config.yaml +2 -2
  307. package/templates/strikethroo/config/templates/TASK_TEMPLATE.md +1 -1
  308. package/dist-web/assets/apl-B4CMkyY2.js +0 -1
  309. package/dist-web/assets/arc-DA6tr0lU.js +0 -1
  310. package/dist-web/assets/architectureDiagram-3BPJPVTR-BEH2YihV.js +0 -36
  311. package/dist-web/assets/asciiarmor-Df11BRmG.js +0 -1
  312. package/dist-web/assets/asn1-EdZsLKOL.js +0 -1
  313. package/dist-web/assets/asterisk-B-8jnY81.js +0 -1
  314. package/dist-web/assets/blockDiagram-GPEHLZMM-jgUyFGxx.js +0 -132
  315. package/dist-web/assets/brainfuck-C4LP7Hcl.js +0 -1
  316. package/dist-web/assets/c4Diagram-AAUBKEIU-Do-n7I5O.js +0 -10
  317. package/dist-web/assets/channel-DJ33z8Wx.js +0 -1
  318. package/dist-web/assets/chunk-2J33WTMH-CgMvCn22.js +0 -1
  319. package/dist-web/assets/chunk-4BX2VUAB-FO8RQkkL.js +0 -1
  320. package/dist-web/assets/chunk-55IACEB6-DL_6pZjw.js +0 -1
  321. package/dist-web/assets/chunk-727SXJPM-DW7J3hiD.js +0 -206
  322. package/dist-web/assets/chunk-AQP2D5EJ-TPWnvFSC.js +0 -231
  323. package/dist-web/assets/chunk-ND2GUHAM-D0Nmkz3P.js +0 -1
  324. package/dist-web/assets/chunk-QZHKN3VN-BxgxRNWB.js +0 -1
  325. package/dist-web/assets/classDiagram-4FO5ZUOK--tqQjy0s.js +0 -1
  326. package/dist-web/assets/classDiagram-v2-Q7XG4LA2--tqQjy0s.js +0 -1
  327. package/dist-web/assets/clike-B9uivgTg.js +0 -1
  328. package/dist-web/assets/clojure-BMjYHr_A.js +0 -1
  329. package/dist-web/assets/cmake-BQqOBYOt.js +0 -1
  330. package/dist-web/assets/cobol-CWcv1MsR.js +0 -1
  331. package/dist-web/assets/coffeescript-S37ZYGWr.js +0 -1
  332. package/dist-web/assets/commonlisp-DBKNyK5s.js +0 -1
  333. package/dist-web/assets/cose-bilkent-S5V4N54A-ysHXC5GT.js +0 -1
  334. package/dist-web/assets/crystal-SjHAIU92.js +0 -1
  335. package/dist-web/assets/css-BnMrqG3P.js +0 -1
  336. package/dist-web/assets/cypher-C_CwsFkJ.js +0 -1
  337. package/dist-web/assets/cytoscape.esm-BHYC38rz.js +0 -331
  338. package/dist-web/assets/d-pRatUO7H.js +0 -1
  339. package/dist-web/assets/dagre-BM42HDAG-G_XIk8Dj.js +0 -4
  340. package/dist-web/assets/defaultLocale-CrowFXzY.js +0 -1
  341. package/dist-web/assets/diagram-2AECGRRQ-7HG2PLhr.js +0 -43
  342. package/dist-web/assets/diagram-5GNKFQAL-BdS1bF_R.js +0 -10
  343. package/dist-web/assets/diagram-KO2AKTUF-AFaiXGZQ.js +0 -3
  344. package/dist-web/assets/diagram-LMA3HP47-CTWrauj7.js +0 -24
  345. package/dist-web/assets/diagram-OG6HWLK6-BmBckGLa.js +0 -24
  346. package/dist-web/assets/diff-DbItnlRl.js +0 -1
  347. package/dist-web/assets/dockerfile-BKs6k2Af.js +0 -1
  348. package/dist-web/assets/dtd-DF_7sFjM.js +0 -1
  349. package/dist-web/assets/dylan-DwRh75JA.js +0 -1
  350. package/dist-web/assets/ebnf-CDyGwa7X.js +0 -1
  351. package/dist-web/assets/ecl-Cabwm37j.js +0 -1
  352. package/dist-web/assets/eiffel-CnydiIhH.js +0 -1
  353. package/dist-web/assets/elm-vLlmbW-K.js +0 -1
  354. package/dist-web/assets/erDiagram-TEJ5UH35-C0B2cXE-.js +0 -85
  355. package/dist-web/assets/erlang-BNw1qcRV.js +0 -1
  356. package/dist-web/assets/factor-kuTfRLto.js +0 -1
  357. package/dist-web/assets/fcl-Kvtd6kyn.js +0 -1
  358. package/dist-web/assets/flowDiagram-I6XJVG4X-CZ70tn_Q.js +0 -162
  359. package/dist-web/assets/forth-Ffai-XNe.js +0 -1
  360. package/dist-web/assets/fortran-DYz_wnZ1.js +0 -1
  361. package/dist-web/assets/ganttDiagram-6RSMTGT7-h7O-Xh-D.js +0 -292
  362. package/dist-web/assets/gas-Bneqetm1.js +0 -1
  363. package/dist-web/assets/gherkin-heZmZLOM.js +0 -1
  364. package/dist-web/assets/gitGraphDiagram-PVQCEYII-BDZixURw.js +0 -106
  365. package/dist-web/assets/graph-Dqkl27Ch.js +0 -1
  366. package/dist-web/assets/groovy-D9Dt4D0W.js +0 -1
  367. package/dist-web/assets/haskell-BWDZoCOh.js +0 -1
  368. package/dist-web/assets/haxe-H-WmDvRZ.js +0 -1
  369. package/dist-web/assets/http-DBlCnlav.js +0 -1
  370. package/dist-web/assets/idl-BEugSyMb.js +0 -1
  371. package/dist-web/assets/index-B-KlYMEI.js +0 -1
  372. package/dist-web/assets/index-B0a4C4Lb.js +0 -1
  373. package/dist-web/assets/index-B90tvIxG.js +0 -3
  374. package/dist-web/assets/index-BLXIwDvb.js +0 -1
  375. package/dist-web/assets/index-BjZYUODU.js +0 -1
  376. package/dist-web/assets/index-BmsczLPZ.js +0 -1
  377. package/dist-web/assets/index-BpU7RpGz.js +0 -1
  378. package/dist-web/assets/index-BwFIiA6J.js +0 -2
  379. package/dist-web/assets/index-C53NKwQ5.js +0 -1
  380. package/dist-web/assets/index-C73riuQN.js +0 -1
  381. package/dist-web/assets/index-C9RcHSPW.js +0 -1
  382. package/dist-web/assets/index-CEBV-m_B.js +0 -1
  383. package/dist-web/assets/index-CG1O2Vjg.js +0 -1
  384. package/dist-web/assets/index-CRL66wiY.js +0 -169
  385. package/dist-web/assets/index-CddJufqj.js +0 -1
  386. package/dist-web/assets/index-CrzBaw1K.css +0 -1
  387. package/dist-web/assets/index-CuBsNWzH.js +0 -13
  388. package/dist-web/assets/index-CuCAyRTw.js +0 -7
  389. package/dist-web/assets/index-CuUh3dvS.js +0 -1
  390. package/dist-web/assets/index-CwPWc9jQ.js +0 -1
  391. package/dist-web/assets/index-DHBbud_j.js +0 -1
  392. package/dist-web/assets/index-LLaxWDbg.js +0 -1
  393. package/dist-web/assets/index-XOpAvr0E.js +0 -13
  394. package/dist-web/assets/index-aX67gnlS.js +0 -2
  395. package/dist-web/assets/index-lkaAPBmM.js +0 -6
  396. package/dist-web/assets/index-pUkuW0EP.js +0 -1
  397. package/dist-web/assets/index-sbfqG9cy.js +0 -1
  398. package/dist-web/assets/index-udFbjSlZ.js +0 -2
  399. package/dist-web/assets/index-yjEPVwyN.js +0 -1
  400. package/dist-web/assets/infoDiagram-5YYISTIA-CKXHxMh_.js +0 -2
  401. package/dist-web/assets/init-Gi6I4Gst.js +0 -1
  402. package/dist-web/assets/ishikawaDiagram-YF4QCWOH-DGancylI.js +0 -70
  403. package/dist-web/assets/javascript-qCveANmP.js +0 -1
  404. package/dist-web/assets/journeyDiagram-JHISSGLW-DifvKy-w.js +0 -139
  405. package/dist-web/assets/julia-DuME0IfC.js +0 -1
  406. package/dist-web/assets/kanban-definition-UN3LZRKU-ByC-4YyY.js +0 -89
  407. package/dist-web/assets/katex-C5jXJg4s.js +0 -257
  408. package/dist-web/assets/layout-DO9UvE1P.js +0 -1
  409. package/dist-web/assets/linear-Cm0Y2OWD.js +0 -1
  410. package/dist-web/assets/livescript-BwQOo05w.js +0 -1
  411. package/dist-web/assets/lua-VAEuO923.js +0 -1
  412. package/dist-web/assets/mathematica-DTrFuWx2.js +0 -1
  413. package/dist-web/assets/mbox-CNhZ1qSd.js +0 -1
  414. package/dist-web/assets/mermaid.core-DBlLt9sa.js +0 -307
  415. package/dist-web/assets/mindmap-definition-RKZ34NQL-tkThiQWd.js +0 -96
  416. package/dist-web/assets/mllike-CXdrOF99.js +0 -1
  417. package/dist-web/assets/modelica-Dc1JOy9r.js +0 -1
  418. package/dist-web/assets/mscgen-BA5vi2Kp.js +0 -1
  419. package/dist-web/assets/mumps-BT43cFF4.js +0 -1
  420. package/dist-web/assets/nsis-LdVXkNf5.js +0 -1
  421. package/dist-web/assets/ntriples-BfvgReVJ.js +0 -1
  422. package/dist-web/assets/octave-Ck1zUtKM.js +0 -1
  423. package/dist-web/assets/ordinal-Cboi1Yqb.js +0 -1
  424. package/dist-web/assets/oz-BzwKVEFT.js +0 -1
  425. package/dist-web/assets/pascal--L3eBynH.js +0 -1
  426. package/dist-web/assets/perl-CdXCOZ3F.js +0 -1
  427. package/dist-web/assets/pieDiagram-4H26LBE5-BXGBVjcj.js +0 -30
  428. package/dist-web/assets/pig-CevX1Tat.js +0 -1
  429. package/dist-web/assets/powershell-CFHJl5sT.js +0 -1
  430. package/dist-web/assets/properties-C78fOPTZ.js +0 -1
  431. package/dist-web/assets/protobuf-ChK-085T.js +0 -1
  432. package/dist-web/assets/pug-DukmZTjD.js +0 -1
  433. package/dist-web/assets/puppet-DMA9R1ak.js +0 -1
  434. package/dist-web/assets/python-BuPzkPfP.js +0 -1
  435. package/dist-web/assets/q-pXgVlZs6.js +0 -1
  436. package/dist-web/assets/quadrantDiagram-W4KKPZXB-DnF0iEBo.js +0 -7
  437. package/dist-web/assets/r-DUYO_cvP.js +0 -1
  438. package/dist-web/assets/requirementDiagram-4Y6WPE33-BooGjmPb.js +0 -84
  439. package/dist-web/assets/rpm-CTu-6PCP.js +0 -1
  440. package/dist-web/assets/ruby-B2Rjki9n.js +0 -1
  441. package/dist-web/assets/sankeyDiagram-5OEKKPKP-7le5uaC7.js +0 -40
  442. package/dist-web/assets/scheme-C41bIUwD.js +0 -1
  443. package/dist-web/assets/sequenceDiagram-3UESZ5HK-B9GCKMDd.js +0 -162
  444. package/dist-web/assets/shell-CjFT_Tl9.js +0 -1
  445. package/dist-web/assets/sieve-C3Gn_uJK.js +0 -1
  446. package/dist-web/assets/simple-mode-GW_nhZxv.js +0 -1
  447. package/dist-web/assets/smalltalk-CnHTOXQT.js +0 -1
  448. package/dist-web/assets/solr-DehyRSwq.js +0 -1
  449. package/dist-web/assets/sparql-DkYu6x3z.js +0 -1
  450. package/dist-web/assets/spreadsheet-BCZA_wO0.js +0 -1
  451. package/dist-web/assets/sql-D0XecflT.js +0 -1
  452. package/dist-web/assets/stateDiagram-AJRCARHV-CO2qL8R6.js +0 -1
  453. package/dist-web/assets/stateDiagram-v2-BHNVJYJU-DUkgqBTd.js +0 -1
  454. package/dist-web/assets/stex-C3f8Ysf7.js +0 -1
  455. package/dist-web/assets/stylus-B533Al4x.js +0 -1
  456. package/dist-web/assets/swift-BzpIVaGY.js +0 -1
  457. package/dist-web/assets/tcl-DVfN8rqt.js +0 -1
  458. package/dist-web/assets/textile-CnDTJFAw.js +0 -1
  459. package/dist-web/assets/tiddlywiki-DO-Gjzrf.js +0 -1
  460. package/dist-web/assets/tiki-DGYXhP31.js +0 -1
  461. package/dist-web/assets/timeline-definition-PNZ67QCA-FLzWM85F.js +0 -120
  462. package/dist-web/assets/toml-Bm5Em-hy.js +0 -1
  463. package/dist-web/assets/troff-wAsdV37c.js +0 -1
  464. package/dist-web/assets/ttcn-CfJYG6tj.js +0 -1
  465. package/dist-web/assets/ttcn-cfg-B9xdYoR4.js +0 -1
  466. package/dist-web/assets/turtle-B1tBg_DP.js +0 -1
  467. package/dist-web/assets/vb-CmGdzxic.js +0 -1
  468. package/dist-web/assets/vbscript-BuJXcnF6.js +0 -1
  469. package/dist-web/assets/velocity-D8B20fx6.js +0 -1
  470. package/dist-web/assets/vennDiagram-CIIHVFJN-Dt6p6Ib1.js +0 -34
  471. package/dist-web/assets/verilog-C6RDOZhf.js +0 -1
  472. package/dist-web/assets/vhdl-lSbBsy5d.js +0 -1
  473. package/dist-web/assets/wardley-L42UT6IY-D6k_B5It.js +0 -173
  474. package/dist-web/assets/wardleyDiagram-YWT4CUSO-jT3ItNLi.js +0 -78
  475. package/dist-web/assets/webidl-ZXfAyPTL.js +0 -1
  476. package/dist-web/assets/xquery-CQfU5ijd.js +0 -1
  477. package/dist-web/assets/xychartDiagram-2RQKCTM6-CpM6UhUO.js +0 -7
  478. package/dist-web/assets/yacas-BJ4BC0dw.js +0 -1
  479. package/dist-web/assets/z80-Hz9HOZM7.js +0 -1
@@ -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
- /** Reads and JSON-parses a request body, rejecting oversized or malformed input. */
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
- reject(new Error('Request body too large'));
252
- req.destroy();
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
- resolve({});
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 Error('Invalid JSON body'));
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. Returns `true` once it has owned the response.
426
+ * the requested plan path.
275
427
  */
276
- const handleSelfReview = (req, res, root) => {
428
+ const handleSelfReview = (req, res, ctx) => {
277
429
  readJsonBody(req)
278
430
  .then(body => {
279
431
  const clientPath = body.path;
280
- const result = (0, self_review_1.launchSelfReview)(root, typeof clientPath === 'string' ? clientPath : '');
281
- sendJson(res, result.status, result.body);
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
- sendJson(res, 400, { ok: false, error: err.message });
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. Returns `true` once it owns the
305
- * response.
460
+ * client can update without a second fetch.
306
461
  */
307
- const handleConfigWrite = (req, res, root, pathname) => {
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 true;
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
- sendJson(res, 400, { error: err.message });
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
- /** Streams a static file with an appropriate content type. */
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.getPlanSummaries)(ctx.root)),
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, { 'Content-Type': mimeFor(filePath) });
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
- const server = http.createServer((req, res) => {
428
- const pathname = new url_1.URL(req.url ?? '/', 'http://localhost').pathname;
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
- // The one sanctioned workspace mutation: archive a done plan.
435
- if (req.method === 'POST' && /^\/api\/plans\/[^/]+\/archive\/?$/.test(pathname)) {
436
- if (handleArchive(res, opts.root, pathname))
437
- return;
438
- }
439
- // The one write-ish endpoint: launch the external self-review binary.
440
- if (req.method === 'POST' && /^\/api\/self-review\/?$/.test(pathname)) {
441
- if (handleSelfReview(req, res, opts.root))
442
- return;
443
- }
444
- // The second sanctioned workspace mutation: overwrite a config file.
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
- return new Promise((resolve, reject) => {
462
- server.once('error', reject);
463
- server.listen(opts.port, () => {
464
- server.removeListener('error', reject);
465
- const address = server.address();
466
- const boundPort = typeof address === 'object' && address ? address.port : opts.port;
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