0sec-cli 0.24.1 → 0.26.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/0.js +67 -62
- package/README.md +121 -82
- package/chunks/{adapt-loop-L77BWMNE.js → adapt-loop-P4HKONB5.js} +6 -5
- package/chunks/{artifact-scraper-BV67RAP5.js → artifact-scraper-IIRJNYKT.js} +4 -3
- package/chunks/{assumption-mining-RPVR77A3.js → assumption-mining-YA6VJ2V6.js} +22 -20
- package/chunks/{backend-connections-ACIXNYZN.js → backend-connections-64PKYKL7.js} +5 -4
- package/chunks/chunk-36J5ZLFE.js +18729 -0
- package/chunks/{chunk-ZM6ANQZJ.js → chunk-3MWQNNJU.js} +16180 -34467
- package/chunks/{chunk-GV5WXDHY.js → chunk-3VQ5EZKG.js} +11 -9
- package/chunks/chunk-5XOYTAOK.js +1546 -0
- package/chunks/{chunk-UJE4WOQS.js → chunk-6GV5G6U3.js} +1 -1
- package/chunks/{chunk-X2ZMSRTP.js → chunk-6SNOKNCS.js} +333 -3
- package/chunks/{chunk-BMDG6T7D.js → chunk-72PU46MS.js} +178 -133
- package/chunks/{chunk-KCA35IL3.js → chunk-7PUJI64A.js} +11 -9
- package/chunks/chunk-A7G6S5RA.js +90 -0
- package/chunks/chunk-ABKUI3QB.js +196 -0
- package/chunks/{chunk-5ETI27Q3.js → chunk-AFJ2UMPR.js} +3 -3
- package/chunks/{chunk-XA2IBHPB.js → chunk-B7XL6RON.js} +1 -1
- package/chunks/{chunk-TVS6WVSK.js → chunk-BM6EYP2D.js} +3 -3
- package/chunks/{chunk-7UHAXEOV.js → chunk-D4M5WURF.js} +226 -4
- package/chunks/{chunk-K74DRDC5.js → chunk-EHNVP6QB.js} +1 -1
- package/chunks/{chunk-I43YYHDU.js → chunk-FBZBJYZS.js} +5 -50
- package/chunks/{chunk-GXXX7AGH.js → chunk-GYEYTNHK.js} +144 -14
- package/chunks/{chunk-IJ6UPW3H.js → chunk-HS2NQ7K6.js} +393 -58
- package/chunks/chunk-HUZ2EC2R.js +196 -0
- package/chunks/{chunk-SU4IHQQI.js → chunk-IUUNIJPM.js} +1 -1
- package/chunks/chunk-JGDA6EIJ.js +49 -0
- package/chunks/{chunk-CJRZSWII.js → chunk-K3PTYN43.js} +3 -3
- package/chunks/{chunk-2NBZZFRG.js → chunk-KCABBZHN.js} +8 -10
- package/chunks/{chunk-3LPBJMHB.js → chunk-LRD5N2TQ.js} +1 -1
- package/chunks/{chunk-SQCBHFWN.js → chunk-LWMEJYCP.js} +2 -2
- package/chunks/chunk-MNK373HF.js +57 -0
- package/chunks/{chunk-R2XBH476.js → chunk-MOEVVXML.js} +2 -2
- package/chunks/{chunk-F2P25DJY.js → chunk-MOGWEHSC.js} +1448 -1050
- package/chunks/{chunk-6XMFCVJS.js → chunk-N7YCONYR.js} +1 -1
- package/chunks/{chunk-3WUPCZGF.js → chunk-NA3FKCI2.js} +1 -1
- package/chunks/{chunk-S2GITRD5.js → chunk-NABVDAAO.js} +2 -2
- package/chunks/{chunk-SZDD77KX.js → chunk-NNMOTHKC.js} +6 -10
- package/chunks/{chunk-LMP2T4G2.js → chunk-OILVEROI.js} +22 -172
- package/chunks/{chunk-YERUDEXT.js → chunk-PAE7ZAV6.js} +2780 -1255
- package/chunks/{chunk-NBV4I237.js → chunk-PAMJEXE2.js} +1 -1
- package/chunks/{chunk-PJU24AKR.js → chunk-R7VWEY45.js} +337 -257
- package/chunks/{chunk-QNRYOJEB.js → chunk-RZZ6VPRY.js} +1 -1
- package/chunks/{chunk-WUPN3D37.js → chunk-S4DFH2FJ.js} +26 -1
- package/chunks/{chunk-5WAHHZBF.js → chunk-SIR7XW2S.js} +61910 -30777
- package/chunks/{chunk-BD7WULFS.js → chunk-SSPP64FF.js} +3 -2
- package/chunks/{chunk-BOBIAHKA.js → chunk-STPHTPSD.js} +3 -333
- package/chunks/{chunk-Q7JKIKKF.js → chunk-UGCTPKJG.js} +89 -17
- package/chunks/{chunk-5PFRE522.js → chunk-ULRLUXSS.js} +1 -1
- package/chunks/{chunk-GZA7RGK7.js → chunk-UQS7XCBV.js} +2085 -1093
- package/chunks/{chunk-X2IPO5JO.js → chunk-V2KJQVYE.js} +2676 -2959
- package/chunks/{chunk-CGBDEST2.js → chunk-VJBEB2K6.js} +1 -1
- package/chunks/{chunk-ILRMLA6J.js → chunk-VWE36MUN.js} +1 -1
- package/chunks/{chunk-RURJYCWY.js → chunk-WNWDQ2TO.js} +1 -1
- package/chunks/{chunk-GTED3FSZ.js → chunk-WPJMA5AK.js} +4 -99
- package/chunks/{chunk-5NFIY2SJ.js → chunk-XJ2TBEV7.js} +3 -3
- package/chunks/{chunk-PVJUPIQG.js → chunk-XOVYV7TT.js} +6 -6
- package/chunks/{chunk-IXT64DV5.js → chunk-XPDJE537.js} +97 -1
- package/chunks/{chunk-YKWARO3T.js → chunk-YGYJ2FTH.js} +95 -21
- package/chunks/{chunk-JWSOUKFF.js → chunk-YJGPPPUB.js} +66 -21
- package/chunks/chunk-YP2DHYNI.js +650 -0
- package/chunks/{chunk-RVJVM5L4.js → chunk-Z2VLJEQJ.js} +5 -5
- package/chunks/chunk-ZPVSDB63.js +3839 -0
- package/chunks/{codex-models-6XVNFRXZ.js → codex-models-CI22KIHY.js} +9 -8
- package/chunks/{commands-FRPOFIO4.js → commands-C2WRSCNB.js} +909 -610
- package/chunks/dashboard-XO2X2SPD.js +75 -0
- package/chunks/{db-BXJNWUYJ.js → db-FU3OIGQK.js} +5 -4
- package/chunks/{deep-review-GSJEBLCG.js → deep-review-5AL6QYTK.js} +37 -38
- package/chunks/{disclose-OU6Z6GVT.js → disclose-YPV5KIUU.js} +6 -5
- package/chunks/{dist-M2X367VW.js → dist-JAZWL3SC.js} +3 -2
- package/chunks/{dist-TQMHMSBZ.js → dist-ONIRKDH6.js} +376 -333
- package/chunks/{dist-RKLQIXDT.js → dist-QLMPTFT7.js} +4 -3
- package/chunks/{dist-7KSYDDSG.js → dist-QXJKD743.js} +5 -2
- package/chunks/{dist-LNXGATR2.js → dist-WNAOCQGA.js} +5 -4
- package/chunks/eval-runner-D3RUCU3R.js +38 -0
- package/chunks/{exploit-agent-Y2P4GFIY.js → exploit-agent-4E4HXKUA.js} +1 -1
- package/chunks/{exploit-autoclimb-YRZ3TMS2.js → exploit-autoclimb-5GRXI6VQ.js} +1 -1
- package/chunks/{exploit-climb-4LEY4SDW.js → exploit-climb-AHQEJ2I2.js} +4 -3
- package/chunks/{fix-RQSIG75B.js → fix-CHD7LXDS.js} +9 -8
- package/chunks/{harness-3NVC3DDY.js → harness-E63KDHPG.js} +5 -4
- package/chunks/{http-conformance-U5F6GKYG.js → http-conformance-AXMUOE6C.js} +3 -2
- package/chunks/hunt-scan-ME2Z4CTC.js +43 -0
- package/chunks/{kernel-primitive-T47V3RD3.js → kernel-primitive-S566GXWP.js} +4 -3
- package/chunks/{kernel-vm-runner-AEL5E3SV.js → kernel-vm-runner-KREBY4DC.js} +4 -3
- package/chunks/{local-engine-ZCW4YC6E.js → local-engine-ZRTZVSSX.js} +7 -6
- package/chunks/managed-operation-ZMLLRCCQ.js +59 -0
- package/chunks/{memsafety-scan-L2Z7E7TE.js → memsafety-scan-Q2MBZEGJ.js} +1 -2
- package/chunks/{native-loop-26X2CHBV.js → native-loop-UV3Z4Z2F.js} +31 -16
- package/chunks/operator-services-JOCREEBA.js +79 -0
- package/chunks/orchestrate-ZF6ZTSIQ.js +62 -0
- package/chunks/{prepare-C3KGMBXT.js → prepare-PLDVGLDX.js} +7 -6
- package/chunks/{provider-model-discovery-QGUGL3XW.js → provider-model-discovery-3X6DON3Y.js} +7 -2
- package/chunks/{remote-workflow-runtime-DB52SINR.js → remote-workflow-runtime-GOI4TYP5.js} +4 -3
- package/chunks/routing-WCQLO5SP.js +61 -0
- package/chunks/{runtime-AUBLQUOH.js → runtime-6ZIXFTW5.js} +4 -3
- package/chunks/{runtime-U66JNSAY.js → runtime-TVH5ORUK.js} +13 -8
- package/chunks/{scan-stream-34F3LTPV.js → scan-stream-HJVTWZ44.js} +6 -4
- package/chunks/{scope-HSTG63TQ.js → scope-RCYXVGJR.js} +4 -3
- package/chunks/service-plugins-TN6KDX4L.js +63 -0
- package/chunks/{session-store-MKAAP3WL.js → session-store-6QR3CVUA.js} +8 -5
- package/chunks/{variant-candidates-LG767HWH.js → variant-candidates-SGLKPPHI.js} +9 -8
- package/chunks/{web-recon-prepass-NJP6J3E4.js → web-recon-prepass-GO7CTMVQ.js} +7 -6
- package/chunks/{workflow-runtime-AR627I26.js → workflow-runtime-CQTDASFA.js} +50 -49
- package/dashboard/assets/{abnfDiagram-VCTEODGH-Cq149Jpk.js → abnfDiagram-VCTEODGH-DDZsU70-.js} +1 -1
- package/dashboard/assets/{arc-CpP-3TRW.js → arc-iDXNTSL6.js} +1 -1
- package/dashboard/assets/architecture-7GRP2DOG-DJcY9RFg.js +1 -0
- package/dashboard/assets/architectureDiagram-5GKGNRK7-d0ZuOniQ.js +36 -0
- package/dashboard/assets/{blockDiagram-I7D4REHJ-CZYkRRDi.js → blockDiagram-I7D4REHJ-B-EWfMy1.js} +2 -2
- package/dashboard/assets/{c4Diagram-7LVT6UL2-CV7Iu3_Z.js → c4Diagram-7LVT6UL2-KcZRuXZC.js} +1 -1
- package/dashboard/assets/channel-CeWMIm0w.js +1 -0
- package/dashboard/assets/{chunk-4HAMMTFA-PKCyW4m-.js → chunk-4HAMMTFA-DXWulBlo.js} +1 -1
- package/dashboard/assets/{chunk-75Z2AOVW-C5MlFTc_.js → chunk-75Z2AOVW-RiLZItrz.js} +1 -1
- package/dashboard/assets/chunk-DU6HZSFF-MBcaIyTX.js +127 -0
- package/dashboard/assets/{chunk-F27PBJKO-DTAok0qM.js → chunk-F27PBJKO-kLWCBX_W.js} +1 -1
- package/dashboard/assets/{chunk-FOHPRMQF-DHwB1DNv.js → chunk-FOHPRMQF-COOliuUh.js} +14 -14
- package/dashboard/assets/{chunk-GMAD6QVW-CPsr2F33.js → chunk-GMAD6QVW-BeQeSjpW.js} +7 -7
- package/dashboard/assets/{chunk-GVQU2GXP-C2AXGf9A.js → chunk-GVQU2GXP-CjCMlOIB.js} +1 -1
- package/dashboard/assets/{chunk-IMKFNOWR-DMG6yD_O.js → chunk-IMKFNOWR-BwNst53D.js} +1 -1
- package/dashboard/assets/{chunk-L3NEJ4N5-BSvwdbXx.js → chunk-L3NEJ4N5-CHXWQrkL.js} +1 -1
- package/dashboard/assets/{chunk-OSK3NFVY-CfB8V7FU.js → chunk-OSK3NFVY-B2vd19Ed.js} +1 -1
- package/dashboard/assets/{chunk-P2QGCYS3-CvmZvq6n.js → chunk-P2QGCYS3-Dk7Fyh6n.js} +1 -1
- package/dashboard/assets/{chunk-POPQ4Y6H-D_TvwDxL.js → chunk-POPQ4Y6H-CPAgOvYe.js} +1 -1
- package/dashboard/assets/{chunk-PWAF6VOD-Da5GnuOY.js → chunk-PWAF6VOD-BotyfMha.js} +1 -1
- package/dashboard/assets/{chunk-SHT3W25Y-CcsMJpR-.js → chunk-SHT3W25Y-Buz5gLRP.js} +1 -1
- package/dashboard/assets/{chunk-SVP7TREG-COojjieJ.js → chunk-SVP7TREG-_rpYEZcs.js} +1 -1
- package/dashboard/assets/{chunk-TICWLB2K-nBAmJwx6.js → chunk-TICWLB2K-DOJxPoec.js} +2 -2
- package/dashboard/assets/{chunk-XXDRQBXY-r5HyhWDv.js → chunk-XXDRQBXY-EXxKJbAs.js} +1 -1
- package/dashboard/assets/classDiagram-ZZMXUADV-DV_h1NoS.js +1 -0
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-DV_h1NoS.js +1 -0
- package/dashboard/assets/common-VLJRx-Jq.js +5 -0
- package/dashboard/assets/cose-bilkent-JH36ORCC-B1SVC57o.js +1 -0
- package/dashboard/assets/cynefin-OW5HDTMX-BzhRorj4.js +1 -0
- package/dashboard/assets/{cynefinDiagram-5FMLGOSQ-xodkuQtW.js → cynefinDiagram-5FMLGOSQ-JXzRmldX.js} +1 -1
- package/dashboard/assets/cytoscape.esm-BbBREfsg.js +321 -0
- package/dashboard/assets/dagre-D7JM2Yhv.js +1 -0
- package/dashboard/assets/dagre-GXQ25YYZ-1LZ-X0-Z.js +4 -0
- package/dashboard/assets/{diagram-S7CK7UJ4-DNoTMlCO.js → diagram-S7CK7UJ4-C_-J_LQ4.js} +2 -2
- package/dashboard/assets/{diagram-UQ7AKVKN-COccuyew.js → diagram-UQ7AKVKN-CnxL1k8g.js} +1 -1
- package/dashboard/assets/{diagram-VSXAHHWV-g6D1tWRb.js → diagram-VSXAHHWV-WzSb3aby.js} +1 -1
- package/dashboard/assets/{diagram-VX7I27RA-DhO4eVIC.js → diagram-VX7I27RA-DKhrMInE.js} +1 -1
- package/dashboard/assets/{diagram-Z3DM3KII-DeOQFexd.js → diagram-Z3DM3KII-CDhI2WGP.js} +1 -1
- package/dashboard/assets/{dist-BQYeOQg3.js → dist-B0bfBMeP.js} +1 -1
- package/dashboard/assets/{ebnfDiagram-PWID7BFC-MtkX2feM.js → ebnfDiagram-PWID7BFC-iL5NQ1PE.js} +1 -1
- package/dashboard/assets/{erDiagram-RLTQ6QDP-DIWSsoYZ.js → erDiagram-RLTQ6QDP-RK2Voa5O.js} +1 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-WgE7OJBb.js +1 -0
- package/dashboard/assets/flowDiagram-HODETNUW-B9YcQ0Aq.js +1 -0
- package/dashboard/assets/{ganttDiagram-EL5Y4UJY-DKggkJe0.js → ganttDiagram-EL5Y4UJY-D8CJXHRF.js} +1 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-BuZ9Lg04.js +1 -0
- package/dashboard/assets/{gitGraphDiagram-WWUBYQGX-r_iR8Qzo.js → gitGraphDiagram-WWUBYQGX-LEJMh54j.js} +1 -1
- package/dashboard/assets/info-A6RAGUB7-BL5k3pev.js +1 -0
- package/dashboard/assets/{infoDiagram-27XIBGKW-ClQk7UyQ.js → infoDiagram-27XIBGKW-LJvntUnZ.js} +1 -1
- package/dashboard/assets/{ishikawaDiagram-5VMMS53U-tR7Vziog.js → ishikawaDiagram-5VMMS53U-BYTYgKCG.js} +2 -2
- package/dashboard/assets/{journeyDiagram-3NMN7TZE-Cpb9IqKL.js → journeyDiagram-3NMN7TZE-G25qgQgO.js} +1 -1
- package/dashboard/assets/{kanban-definition-UXKFOSKX-BzUuKbA9.js → kanban-definition-UXKFOSKX-Bk3uLl1i.js} +1 -1
- package/dashboard/assets/{katex-BdBmVSus.js → katex-nLxf_RlZ.js} +2 -2
- package/dashboard/assets/{line-BHY_tSHu.js → line-i4qHBaWO.js} +1 -1
- package/dashboard/assets/linear-DmrqRsKG.js +1 -0
- package/dashboard/assets/mermaid-parser.core-BxbKr4QF.js +7 -0
- package/dashboard/assets/{mermaid.core-D_QsUPU8.js → mermaid.core-Cltee6FS.js} +9 -9
- package/dashboard/assets/{mindmap-definition-YA3MSWOX-CtfLTPPJ.js → mindmap-definition-YA3MSWOX-DkzO2JgS.js} +1 -1
- package/dashboard/assets/operations-B4_timQX.css +1 -0
- package/dashboard/assets/operations-DwppJviL.js +199 -0
- package/dashboard/assets/packet-AYTQ26CC-uZQUKnrE.js +1 -0
- package/dashboard/assets/{pegDiagram-XKGWAZYB-C1Dclbzj.js → pegDiagram-XKGWAZYB-CrOEi00T.js} +1 -1
- package/dashboard/assets/pie-WAS4IAKB-Dn9_tpQT.js +1 -0
- package/dashboard/assets/{pieDiagram-E7YTZNPT-Cd7XwYo5.js → pieDiagram-E7YTZNPT-D-niUkzc.js} +1 -1
- package/dashboard/assets/{quadrantDiagram-AXDQQJYC-CaS-GQ4X.js → quadrantDiagram-AXDQQJYC-CJOTiG5V.js} +1 -1
- package/dashboard/assets/radar-RG4KPBEZ-DIIVz4e8.js +1 -0
- package/dashboard/assets/railroad-74A4TZTK-1kiqDEID.js +1 -0
- package/dashboard/assets/railroad-abnf-HS5TGJTU-DK6rQPZR.js +1 -0
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-C6cOgYW4.js +1 -0
- package/dashboard/assets/railroad-peg-WCYAUIDC-CQ3ptKef.js +1 -0
- package/dashboard/assets/{railroadDiagram-O6MQD6OU-BeesTrqn.js → railroadDiagram-O6MQD6OU-nEfkpa-m.js} +1 -1
- package/dashboard/assets/{requirementDiagram-BXWQKSXE-IjyK0DhW.js → requirementDiagram-BXWQKSXE-CdFMY8tG.js} +1 -1
- package/dashboard/assets/{sankeyDiagram-P5KCCOFB-DmIluQqo.js → sankeyDiagram-P5KCCOFB-C0KxrFYv.js} +1 -1
- package/dashboard/assets/{sequenceDiagram-WJ2MYXX4-CUp8ZjTc.js → sequenceDiagram-WJ2MYXX4-DeZ5ATiu.js} +2 -2
- package/dashboard/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dashboard/assets/src-Cg-chcoE.js +1 -0
- package/dashboard/assets/{stateDiagram-D77RDMKH-Dy9TQb8P.js → stateDiagram-D77RDMKH-B9MwWL5B.js} +1 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-SucFndhx.js +1 -0
- package/dashboard/assets/swimlanes-42K2YHIH-DB--fwQE.js +1 -0
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-BzH-rIaK.js +8 -0
- package/dashboard/assets/{timeline-definition-24CTP7MA-BGdXW-iY.js → timeline-definition-24CTP7MA-DLkkTbWb.js} +1 -1
- package/dashboard/assets/treeView-Q6P3EWNA-XkZ9rl9H.js +1 -0
- package/dashboard/assets/treemap-WGGIJYW6-JCtVKxCE.js +1 -0
- package/dashboard/assets/{vennDiagram-4TSXK5OY-DzKWHJsl.js → vennDiagram-4TSXK5OY-DEb-x29Z.js} +1 -1
- package/dashboard/assets/wardley-WFR3VGLG-DhI7ROY3.js +1 -0
- package/dashboard/assets/{wardleyDiagram-VM6X3IG4-DGsjYmNJ.js → wardleyDiagram-VM6X3IG4-KNckudc5.js} +1 -1
- package/dashboard/assets/{xychartDiagram-S5SC5T6Z-C7_T_tWt.js → xychartDiagram-S5SC5T6Z-D4c0ynCk.js} +1 -1
- package/dashboard/index.html +13 -2
- package/package.json +2 -5
- package/worker-assets/demo-target/README.md +7 -3
- package/chunks/chunk-2XGBEH53.js +0 -4304
- package/chunks/chunk-4ROZFLSR.js +0 -279
- package/chunks/chunk-56UTQKXO.js +0 -56
- package/chunks/chunk-BPU6M42R.js +0 -77
- package/chunks/chunk-H5QURBBK.js +0 -1123
- package/chunks/chunk-IIBNQTOK.js +0 -1532
- package/chunks/chunk-NJLKFC66.js +0 -4790
- package/chunks/chunk-OOMBOLAF.js +0 -295
- package/chunks/chunk-VIT5ALXF.js +0 -4054
- package/chunks/chunk-VU7V2SDW.js +0 -284
- package/chunks/chunk-W624J7NF.js +0 -117
- package/chunks/chunk-YSQWZ67M.js +0 -6027
- package/chunks/dashboard-5HDSNBAU.js +0 -76
- package/chunks/eval-runner-A2DIGVP7.js +0 -31
- package/chunks/hunt-scan-W246VWJC.js +0 -40
- package/chunks/managed-operation-6R2QPPW5.js +0 -60
- package/chunks/orchestrate-OBS37FWQ.js +0 -63
- package/chunks/run-MU3MFL2Z.js +0 -28242
- package/chunks/service-plugins-5B23PWXC.js +0 -64
- package/dashboard/assets/architecture-7GRP2DOG-z5EHYkyH.js +0 -1
- package/dashboard/assets/architectureDiagram-5GKGNRK7-ewtXpjR_.js +0 -36
- package/dashboard/assets/channel-CuYjfiC5.js +0 -1
- package/dashboard/assets/chunk-DU6HZSFF-3M37ulcO.js +0 -127
- package/dashboard/assets/classDiagram-ZZMXUADV-DCuOrWUV.js +0 -1
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-DCuOrWUV.js +0 -1
- package/dashboard/assets/common-C6snVPsk.js +0 -5
- package/dashboard/assets/cose-bilkent-JH36ORCC-BLGcaZHk.js +0 -1
- package/dashboard/assets/cynefin-OW5HDTMX-DbZMK00I.js +0 -1
- package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +0 -321
- package/dashboard/assets/dagre-GXQ25YYZ-CRk9qfSG.js +0 -4
- package/dashboard/assets/dagre-PrKaheQc.js +0 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-D_3ez4Gv.js +0 -1
- package/dashboard/assets/flowDiagram-HODETNUW-D2boyYEd.js +0 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-DFOQYwXc.js +0 -1
- package/dashboard/assets/info-A6RAGUB7-DeednLoB.js +0 -1
- package/dashboard/assets/linear-naZZ1pHk.js +0 -1
- package/dashboard/assets/mermaid-parser.core-CPvd4cEx.js +0 -7
- package/dashboard/assets/operations-BwCzhVnT.js +0 -130
- package/dashboard/assets/operations-QjKT0Hwi.css +0 -1
- package/dashboard/assets/packet-AYTQ26CC-DDQbkT9I.js +0 -1
- package/dashboard/assets/pie-WAS4IAKB-LpU4ULAs.js +0 -1
- package/dashboard/assets/radar-RG4KPBEZ-CcxW3NJm.js +0 -1
- package/dashboard/assets/railroad-74A4TZTK-9P5Rl8fP.js +0 -1
- package/dashboard/assets/railroad-abnf-HS5TGJTU-CSfpwjna.js +0 -1
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-D6R5qzr-.js +0 -1
- package/dashboard/assets/railroad-peg-WCYAUIDC-DKEX7ITG.js +0 -1
- package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +0 -1
- package/dashboard/assets/src-r61wDjcA.js +0 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-CQb0ZtPr.js +0 -1
- package/dashboard/assets/swimlanes-42K2YHIH-CZuDV7V5.js +0 -1
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-BxgWMq8X.js +0 -8
- package/dashboard/assets/treeView-Q6P3EWNA-DlRrayjM.js +0 -1
- package/dashboard/assets/treemap-WGGIJYW6-C6UqLKfI.js +0 -1
- package/dashboard/assets/wardley-WFR3VGLG-thmyHKb0.js +0 -1
|
@@ -4,24 +4,23 @@ const require = __0CreateRequire(import.meta.url);
|
|
|
4
4
|
import {
|
|
5
5
|
ConsoleGateway,
|
|
6
6
|
ConsoleGatewayError,
|
|
7
|
-
WebOperatorServices,
|
|
8
7
|
WebWorkflowService,
|
|
9
|
-
WorkflowEngineService
|
|
10
|
-
|
|
8
|
+
WorkflowEngineService,
|
|
9
|
+
buildFindingConsoleCommand,
|
|
10
|
+
createLocalConsoleSession,
|
|
11
|
+
findingFromRow,
|
|
12
|
+
loadFindingFocus
|
|
13
|
+
} from "./chunk-UQS7XCBV.js";
|
|
11
14
|
import {
|
|
12
15
|
assertLocalEngineAvailable,
|
|
13
16
|
registerLocalEngine
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import {
|
|
16
|
-
findingFromRow as findingFromRow2
|
|
17
|
-
} from "./chunk-W624J7NF.js";
|
|
17
|
+
} from "./chunk-Z2VLJEQJ.js";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
WebOperatorServices,
|
|
20
|
+
createRemoteJWKSet,
|
|
21
21
|
createSessionPluginHostManager,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from "./chunk-2XGBEH53.js";
|
|
22
|
+
jwtVerify
|
|
23
|
+
} from "./chunk-ZPVSDB63.js";
|
|
25
24
|
import {
|
|
26
25
|
BackendConnectionRegistry,
|
|
27
26
|
backendBearerFromEnv,
|
|
@@ -29,13 +28,11 @@ import {
|
|
|
29
28
|
handleBackendConnectionRequest,
|
|
30
29
|
loadManagedWorker,
|
|
31
30
|
validateWorkerOptions
|
|
32
|
-
} from "./chunk-
|
|
33
|
-
import {
|
|
34
|
-
Option
|
|
35
|
-
} from "./chunk-WUPN3D37.js";
|
|
31
|
+
} from "./chunk-UGCTPKJG.js";
|
|
36
32
|
import {
|
|
33
|
+
Option,
|
|
37
34
|
presentationEventBus
|
|
38
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-S4DFH2FJ.js";
|
|
39
36
|
import {
|
|
40
37
|
source_default
|
|
41
38
|
} from "./chunk-HLFS6WV2.js";
|
|
@@ -61,43 +58,47 @@ import {
|
|
|
61
58
|
validateWorkbenchSourceLesson,
|
|
62
59
|
workbenchConfigPath,
|
|
63
60
|
workbenchNetworkEnabled
|
|
64
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-V2KJQVYE.js";
|
|
65
62
|
import {
|
|
66
63
|
connectServicePlugins,
|
|
67
64
|
createConsoleRuntime,
|
|
68
65
|
exportReport,
|
|
69
66
|
isReportExportFormat
|
|
70
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-3MWQNNJU.js";
|
|
71
68
|
import {
|
|
72
69
|
HuntMemoryStore,
|
|
73
70
|
TOOL_DEFINITIONS,
|
|
74
71
|
discoverAgentSkills,
|
|
72
|
+
listBrowserViews,
|
|
75
73
|
loadSkillRegistry,
|
|
74
|
+
parseImpactAssessment,
|
|
76
75
|
readToolCallNames,
|
|
76
|
+
subscribeBrowserView,
|
|
77
77
|
validateAgentSkillContent
|
|
78
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-SIR7XW2S.js";
|
|
79
79
|
import {
|
|
80
80
|
SecurityWorkflowStore,
|
|
81
81
|
WorkflowTriggerStore,
|
|
82
82
|
findingStatuses,
|
|
83
83
|
nextWorkflowTriggerFire,
|
|
84
84
|
osecDB,
|
|
85
|
-
resolveOsecDbPath
|
|
86
|
-
|
|
85
|
+
resolveOsecDbPath,
|
|
86
|
+
restoreFindingReviewFields
|
|
87
|
+
} from "./chunk-72PU46MS.js";
|
|
87
88
|
import {
|
|
88
89
|
LlmApiRuntime
|
|
89
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-HS2NQ7K6.js";
|
|
90
91
|
import {
|
|
91
92
|
DEFAULT_WORKBENCH_BROKER_LIMITS,
|
|
92
93
|
approveSmolvmWorkbenchImage,
|
|
93
94
|
getSmolvmWorkbenchStatus,
|
|
94
95
|
isAdmittedSmolvmWorkbench,
|
|
95
96
|
resolveSmolvmRuntime
|
|
96
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-XOVYV7TT.js";
|
|
97
98
|
import {
|
|
98
99
|
ScopePolicy,
|
|
99
100
|
withScopeEnforcement
|
|
100
|
-
} from "./chunk-
|
|
101
|
+
} from "./chunk-RZZ6VPRY.js";
|
|
101
102
|
import {
|
|
102
103
|
BackendDescriptorSchema,
|
|
103
104
|
BackendRequestIdSchema,
|
|
@@ -106,14 +107,14 @@ import {
|
|
|
106
107
|
VERSION,
|
|
107
108
|
createPresentationEvent,
|
|
108
109
|
homeStateDir
|
|
109
|
-
} from "./chunk-
|
|
110
|
+
} from "./chunk-GYEYTNHK.js";
|
|
110
111
|
import {
|
|
111
112
|
external_exports
|
|
112
|
-
} from "./chunk-
|
|
113
|
+
} from "./chunk-5XOYTAOK.js";
|
|
113
114
|
|
|
114
115
|
// packages/cli/src/commands/dashboard.ts
|
|
115
116
|
import { createServer as createServer2 } from "node:http";
|
|
116
|
-
import { createHash as createHash8, randomUUID as
|
|
117
|
+
import { createHash as createHash8, randomUUID as randomUUID9, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
117
118
|
import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
|
|
118
119
|
import { existsSync as existsSync2, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
119
120
|
import { dirname as dirname2, extname, join as join7, resolve as resolve4, sep } from "node:path";
|
|
@@ -132,6 +133,44 @@ import { createServer } from "node:http";
|
|
|
132
133
|
import { randomUUID } from "node:crypto";
|
|
133
134
|
import { spawn } from "node:child_process";
|
|
134
135
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
136
|
+
|
|
137
|
+
// packages/cli/src/tui/findings-data.ts
|
|
138
|
+
function parseJsonColumn(raw) {
|
|
139
|
+
if (!raw) return void 0;
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(raw);
|
|
142
|
+
} catch {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function findingFromRow2(row) {
|
|
147
|
+
const impactAssessment = parseJsonColumn(row.impactAssessment) === void 0 ? void 0 : parseImpactAssessment(row.impactAssessment) ?? void 0;
|
|
148
|
+
const record = {
|
|
149
|
+
id: row.id,
|
|
150
|
+
templateId: row.templateId,
|
|
151
|
+
title: row.title,
|
|
152
|
+
description: row.description,
|
|
153
|
+
severity: row.severity,
|
|
154
|
+
cvssScore: row.cvssScore ?? void 0,
|
|
155
|
+
cvssVector: row.cvssVector ?? void 0,
|
|
156
|
+
category: row.category,
|
|
157
|
+
status: row.status,
|
|
158
|
+
fingerprint: row.fingerprint ?? void 0,
|
|
159
|
+
triageStatus: row.triageStatus ?? void 0,
|
|
160
|
+
timestamp: row.timestamp,
|
|
161
|
+
evidence: {
|
|
162
|
+
request: row.evidenceRequest,
|
|
163
|
+
response: row.evidenceResponse,
|
|
164
|
+
analysis: row.evidenceAnalysis ?? void 0
|
|
165
|
+
},
|
|
166
|
+
verificationSpec: parseJsonColumn(row.verificationSpec),
|
|
167
|
+
impactAssessment,
|
|
168
|
+
...restoreFindingReviewFields(row)
|
|
169
|
+
};
|
|
170
|
+
return record;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// packages/cli/src/workbench-console-guest.ts
|
|
135
174
|
var CALLBACKS = ["requestScope", "requestLocalScope", "approveTool", "escalateScopedAudit", "askOperator"];
|
|
136
175
|
var EVENTS = ["onHarnessUpdate", "onAssistantDelta", "onReasoningDelta", "onToolStart", "onToolResult", "onUsage", "onNotice", "onCompaction"];
|
|
137
176
|
async function runGuest(cli) {
|
|
@@ -156,9 +195,9 @@ async function runGuest(cli) {
|
|
|
156
195
|
let policy;
|
|
157
196
|
const callback = (name, args) => {
|
|
158
197
|
if (decisions.size >= WORKBENCH_MAX_PENDING) return Promise.reject(new Error("Workbench decision limit reached"));
|
|
159
|
-
const
|
|
160
|
-
emit({ type: "decision", id, name, args });
|
|
161
|
-
return new Promise((resolve5, reject) => decisions.set(
|
|
198
|
+
const id2 = randomUUID();
|
|
199
|
+
emit({ type: "decision", id: id2, name, args });
|
|
200
|
+
return new Promise((resolve5, reject) => decisions.set(id2, { resolve: resolve5, reject }));
|
|
162
201
|
};
|
|
163
202
|
const server = createServer(async (request, response) => {
|
|
164
203
|
if (request.method !== "POST" || request.url !== "/provider/request" || providers.size >= 4) {
|
|
@@ -175,12 +214,12 @@ async function runGuest(cli) {
|
|
|
175
214
|
}
|
|
176
215
|
const envelope = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
177
216
|
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) throw new Error("Invalid provider request");
|
|
178
|
-
const
|
|
179
|
-
providers.set(
|
|
217
|
+
const id2 = randomUUID();
|
|
218
|
+
providers.set(id2, response);
|
|
180
219
|
response.once("close", () => {
|
|
181
|
-
if (providers.delete(
|
|
220
|
+
if (providers.delete(id2)) emit({ type: "provider-cancel", id: id2 });
|
|
182
221
|
});
|
|
183
|
-
emit({ type: "provider", id, envelope });
|
|
222
|
+
emit({ type: "provider", id: id2, envelope });
|
|
184
223
|
} catch {
|
|
185
224
|
response.writeHead(400).end("Invalid provider request");
|
|
186
225
|
}
|
|
@@ -205,6 +244,7 @@ async function runGuest(cli) {
|
|
|
205
244
|
tools: session.tools,
|
|
206
245
|
messages: session.messages,
|
|
207
246
|
autonomyMode: session.autonomyMode,
|
|
247
|
+
webSearchEnabled: session.webSearchEnabled,
|
|
208
248
|
target: session.target,
|
|
209
249
|
scope: session.scope?.raw,
|
|
210
250
|
localScopePath: session.localScopePath,
|
|
@@ -212,7 +252,7 @@ async function runGuest(cli) {
|
|
|
212
252
|
};
|
|
213
253
|
await mkdir("/home/zero/.0/controller", { recursive: true, mode: 448 });
|
|
214
254
|
await writeFile("/home/zero/.0/controller/session.json", JSON.stringify(scrub(snapshot)), { mode: 384 });
|
|
215
|
-
emit({ type: "state", snapshot: checkpoint?.harnessRoot ? {
|
|
255
|
+
emit({ type: "state", snapshot: { ...snapshot, ...checkpoint?.harnessRoot ? { checkpoint: void 0 } : {}, sourceContext: session.sourceContext } });
|
|
216
256
|
const db = new osecDB("/home/zero/.0/controller.sqlite");
|
|
217
257
|
try {
|
|
218
258
|
const rows = db.getFindings(session.scanId);
|
|
@@ -373,6 +413,23 @@ async function runGuest(cli) {
|
|
|
373
413
|
}
|
|
374
414
|
});
|
|
375
415
|
};
|
|
416
|
+
if (frame.callbacks.includes("skillRead")) callbacks.skillAuthoring = {
|
|
417
|
+
read: (id2) => callback("skillRead", [id2]),
|
|
418
|
+
create: (content) => callback("skillCreate", [content]),
|
|
419
|
+
update: (id2, input) => callback("skillSave", [id2, input]),
|
|
420
|
+
copy: (id2, input) => callback("skillCopy", [id2, input])
|
|
421
|
+
};
|
|
422
|
+
if (frame.callbacks.includes("skillEditorRead")) callbacks.skillEditor = { read: () => callback("skillEditorRead", []) };
|
|
423
|
+
if (frame.callbacks.includes("workflowList")) callbacks.workflowAuthoring = {
|
|
424
|
+
list: () => callback("workflowList", []),
|
|
425
|
+
save: (input) => callback("workflowSave", [input]),
|
|
426
|
+
...frame.callbacks.includes("workflowEditorRead") && typeof frame.workflowEditorId === "string" ? {
|
|
427
|
+
editor: { id: frame.workflowEditorId, read: () => callback("workflowEditorRead", []) }
|
|
428
|
+
} : {}
|
|
429
|
+
};
|
|
430
|
+
if (frame.callbacks.includes("workflowInvoke")) callbacks.workflowLifecycle = {
|
|
431
|
+
invoke: (name, args) => callback("workflowInvoke", [name, args])
|
|
432
|
+
};
|
|
376
433
|
if (frame.callbacks.includes("historyList")) callbacks.conversationHistory = { list: (...args) => callback("historyList", args), read: (...args) => callback("historyRead", args) };
|
|
377
434
|
pluginManager = await createSessionPluginHostManager({ projectPath: "/workspace", reservedToolNames: Object.values(TOOL_DEFINITIONS).map((tool) => tool.name), coreVersion: VERSION });
|
|
378
435
|
const unavailable = pluginManager.current().status().filter((entry) => entry.state !== "ready");
|
|
@@ -399,15 +456,22 @@ async function runGuest(cli) {
|
|
|
399
456
|
if (typeof frame.text !== "string") throw new Error("Invalid operator message");
|
|
400
457
|
turn = new AbortController();
|
|
401
458
|
const callbacks = Object.fromEntries(EVENTS.map((name) => [name, (...args) => emit({ type: "event", name, args })]));
|
|
402
|
-
const result = withScopeEnforcement(policy, () => session.send(frame.text, callbacks, { signal: turn.signal, generateTitle: frame.generateTitle === true }));
|
|
459
|
+
const result = withScopeEnforcement(policy, () => session.send(frame.text, callbacks, { signal: turn.signal, generateTitle: frame.generateTitle === true, images: frame.images }));
|
|
403
460
|
await state();
|
|
404
461
|
value = await result;
|
|
405
462
|
turn = void 0;
|
|
406
463
|
break;
|
|
407
464
|
}
|
|
408
|
-
case "autonomy":
|
|
465
|
+
case "autonomy": {
|
|
466
|
+
if (typeof frame.value !== "string" || !["standard", "recon", "copilot", "yolo"].includes(frame.value)) throw new Error("Invalid autonomy mode");
|
|
409
467
|
session.setAutonomyMode(frame.value);
|
|
410
468
|
break;
|
|
469
|
+
}
|
|
470
|
+
case "webSearch": {
|
|
471
|
+
if (typeof frame.value !== "boolean") throw new Error("Invalid web search setting");
|
|
472
|
+
await session.setWebSearchEnabled(frame.value);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
411
475
|
case "configure": {
|
|
412
476
|
const value2 = frame.value;
|
|
413
477
|
session.configureEngagement({ target: value2.target, ...value2.scope === void 0 ? {} : { scope: value2.scope ? new ScopePolicy(value2.scope) : null } });
|
|
@@ -514,7 +578,7 @@ async function setupWorkbench(options, home = process.env.HOME || homedir()) {
|
|
|
514
578
|
const workspaceRoot = options.workspace === void 0 ? previous?.workspaceRoot : realpathSync(resolve(options.workspace));
|
|
515
579
|
if (workspaceRoot !== void 0 && !lstatSync(workspaceRoot).isDirectory()) throw new Error("Workbench workspace must be a directory.");
|
|
516
580
|
const providers = options.provider ?? previous?.providers ?? [];
|
|
517
|
-
for (const
|
|
581
|
+
for (const id2 of providers) if (!PROVIDERS.some((provider) => provider.id === id2)) throw new Error(`Unknown provider grant: ${id2}. Use 0 workbench providers.`);
|
|
518
582
|
const imageArchive = options.image ? resolve(options.image) : previous?.image;
|
|
519
583
|
if (!imageArchive) throw new Error("No approved workbench image is available. Supply a locally provisioned archive with 0 workbench setup --image <archive>. Docker is only needed to build an image, never to run this profile.");
|
|
520
584
|
const cpus = resource(options.cpus, previous?.cpus ?? 2, 64, "CPUs");
|
|
@@ -574,7 +638,7 @@ function registerWorkbenchCommand(program) {
|
|
|
574
638
|
workbench.command("console-agent", { hidden: true }).action(async () => {
|
|
575
639
|
await runWorkbenchConsoleGuest();
|
|
576
640
|
});
|
|
577
|
-
workbench.command("setup").description("Verify/provision the signed native runtime, approve a local image, and select SmolVM execution").option("--image <archive>", "Local OCI/Docker archive to digest-pin and approve; never a mutable registry tag").option("--state <directory>", "Private VM state directory (defaults to ~/.0/workbench)").option("--workspace <directory>", "Explicit workspace mount; otherwise each invocation mounts its current directory").option("--provider <id>", "Grant chatgpt-codex requests through the host provider broker; credentials stay on the host", (
|
|
641
|
+
workbench.command("setup").description("Verify/provision the signed native runtime, approve a local image, and select SmolVM execution").option("--image <archive>", "Local OCI/Docker archive to digest-pin and approve; never a mutable registry tag").option("--state <directory>", "Private VM state directory (defaults to ~/.0/workbench)").option("--workspace <directory>", "Explicit workspace mount; otherwise each invocation mounts its current directory").option("--provider <id>", "Grant chatgpt-codex requests through the host provider broker; credentials stay on the host", (id2, ids = []) => [...ids, id2]).option("--github", "Unsupported: GitHub credential forwarding is refused").option("--no-github", "Revoke the GitHub token grant").option("--cpus <count>", "Guest virtual CPUs").option("--memory <MiB>", "Guest RAM in MiB").option("--storage <GiB>", "Guest private writable storage in GiB").option("--sandbox-image <reference=archive>", "Approve an immutable image reference for brokered isolated container actions; repeat for multiple images", (selection, selections = []) => [...selections, selection]).action(async (options) => {
|
|
578
642
|
try {
|
|
579
643
|
const config = await setupWorkbench(options);
|
|
580
644
|
console.log(`SmolVM workbench selected. Approved image: ${config.imageDigest}`);
|
|
@@ -606,7 +670,7 @@ function registerWorkbenchCommand(program) {
|
|
|
606
670
|
try {
|
|
607
671
|
const config = loadWorkbenchConfig();
|
|
608
672
|
if (!config) throw new Error("No workbench is configured. Run 0 workbench setup first.");
|
|
609
|
-
if (options.provider !== void 0) config.providers = options.provider === "none" ? [] : options.provider.split(",").map((
|
|
673
|
+
if (options.provider !== void 0) config.providers = options.provider === "none" ? [] : options.provider.split(",").map((id2) => id2.trim());
|
|
610
674
|
if (options.github !== void 0) config.github = options.github;
|
|
611
675
|
if (options.currentWorkspace) delete config.workspaceRoot;
|
|
612
676
|
if (options.clearSandboxImages) delete config.approvedImages;
|
|
@@ -656,10 +720,775 @@ function findDashboardInstance(origin) {
|
|
|
656
720
|
});
|
|
657
721
|
}
|
|
658
722
|
|
|
723
|
+
// packages/cli/src/web/managed-host-reconnect.ts
|
|
724
|
+
function startManagedHostReconnect(options) {
|
|
725
|
+
const metadata = options.getMetadata ?? loadManagedWorker;
|
|
726
|
+
const now = options.now ?? Date.now;
|
|
727
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
728
|
+
const controller = new AbortController();
|
|
729
|
+
let busy = false;
|
|
730
|
+
const tick = async () => {
|
|
731
|
+
if (busy || controller.signal.aborted) return;
|
|
732
|
+
busy = true;
|
|
733
|
+
try {
|
|
734
|
+
const registry2 = options.getRegistry();
|
|
735
|
+
const hosts = registry2.list().filter((host) => host.id !== "local");
|
|
736
|
+
for (const id2 of attempts.keys()) if (!hosts.some((host) => host.id === id2)) attempts.delete(id2);
|
|
737
|
+
for (const host of hosts) {
|
|
738
|
+
if (controller.signal.aborted) break;
|
|
739
|
+
try {
|
|
740
|
+
if (metadata(host.id).autoReconnect === false || (attempts.get(host.id)?.next ?? 0) > now()) continue;
|
|
741
|
+
const view = await registry2.handshake(host.id).catch(() => null);
|
|
742
|
+
if (view?.backend.status === "connected") {
|
|
743
|
+
attempts.delete(host.id);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (controller.signal.aborted || !registry2.list().some((item) => item.id === host.id) || metadata(host.id).autoReconnect === false) continue;
|
|
747
|
+
try {
|
|
748
|
+
await options.repair(host.id, controller.signal);
|
|
749
|
+
if (!controller.signal.aborted) await options.onRecovered?.(host.id);
|
|
750
|
+
attempts.delete(host.id);
|
|
751
|
+
} catch {
|
|
752
|
+
const count = (attempts.get(host.id)?.count ?? 0) + 1;
|
|
753
|
+
attempts.set(host.id, { count, next: now() + [6e4, 12e4, 3e5][Math.min(count - 1, 2)] });
|
|
754
|
+
}
|
|
755
|
+
break;
|
|
756
|
+
} catch {
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
} catch {
|
|
760
|
+
} finally {
|
|
761
|
+
busy = false;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
const timer = setInterval(() => {
|
|
765
|
+
void tick();
|
|
766
|
+
}, 3e4);
|
|
767
|
+
timer.unref?.();
|
|
768
|
+
void tick();
|
|
769
|
+
return () => {
|
|
770
|
+
clearInterval(timer);
|
|
771
|
+
controller.abort();
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// packages/cli/src/web/local-provider-relay.ts
|
|
776
|
+
async function createLocalRelayProvider(selection = {}, signal) {
|
|
777
|
+
const [{ createWebConsoleRuntime }, { createWorkbenchProviderBroker }] = await Promise.all([
|
|
778
|
+
import("./operator-services-JOCREEBA.js"),
|
|
779
|
+
import("./dist-ONIRKDH6.js")
|
|
780
|
+
]);
|
|
781
|
+
signal?.throwIfAborted();
|
|
782
|
+
if (selection.providerId && selection.providerId !== "chatgpt-codex") throw new Error("Local model relay currently supports ChatGPT Codex only. Choose the local ChatGPT connection.");
|
|
783
|
+
const { runtime, info } = await createWebConsoleRuntime({ ...selection, providerId: "chatgpt-codex" });
|
|
784
|
+
signal?.throwIfAborted();
|
|
785
|
+
const provider = runtime.resolvedProvider();
|
|
786
|
+
const model = runtime.resolvedModel();
|
|
787
|
+
if (provider !== "chatgpt-codex" || !model || !info.configured || !info.diagnostics.valid) throw new Error("Local model relay requires a connected ChatGPT Codex account and an explicit model.");
|
|
788
|
+
const routing = runtime.modelSelection();
|
|
789
|
+
const models = [.../* @__PURE__ */ new Set([model, ...Object.values(routing.agentModels).filter((value) => value !== "auto")])];
|
|
790
|
+
const broker = createWorkbenchProviderBroker({ provider, models, resolveCredentials: runtime.workbenchCredentialResolver(), signal });
|
|
791
|
+
return { info, grant: broker.grant, transport: { provider, models: broker.grant.models, request: (request, requestSignal) => broker.request(request, requestSignal), close: broker.close } };
|
|
792
|
+
}
|
|
793
|
+
function object(value) {
|
|
794
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
795
|
+
}
|
|
796
|
+
function id(value) {
|
|
797
|
+
return typeof value === "string" && value.length > 0 && value.length <= 160 && !/[\x00-\x20\x7f]/.test(value);
|
|
798
|
+
}
|
|
799
|
+
var RelayResponseError = class extends Error {
|
|
800
|
+
constructor(code, status) {
|
|
801
|
+
super("Provider relay transport failed.");
|
|
802
|
+
this.code = code;
|
|
803
|
+
this.status = status;
|
|
804
|
+
this.name = "RelayResponseError";
|
|
805
|
+
}
|
|
806
|
+
code;
|
|
807
|
+
status;
|
|
808
|
+
};
|
|
809
|
+
function safeRelayFailure(error2) {
|
|
810
|
+
if (error2 instanceof RelayResponseError) return { name: error2.name, code: error2.code };
|
|
811
|
+
const name = error2 instanceof Error && ["Error", "TypeError", "SyntaxError", "AbortError", "TimeoutError", "BackendConnectionError"].includes(error2.name) ? error2.name : "OtherError";
|
|
812
|
+
const known = {
|
|
813
|
+
"Provider request cancelled or timed out": "provider_timeout_or_cancel",
|
|
814
|
+
"Provider response stream failed": "provider_stream_failed",
|
|
815
|
+
"Host provider request failed": "provider_request_failed",
|
|
816
|
+
"Provider broker concurrency limit reached": "provider_concurrency",
|
|
817
|
+
"Provider broker session budget exhausted": "provider_budget",
|
|
818
|
+
"Provider broker is closed": "provider_closed",
|
|
819
|
+
"Provider or model is not granted to this workbench": "provider_grant_rejected",
|
|
820
|
+
"Provider request exceeds its byte limit": "provider_input_limit",
|
|
821
|
+
"Invalid provider request JSON": "provider_input_json",
|
|
822
|
+
"Provider broker permits only stateless streamed Responses requests": "provider_body_shape",
|
|
823
|
+
"Provider broker refuses hosted tools and external tool destinations": "provider_tool_destination",
|
|
824
|
+
"Unsupported provider input item": "provider_input_type",
|
|
825
|
+
"Provider broker permits text input only": "provider_text_only",
|
|
826
|
+
"Remote provider relay scope changed.": "remote_scope_changed",
|
|
827
|
+
"Invalid provider cancellation.": "remote_cancel_invalid",
|
|
828
|
+
"Remote provider request exceeds its approval.": "remote_request_not_approved",
|
|
829
|
+
"Remote provider relay exceeded its request budget.": "relay_budget",
|
|
830
|
+
"Provider output exceeds approval.": "provider_output_limit",
|
|
831
|
+
"Provider SSE event exceeds response limit.": "provider_sse_limit",
|
|
832
|
+
"Backend response identity does not match the registered engine.": "remote_identity_changed",
|
|
833
|
+
"Backend response exceeds the transport limit.": "remote_response_limit",
|
|
834
|
+
"Backend request timed out.": "remote_timeout",
|
|
835
|
+
"Backend transport failed.": "remote_transport_failed",
|
|
836
|
+
"Backend request capacity exceeded.": "remote_capacity"
|
|
837
|
+
};
|
|
838
|
+
return { name, code: error2 instanceof Error ? Object.hasOwn(known, error2.message) ? known[error2.message] : "unclassified" : "unclassified" };
|
|
839
|
+
}
|
|
840
|
+
async function json(response) {
|
|
841
|
+
if (!response.ok) {
|
|
842
|
+
await response.body?.cancel();
|
|
843
|
+
throw new RelayResponseError("remote_http_rejected", response.status);
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
return object(await response.json());
|
|
847
|
+
} catch {
|
|
848
|
+
throw new RelayResponseError("remote_invalid_json", response.status);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async function pause(signal, delayMs = 500) {
|
|
852
|
+
if (signal.aborted) return;
|
|
853
|
+
await new Promise((resolve5) => {
|
|
854
|
+
const abort = () => {
|
|
855
|
+
clearTimeout(timer);
|
|
856
|
+
resolve5();
|
|
857
|
+
};
|
|
858
|
+
const timer = setTimeout(() => {
|
|
859
|
+
signal.removeEventListener("abort", abort);
|
|
860
|
+
resolve5();
|
|
861
|
+
}, delayMs);
|
|
862
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function retryablePollFailure(error2) {
|
|
866
|
+
if (error2 instanceof RelayResponseError) return [408, 425, 429, 500, 502, 503, 504].includes(error2.status);
|
|
867
|
+
if (!(error2 instanceof Error) || error2.name !== "BackendConnectionError") return false;
|
|
868
|
+
const status = error2.statusCode;
|
|
869
|
+
return status === 504 && error2.message === "Backend request timed out." || status === 502 && error2.message === "Backend transport failed." || status === 429 && error2.message === "Backend request capacity exceeded.";
|
|
870
|
+
}
|
|
871
|
+
function failureTransportStatus(error2, fallback) {
|
|
872
|
+
if (error2 instanceof RelayResponseError) return error2.status;
|
|
873
|
+
if (error2 instanceof Error && error2.name === "BackendConnectionError" && typeof error2.statusCode === "number") return error2.statusCode;
|
|
874
|
+
return fallback;
|
|
875
|
+
}
|
|
876
|
+
function providerSseCompletion(maximumBytes = 8 * 1024 * 1024) {
|
|
877
|
+
const decoder = new TextDecoder();
|
|
878
|
+
let buffer = "", completed = false;
|
|
879
|
+
return (chunk) => {
|
|
880
|
+
if (completed) return true;
|
|
881
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
882
|
+
let boundary;
|
|
883
|
+
while (boundary = /\r?\n\r?\n/.exec(buffer)) {
|
|
884
|
+
const block = buffer.slice(0, boundary.index);
|
|
885
|
+
if (Buffer.byteLength(block) > maximumBytes) throw new Error("Provider SSE event exceeds response limit.");
|
|
886
|
+
buffer = buffer.slice(boundary.index + boundary[0].length);
|
|
887
|
+
const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
888
|
+
if (!data) continue;
|
|
889
|
+
if (data === "[DONE]") {
|
|
890
|
+
completed = true;
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
const event = JSON.parse(data);
|
|
895
|
+
if (event && ["response.completed", "response.failed", "response.incomplete", "error"].includes(String(event.type))) {
|
|
896
|
+
completed = true;
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
} catch {
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (Buffer.byteLength(buffer) > maximumBytes) throw new Error("Provider SSE event exceeds response limit.");
|
|
903
|
+
return completed;
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
async function createLocalProviderRelay(options) {
|
|
907
|
+
if (options.backendId === "local" || !id(options.backendId)) throw new Error("Provider relay requires a registered remote host.");
|
|
908
|
+
const lifetime = new AbortController();
|
|
909
|
+
const signal = options.signal ? AbortSignal.any([lifetime.signal, options.signal]) : lifetime.signal;
|
|
910
|
+
signal.throwIfAborted();
|
|
911
|
+
const peer = await options.registry.handshake(options.backendId);
|
|
912
|
+
const epoch = peer.handshake?.serverInstanceId;
|
|
913
|
+
if (peer.backend.status !== "connected" || !id(epoch)) throw new Error("Provider relay requires a connected, identified host.");
|
|
914
|
+
const local = await (options.createProvider ?? createLocalRelayProvider)(options.selection ?? {}, signal);
|
|
915
|
+
const base = "/api/console/provider-relays";
|
|
916
|
+
let grant;
|
|
917
|
+
try {
|
|
918
|
+
const response = await json(await options.registry.request(options.backendId, base, { method: "POST", body: { epoch, provider: local.grant.provider, models: local.grant.models, model: local.info.model }, signal }));
|
|
919
|
+
const candidate = object(response.grant);
|
|
920
|
+
if (!id(candidate.id) || candidate.epoch !== epoch || candidate.provider !== local.grant.provider || !Array.isArray(candidate.models) || candidate.models.length !== local.grant.models.length || new Set(candidate.models).size !== candidate.models.length || candidate.models.some((model) => typeof model !== "string" || !local.grant.models.includes(model)) || candidate.model !== local.info.model || typeof candidate.expiresAt !== "string" || !Number.isFinite(Date.parse(candidate.expiresAt))) throw new Error("Remote provider relay grant does not match the local approval.");
|
|
921
|
+
grant = candidate;
|
|
922
|
+
} catch (error2) {
|
|
923
|
+
await local.transport.close?.();
|
|
924
|
+
throw error2;
|
|
925
|
+
}
|
|
926
|
+
const diagnostic = { phase: "registered", lastPollAt: null, seenRequests: 0, activeRequests: 0, bytesForwarded: 0 };
|
|
927
|
+
const grantPath = `${base}/${encodeURIComponent(grant.id)}`;
|
|
928
|
+
let revocation;
|
|
929
|
+
const revokeGrant = () => revocation ??= options.registry.request(options.backendId, grantPath, { method: "DELETE", signal: AbortSignal.timeout(5e3) }).then(async (response) => {
|
|
930
|
+
await response.body?.cancel();
|
|
931
|
+
}).catch(() => {
|
|
932
|
+
});
|
|
933
|
+
const active = /* @__PURE__ */ new Map();
|
|
934
|
+
const seen = /* @__PURE__ */ new Set();
|
|
935
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
936
|
+
let boundSession, polling, closing;
|
|
937
|
+
const abortActive = () => {
|
|
938
|
+
for (const controller of active.values()) controller.abort();
|
|
939
|
+
};
|
|
940
|
+
signal.addEventListener("abort", abortActive, { once: true });
|
|
941
|
+
const remoteRequest = async (path, optionsValue) => {
|
|
942
|
+
const response = await options.registry.request(options.backendId, path, optionsValue);
|
|
943
|
+
diagnostic.lastTransportStatus = response.status;
|
|
944
|
+
return response;
|
|
945
|
+
};
|
|
946
|
+
const send = (request, events, callerSignal) => remoteRequest(`${grantPath}/requests/${encodeURIComponent(request.requestId)}/events`, { method: "POST", body: { epoch, sessionId: boundSession, events }, signal: callerSignal }).then(json);
|
|
947
|
+
async function service(request) {
|
|
948
|
+
const controller = new AbortController();
|
|
949
|
+
active.set(request.requestId, controller);
|
|
950
|
+
diagnostic.phase = "requesting";
|
|
951
|
+
diagnostic.activeRequests = active.size;
|
|
952
|
+
delete diagnostic.error;
|
|
953
|
+
const requestSignal = AbortSignal.any([signal, controller.signal, AbortSignal.timeout(local.grant.limits.timeoutMs)]);
|
|
954
|
+
let reader;
|
|
955
|
+
const cancelReader = () => {
|
|
956
|
+
void reader?.cancel().catch(() => {
|
|
957
|
+
});
|
|
958
|
+
};
|
|
959
|
+
requestSignal.addEventListener("abort", cancelReader, { once: true });
|
|
960
|
+
let stage = "provider_request";
|
|
961
|
+
try {
|
|
962
|
+
const response = await local.transport.request({ provider: request.provider, model: request.model, body: request.body }, requestSignal);
|
|
963
|
+
diagnostic.lastUpstreamStatus = response.status;
|
|
964
|
+
stage = "headers_post";
|
|
965
|
+
await send(request, [{ kind: "headers", status: response.status, contentType: response.headers.get("content-type") || "text/event-stream" }], requestSignal);
|
|
966
|
+
diagnostic.phase = "streaming";
|
|
967
|
+
if (response.body) {
|
|
968
|
+
reader = response.body.getReader();
|
|
969
|
+
let bytes = 0;
|
|
970
|
+
const completed = response.headers.get("content-type")?.toLowerCase().includes("text/event-stream") ? providerSseCompletion(local.grant.limits.maxResponseBytes) : void 0;
|
|
971
|
+
while (!requestSignal.aborted) {
|
|
972
|
+
stage = "reader_read";
|
|
973
|
+
const chunk = await reader.read();
|
|
974
|
+
if (chunk.done) break;
|
|
975
|
+
bytes += chunk.value.byteLength;
|
|
976
|
+
if (bytes > local.grant.limits.maxResponseBytes) throw new Error("Provider output exceeds approval.");
|
|
977
|
+
stage = "chunk_post";
|
|
978
|
+
for (let offset = 0; offset < chunk.value.byteLength; offset += 32 * 1024) {
|
|
979
|
+
const portion = chunk.value.subarray(offset, offset + 32 * 1024);
|
|
980
|
+
await send(request, [{ kind: "chunk", data: Buffer.from(portion).toString("base64") }], requestSignal);
|
|
981
|
+
diagnostic.bytesForwarded += portion.byteLength;
|
|
982
|
+
}
|
|
983
|
+
stage = "sse_parse";
|
|
984
|
+
if (completed?.(chunk.value)) break;
|
|
985
|
+
}
|
|
986
|
+
requestSignal.throwIfAborted();
|
|
987
|
+
}
|
|
988
|
+
stage = "end_post";
|
|
989
|
+
await send(request, [{ kind: "end" }], requestSignal);
|
|
990
|
+
} catch (error2) {
|
|
991
|
+
if (!signal.aborted && !controller.signal.aborted) {
|
|
992
|
+
const failure = safeRelayFailure(error2);
|
|
993
|
+
diagnostic.lastFailureStage = stage;
|
|
994
|
+
diagnostic.lastFailureName = failure.name;
|
|
995
|
+
diagnostic.lastFailureCode = failure.code;
|
|
996
|
+
diagnostic.lastFailureTransportStatus = failureTransportStatus(error2, diagnostic.lastTransportStatus);
|
|
997
|
+
diagnostic.error = diagnostic.phase === "requesting" ? "upstream_failed" : "response_rejected";
|
|
998
|
+
await send(request, [{ kind: "error" }], AbortSignal.any([signal, AbortSignal.timeout(5e3)])).catch(() => {
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
} finally {
|
|
1002
|
+
requestSignal.removeEventListener("abort", cancelReader);
|
|
1003
|
+
controller.abort();
|
|
1004
|
+
if (reader) {
|
|
1005
|
+
await reader.cancel().catch(() => {
|
|
1006
|
+
});
|
|
1007
|
+
reader.releaseLock();
|
|
1008
|
+
}
|
|
1009
|
+
active.delete(request.requestId);
|
|
1010
|
+
diagnostic.activeRequests = active.size;
|
|
1011
|
+
if (!signal.aborted && !active.size) diagnostic.phase = "polling";
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async function poll() {
|
|
1015
|
+
let failureCategory = "poll_rejected";
|
|
1016
|
+
try {
|
|
1017
|
+
if (!active.size) diagnostic.phase = "polling";
|
|
1018
|
+
while (!signal.aborted) {
|
|
1019
|
+
failureCategory = "poll_rejected";
|
|
1020
|
+
let body;
|
|
1021
|
+
try {
|
|
1022
|
+
body = await json(await remoteRequest(`${grantPath}/requests`, { method: "GET", signal }));
|
|
1023
|
+
} catch (error2) {
|
|
1024
|
+
if (signal.aborted || !retryablePollFailure(error2)) throw error2;
|
|
1025
|
+
const failure = safeRelayFailure(error2);
|
|
1026
|
+
diagnostic.phase = "reconnecting";
|
|
1027
|
+
diagnostic.error = "poll_rejected";
|
|
1028
|
+
diagnostic.lastFailureStage = "poll";
|
|
1029
|
+
diagnostic.lastFailureName = failure.name;
|
|
1030
|
+
diagnostic.lastFailureCode = failure.code;
|
|
1031
|
+
diagnostic.lastFailureTransportStatus = failureTransportStatus(error2, diagnostic.lastTransportStatus);
|
|
1032
|
+
diagnostic.consecutivePollFailures = (diagnostic.consecutivePollFailures ?? 0) + 1;
|
|
1033
|
+
await pause(signal, Math.min(5e3, 500 * 2 ** Math.min(diagnostic.consecutivePollFailures - 1, 4)));
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
diagnostic.consecutivePollFailures = 0;
|
|
1037
|
+
if (diagnostic.error === "poll_rejected") delete diagnostic.error;
|
|
1038
|
+
diagnostic.phase = active.size ? "streaming" : "polling";
|
|
1039
|
+
diagnostic.lastPollAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1040
|
+
failureCategory = "scope_changed";
|
|
1041
|
+
if (body.grantId !== grant.id || body.epoch !== epoch || body.sessionId !== void 0 && body.sessionId !== boundSession || !Array.isArray(body.requests) || !Array.isArray(body.cancelled) || body.requests.length > 4 || body.cancelled.length > 256) throw new Error("Remote provider relay scope changed.");
|
|
1042
|
+
for (const requestId of body.cancelled) {
|
|
1043
|
+
if (!id(requestId)) throw new Error("Invalid provider cancellation.");
|
|
1044
|
+
active.get(requestId)?.abort();
|
|
1045
|
+
}
|
|
1046
|
+
for (const value of body.requests) {
|
|
1047
|
+
const request = object(value);
|
|
1048
|
+
failureCategory = "request_rejected";
|
|
1049
|
+
if (!id(request.requestId) || request.grantId !== grant.id || request.epoch !== epoch || request.sessionId !== boundSession || request.provider !== local.grant.provider || typeof request.model !== "string" || !local.grant.models.includes(request.model) || typeof request.body !== "string" || Buffer.byteLength(request.body) > local.grant.limits.maxRequestBytes) throw new Error("Remote provider request exceeds its approval.");
|
|
1050
|
+
if (seen.has(request.requestId)) continue;
|
|
1051
|
+
if (seen.size >= local.grant.limits.maxRequests || active.size >= Math.min(4, local.grant.limits.maxConcurrent)) throw new Error("Remote provider relay exceeded its request budget.");
|
|
1052
|
+
seen.add(request.requestId);
|
|
1053
|
+
diagnostic.seenRequests = seen.size;
|
|
1054
|
+
const task = service(request);
|
|
1055
|
+
tasks.add(task);
|
|
1056
|
+
void task.finally(() => tasks.delete(task));
|
|
1057
|
+
}
|
|
1058
|
+
await pause(signal);
|
|
1059
|
+
}
|
|
1060
|
+
} catch (error2) {
|
|
1061
|
+
if (!signal.aborted) {
|
|
1062
|
+
diagnostic.phase = "failed";
|
|
1063
|
+
diagnostic.error = failureCategory;
|
|
1064
|
+
const failure = safeRelayFailure(error2);
|
|
1065
|
+
diagnostic.lastFailureStage = "poll";
|
|
1066
|
+
diagnostic.lastFailureName = failure.name;
|
|
1067
|
+
diagnostic.lastFailureCode = failure.code;
|
|
1068
|
+
diagnostic.lastFailureTransportStatus = failureTransportStatus(error2, diagnostic.lastTransportStatus);
|
|
1069
|
+
}
|
|
1070
|
+
if (!signal.aborted) options.onError?.("The local model connection to this host ended. Reconnect the host before continuing.");
|
|
1071
|
+
lifetime.abort();
|
|
1072
|
+
} finally {
|
|
1073
|
+
if (signal.aborted && diagnostic.phase !== "failed") diagnostic.phase = "closed";
|
|
1074
|
+
abortActive();
|
|
1075
|
+
await local.transport.close?.();
|
|
1076
|
+
await revokeGrant();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
grant,
|
|
1081
|
+
info: local.info,
|
|
1082
|
+
status: () => ({ ...diagnostic }),
|
|
1083
|
+
start(sessionId) {
|
|
1084
|
+
if (!id(sessionId) || signal.aborted || boundSession) throw new Error("Provider relay must bind once to an active remote session.");
|
|
1085
|
+
boundSession = sessionId;
|
|
1086
|
+
polling = poll();
|
|
1087
|
+
},
|
|
1088
|
+
close() {
|
|
1089
|
+
return closing ??= (async () => {
|
|
1090
|
+
diagnostic.phase = "closed";
|
|
1091
|
+
lifetime.abort();
|
|
1092
|
+
abortActive();
|
|
1093
|
+
signal.removeEventListener("abort", abortActive);
|
|
1094
|
+
await local.transport.close?.();
|
|
1095
|
+
await Promise.allSettled([...tasks, ...polling ? [polling] : []]);
|
|
1096
|
+
await revokeGrant();
|
|
1097
|
+
})();
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// packages/cli/src/web/local-provider-sessions.ts
|
|
1103
|
+
var LocalProviderSessionError = class extends Error {
|
|
1104
|
+
constructor(message, statusCode = 400) {
|
|
1105
|
+
super(message);
|
|
1106
|
+
this.statusCode = statusCode;
|
|
1107
|
+
}
|
|
1108
|
+
statusCode;
|
|
1109
|
+
};
|
|
1110
|
+
var resourceId = external_exports.string().min(1).max(160).regex(/^[^\x00-\x20\x7f]+$/);
|
|
1111
|
+
var runtimeSchema = external_exports.object({ providerId: external_exports.string().min(1).max(256).optional(), model: external_exports.string().min(1).max(256).optional(), reasoningEffort: external_exports.string().min(1).max(256).optional(), agentModels: external_exports.record(external_exports.string().min(1).max(64), external_exports.string().min(1).max(256)).refine((value) => Object.keys(value).length <= 32).optional(), singleModel: external_exports.boolean().optional(), autoRoute: external_exports.boolean().optional() }).strict();
|
|
1112
|
+
var createSchema = external_exports.object({ workflowId: external_exports.string().min(1).max(128).optional(), target: external_exports.string().max(4096).optional(), title: external_exports.string().max(200).optional(), role: external_exports.enum(["discovery", "attack", "verify", "report", "audit", "review"]).optional(), autonomyMode: external_exports.enum(["standard", "recon", "copilot", "yolo"]).optional(), scope: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional(), runtime: runtimeSchema.optional(), findingId: external_exports.string().max(256).optional(), findingIntent: external_exports.enum(["investigate", "verify", "draft_fix", "impact"]).optional() }).strict();
|
|
1113
|
+
var LocalProviderSessions = class {
|
|
1114
|
+
constructor(options) {
|
|
1115
|
+
this.options = options;
|
|
1116
|
+
}
|
|
1117
|
+
options;
|
|
1118
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1119
|
+
lifetime = new AbortController();
|
|
1120
|
+
creating = 0;
|
|
1121
|
+
reconnecting = /* @__PURE__ */ new Set();
|
|
1122
|
+
key(backendId, sessionId) {
|
|
1123
|
+
return JSON.stringify([backendId, sessionId]);
|
|
1124
|
+
}
|
|
1125
|
+
signal() {
|
|
1126
|
+
return this.options.signal ? AbortSignal.any([this.lifetime.signal, this.options.signal]) : this.lifetime.signal;
|
|
1127
|
+
}
|
|
1128
|
+
assertBackend(backendId) {
|
|
1129
|
+
if (!resourceId.safeParse(backendId).success || backendId === "local") throw new LocalProviderSessionError("Choose a registered remote host.");
|
|
1130
|
+
this.signal().throwIfAborted();
|
|
1131
|
+
}
|
|
1132
|
+
async request(backendId, path, method, body) {
|
|
1133
|
+
const response = await this.options.registry.request(backendId, path, { method, body, signal: this.signal() });
|
|
1134
|
+
if (!response.ok) {
|
|
1135
|
+
await response.body?.cancel();
|
|
1136
|
+
throw new LocalProviderSessionError("The remote host rejected this conversation change.", response.status);
|
|
1137
|
+
}
|
|
1138
|
+
const value = await response.json();
|
|
1139
|
+
if (!value?.session || !resourceId.safeParse(value.session.id).success) throw new LocalProviderSessionError("The remote host returned an invalid conversation.", 502);
|
|
1140
|
+
return value.session;
|
|
1141
|
+
}
|
|
1142
|
+
async create(backendId, input, ownerId = "local") {
|
|
1143
|
+
this.assertBackend(backendId);
|
|
1144
|
+
const parsed = createSchema.safeParse(input);
|
|
1145
|
+
if (!parsed.success) throw new LocalProviderSessionError("Invalid remote conversation configuration; model grants are issued locally.");
|
|
1146
|
+
if (this.sessions.size + this.creating >= 32) throw new LocalProviderSessionError("Close a local model connection before creating another remote conversation.", 429);
|
|
1147
|
+
this.creating++;
|
|
1148
|
+
let relay;
|
|
1149
|
+
try {
|
|
1150
|
+
relay = await (this.options.createRelay ?? createLocalProviderRelay)({ registry: this.options.registry, backendId, selection: parsed.data.runtime, signal: this.signal() });
|
|
1151
|
+
const session = await this.request(backendId, "/api/console/sessions", "POST", { ...parsed.data, runtime: { ...parsed.data.runtime, providerId: relay.info.providerId, model: relay.info.model }, modelSource: { kind: "local", grantId: relay.grant.id } });
|
|
1152
|
+
this.signal().throwIfAborted();
|
|
1153
|
+
const key = this.key(backendId, session.id);
|
|
1154
|
+
if (this.sessions.has(key)) throw new LocalProviderSessionError("The host reused an existing conversation ID.", 409);
|
|
1155
|
+
relay.start(session.id);
|
|
1156
|
+
this.sessions.set(key, { owner: ownerId, relay, selection: parsed.data.runtime ?? {}, configuring: false });
|
|
1157
|
+
return { session };
|
|
1158
|
+
} catch (error2) {
|
|
1159
|
+
await relay?.close();
|
|
1160
|
+
throw error2;
|
|
1161
|
+
} finally {
|
|
1162
|
+
this.creating--;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async configure(backendId, sessionId, runtime, ownerId = "local") {
|
|
1166
|
+
this.assertBackend(backendId);
|
|
1167
|
+
if (!resourceId.safeParse(sessionId).success) throw new LocalProviderSessionError("Invalid conversation ID.");
|
|
1168
|
+
const managed = this.sessions.get(this.key(backendId, sessionId));
|
|
1169
|
+
if (!managed) return this.reconnect(backendId, sessionId, runtime, ownerId);
|
|
1170
|
+
if (managed.owner !== ownerId) throw new LocalProviderSessionError("This local model connection does not belong to the current workspace.", 404);
|
|
1171
|
+
const parsed = runtimeSchema.safeParse(runtime);
|
|
1172
|
+
if (!parsed.success) throw new LocalProviderSessionError("Invalid local model selection.");
|
|
1173
|
+
if (managed.configuring) throw new LocalProviderSessionError("Wait for the current model change.", 409);
|
|
1174
|
+
managed.configuring = true;
|
|
1175
|
+
let replacement;
|
|
1176
|
+
try {
|
|
1177
|
+
const selection = { ...managed.selection, ...parsed.data };
|
|
1178
|
+
replacement = await (this.options.createRelay ?? createLocalProviderRelay)({ registry: this.options.registry, backendId, selection, signal: this.signal() });
|
|
1179
|
+
const session = await this.request(backendId, `/api/console/sessions/${encodeURIComponent(sessionId)}/configuration`, "PATCH", { runtime: { ...selection, providerId: replacement.info.providerId, model: replacement.info.model }, modelSource: { kind: "local", grantId: replacement.grant.id } });
|
|
1180
|
+
if (session.id !== sessionId) throw new LocalProviderSessionError("The host returned a different conversation.", 409);
|
|
1181
|
+
this.signal().throwIfAborted();
|
|
1182
|
+
replacement.start(sessionId);
|
|
1183
|
+
const previous = managed.relay;
|
|
1184
|
+
managed.relay = replacement;
|
|
1185
|
+
managed.selection = selection;
|
|
1186
|
+
replacement = void 0;
|
|
1187
|
+
await previous.close();
|
|
1188
|
+
return { session };
|
|
1189
|
+
} finally {
|
|
1190
|
+
managed.configuring = false;
|
|
1191
|
+
await replacement?.close();
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/** Explicitly restore a saved local-model marker after either engine restarted. */
|
|
1195
|
+
async reconnect(backendId, sessionId, runtime, ownerId = "local") {
|
|
1196
|
+
this.assertBackend(backendId);
|
|
1197
|
+
if (!resourceId.safeParse(sessionId).success) throw new LocalProviderSessionError("Invalid conversation ID.");
|
|
1198
|
+
const key = this.key(backendId, sessionId), existing = this.sessions.get(key);
|
|
1199
|
+
if (existing) return this.configure(backendId, sessionId, runtime, ownerId);
|
|
1200
|
+
const parsed = runtimeSchema.safeParse(runtime);
|
|
1201
|
+
if (!parsed.success) throw new LocalProviderSessionError("Invalid local model selection.");
|
|
1202
|
+
if (this.reconnecting.has(key)) throw new LocalProviderSessionError("Wait for this conversation to reconnect.", 409);
|
|
1203
|
+
if (this.sessions.size + this.creating >= 32) throw new LocalProviderSessionError("Close a local model connection before reconnecting another conversation.", 429);
|
|
1204
|
+
this.reconnecting.add(key);
|
|
1205
|
+
this.creating++;
|
|
1206
|
+
let replacement;
|
|
1207
|
+
try {
|
|
1208
|
+
const response = await this.options.registry.request(backendId, `/api/console/sessions/${encodeURIComponent(sessionId)}`, { method: "GET", signal: this.signal() });
|
|
1209
|
+
if (!response.ok) {
|
|
1210
|
+
await response.body?.cancel();
|
|
1211
|
+
throw new LocalProviderSessionError("This remote conversation could not be reopened.", response.status);
|
|
1212
|
+
}
|
|
1213
|
+
const snapshot = (await response.json()).snapshot;
|
|
1214
|
+
if (snapshot?.session?.id !== sessionId || snapshot.runtime?.modelSource !== "local") throw new LocalProviderSessionError("Only an existing local-model conversation can reconnect through this route.", 409);
|
|
1215
|
+
if (snapshot.session.status !== "ready") throw new LocalProviderSessionError("Wait until this remote conversation is idle before reconnecting its model.", 409);
|
|
1216
|
+
const selection = runtimeSchema.parse({ providerId: "chatgpt-codex", ...snapshot.runtime.model ? { model: snapshot.runtime.model } : {}, ...parsed.data });
|
|
1217
|
+
replacement = await (this.options.createRelay ?? createLocalProviderRelay)({ registry: this.options.registry, backendId, selection, signal: this.signal() });
|
|
1218
|
+
const session = await this.request(backendId, `/api/console/sessions/${encodeURIComponent(sessionId)}/configuration`, "PATCH", { runtime: { ...selection, providerId: replacement.info.providerId, model: replacement.info.model }, modelSource: { kind: "local", grantId: replacement.grant.id } });
|
|
1219
|
+
if (session.id !== sessionId) throw new LocalProviderSessionError("The host returned a different conversation.", 409);
|
|
1220
|
+
this.signal().throwIfAborted();
|
|
1221
|
+
replacement.start(sessionId);
|
|
1222
|
+
this.sessions.set(key, { owner: ownerId, relay: replacement, selection, configuring: false });
|
|
1223
|
+
replacement = void 0;
|
|
1224
|
+
return { session };
|
|
1225
|
+
} finally {
|
|
1226
|
+
this.creating--;
|
|
1227
|
+
this.reconnecting.delete(key);
|
|
1228
|
+
await replacement?.close();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async status(backendId, sessionId, ownerId = "local") {
|
|
1232
|
+
this.assertBackend(backendId);
|
|
1233
|
+
const managed = this.sessions.get(this.key(backendId, sessionId));
|
|
1234
|
+
if (!managed || managed.owner !== ownerId) throw new LocalProviderSessionError("This local model connection does not belong to the current workspace.", 404);
|
|
1235
|
+
const local = managed.relay.status();
|
|
1236
|
+
try {
|
|
1237
|
+
const response = await this.options.registry.request(backendId, `/api/console/provider-relays/${encodeURIComponent(managed.relay.grant.id)}/status`, { method: "GET", signal: AbortSignal.any([this.signal(), AbortSignal.timeout(5e3)]) });
|
|
1238
|
+
if (!response.ok) {
|
|
1239
|
+
await response.body?.cancel();
|
|
1240
|
+
return { local, remote: { available: false, status: response.status } };
|
|
1241
|
+
}
|
|
1242
|
+
const body = await response.json();
|
|
1243
|
+
if (body.grantId !== managed.relay.grant.id || body.epoch !== managed.relay.grant.epoch || body.sessionId !== sessionId) throw new Error("scope");
|
|
1244
|
+
const pending = external_exports.array(external_exports.object({ delivered: external_exports.boolean(), headers: external_exports.boolean(), receivedBytes: external_exports.number().finite().nonnegative(), bufferedBytes: external_exports.number().finite().nonnegative(), ageMs: external_exports.number().finite().nonnegative() }).strip()).max(4).parse(body.pending);
|
|
1245
|
+
const cancelledCount = external_exports.number().int().nonnegative().parse(body.cancelledCount);
|
|
1246
|
+
return { local, remote: { available: true, pending, cancelledCount } };
|
|
1247
|
+
} catch {
|
|
1248
|
+
return { local, remote: { available: false } };
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async release(backendId, sessionId, ownerId = "local") {
|
|
1252
|
+
const key = this.key(backendId, sessionId), managed = this.sessions.get(key);
|
|
1253
|
+
if (!managed || managed.owner !== ownerId) return;
|
|
1254
|
+
this.sessions.delete(key);
|
|
1255
|
+
await managed.relay.close();
|
|
1256
|
+
}
|
|
1257
|
+
async dispose() {
|
|
1258
|
+
this.lifetime.abort();
|
|
1259
|
+
const entries = [...this.sessions.values()];
|
|
1260
|
+
this.sessions.clear();
|
|
1261
|
+
await Promise.allSettled(entries.map((entry) => entry.relay.close()));
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// packages/cli/src/web/remote-provider-relay.ts
|
|
1266
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1267
|
+
var ProviderRelayError = class extends Error {
|
|
1268
|
+
constructor(message, statusCode = 400) {
|
|
1269
|
+
super(message);
|
|
1270
|
+
this.statusCode = statusCode;
|
|
1271
|
+
}
|
|
1272
|
+
statusCode;
|
|
1273
|
+
};
|
|
1274
|
+
var GRANT_IDLE_MS = 30 * 6e4;
|
|
1275
|
+
var REDELIVER_UNACKNOWLEDGED_MS = 5e3;
|
|
1276
|
+
var modelId = external_exports.string().regex(/^[A-Za-z0-9._-]{1,128}$/);
|
|
1277
|
+
var grantInput = external_exports.object({ epoch: external_exports.string().min(1).max(256), provider: external_exports.literal("chatgpt-codex"), models: external_exports.array(modelId).min(1).max(16), model: modelId }).strict();
|
|
1278
|
+
var eventsInput = external_exports.object({ epoch: external_exports.string(), sessionId: external_exports.string().min(1).max(256), events: external_exports.array(external_exports.discriminatedUnion("kind", [
|
|
1279
|
+
external_exports.object({ kind: external_exports.literal("headers"), status: external_exports.number().int().min(200).max(599).refine((status) => ![204, 205, 304].includes(status)), contentType: external_exports.string().max(128).regex(/^(text\/event-stream|application\/json)(?:;[^\r\n]*)?$/i).transform((value) => value.split(";")[0].toLowerCase()) }).strict(),
|
|
1280
|
+
external_exports.object({ kind: external_exports.literal("chunk"), data: external_exports.string().max(65536).regex(/^[A-Za-z0-9+/]*={0,2}$/) }).strict(),
|
|
1281
|
+
external_exports.object({ kind: external_exports.literal("end") }).strict(),
|
|
1282
|
+
external_exports.object({ kind: external_exports.literal("error") }).strict()
|
|
1283
|
+
])).min(1).max(16) }).strict();
|
|
1284
|
+
var RemoteProviderRelayBroker = class {
|
|
1285
|
+
epoch;
|
|
1286
|
+
#grants = /* @__PURE__ */ new Map();
|
|
1287
|
+
#disposed = false;
|
|
1288
|
+
constructor(options) {
|
|
1289
|
+
this.epoch = options.epoch;
|
|
1290
|
+
}
|
|
1291
|
+
create(input) {
|
|
1292
|
+
const value = grantInput.parse(input);
|
|
1293
|
+
if (this.#disposed || value.epoch !== this.epoch) throw new ProviderRelayError("Provider relay engine epoch changed.", 409);
|
|
1294
|
+
if (!value.models.includes(value.model) || new Set(value.models).size !== value.models.length) throw new ProviderRelayError("Provider relay requires an exact model allowlist.");
|
|
1295
|
+
for (const [id2, state] of this.#grants) if (Date.parse(state.grant.expiresAt) <= Date.now()) this.close(id2);
|
|
1296
|
+
if (this.#grants.size >= 32) throw new ProviderRelayError("Provider relay grant limit reached.", 429);
|
|
1297
|
+
const grant = { id: randomUUID2(), epoch: this.epoch, provider: value.provider, models: value.models, model: value.model, expiresAt: new Date(Date.now() + GRANT_IDLE_MS).toISOString() };
|
|
1298
|
+
this.#grants.set(grant.id, { grant, requests: /* @__PURE__ */ new Map(), cancelled: [] });
|
|
1299
|
+
return structuredClone(grant);
|
|
1300
|
+
}
|
|
1301
|
+
#get(id2) {
|
|
1302
|
+
const state = this.#grants.get(id2);
|
|
1303
|
+
if (!state || this.#disposed) throw new ProviderRelayError("Provider relay grant is unavailable. Reconnect the local model source.", 410);
|
|
1304
|
+
if (Date.parse(state.grant.expiresAt) <= Date.now()) {
|
|
1305
|
+
this.close(id2);
|
|
1306
|
+
throw new ProviderRelayError("Provider relay grant expired. Reconnect the local model source.", 410);
|
|
1307
|
+
}
|
|
1308
|
+
return state;
|
|
1309
|
+
}
|
|
1310
|
+
bind(id2, sessionId) {
|
|
1311
|
+
const state = this.#get(id2);
|
|
1312
|
+
if (state.sessionId && state.sessionId !== sessionId) throw new ProviderRelayError("Provider relay grant belongs to another conversation.", 409);
|
|
1313
|
+
state.sessionId = sessionId;
|
|
1314
|
+
return structuredClone(state.grant);
|
|
1315
|
+
}
|
|
1316
|
+
transport(id2, sessionId) {
|
|
1317
|
+
const grant = this.bind(id2, sessionId);
|
|
1318
|
+
return { provider: grant.provider, models: grant.models, identity: `relay:${grant.epoch}:${grant.id}`, request: (envelope, signal) => this.request(id2, sessionId, envelope, signal) };
|
|
1319
|
+
}
|
|
1320
|
+
runtime(id2, sessionId, selection = {}) {
|
|
1321
|
+
const grant = this.bind(id2, sessionId);
|
|
1322
|
+
if (selection.providerId && selection.providerId !== grant.provider) throw new ProviderRelayError("Provider is outside the local model grant.", 403);
|
|
1323
|
+
return new LlmApiRuntime({
|
|
1324
|
+
type: "api",
|
|
1325
|
+
timeout: 12e4,
|
|
1326
|
+
model: selection.model ?? grant.model,
|
|
1327
|
+
provider: "chatgpt-codex",
|
|
1328
|
+
singleModel: selection.singleModel,
|
|
1329
|
+
agentModels: selection.agentModels,
|
|
1330
|
+
autoRoute: selection.autoRoute,
|
|
1331
|
+
providerTransport: this.transport(id2, sessionId)
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
request(id2, sessionId, envelope, signal) {
|
|
1335
|
+
const state = this.#get(id2);
|
|
1336
|
+
if (state.sessionId !== sessionId || envelope.provider !== state.grant.provider || !state.grant.models.includes(envelope.model)) throw new ProviderRelayError("Request is outside the local model grant.", 403);
|
|
1337
|
+
signal?.throwIfAborted();
|
|
1338
|
+
if (state.requests.size >= 4) throw new ProviderRelayError("Local model request limit reached.", 429);
|
|
1339
|
+
if (Buffer.byteLength(envelope.body) > 4 * 1024 * 1024) throw new ProviderRelayError("Local model request exceeds 4 MiB.", 413);
|
|
1340
|
+
const body = JSON.parse(envelope.body);
|
|
1341
|
+
if (body.model !== envelope.model || body.background === true || body.store === true || !Array.isArray(body.input)) throw new ProviderRelayError("Invalid granted Responses request.");
|
|
1342
|
+
return new Promise((resolve5, reject) => {
|
|
1343
|
+
const requestId = randomUUID2();
|
|
1344
|
+
const cancel = () => this.#finish(state, pending, new Error("Local provider request cancelled."));
|
|
1345
|
+
const pending = {
|
|
1346
|
+
...envelope,
|
|
1347
|
+
requestId,
|
|
1348
|
+
grantId: id2,
|
|
1349
|
+
epoch: this.epoch,
|
|
1350
|
+
sessionId,
|
|
1351
|
+
startedAt: Date.now(),
|
|
1352
|
+
delivered: false,
|
|
1353
|
+
received: 0,
|
|
1354
|
+
buffered: 0,
|
|
1355
|
+
ended: false,
|
|
1356
|
+
headers: false,
|
|
1357
|
+
queue: [],
|
|
1358
|
+
resolve: resolve5,
|
|
1359
|
+
reject,
|
|
1360
|
+
timer: setTimeout(() => this.#finish(state, pending, new Error("Local provider relay timed out.")), 12e4),
|
|
1361
|
+
cleanup: () => signal?.removeEventListener("abort", cancel)
|
|
1362
|
+
};
|
|
1363
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
1364
|
+
state.requests.set(requestId, pending);
|
|
1365
|
+
if (signal?.aborted) cancel();
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
status(id2) {
|
|
1369
|
+
const state = this.#get(id2);
|
|
1370
|
+
return {
|
|
1371
|
+
grantId: id2,
|
|
1372
|
+
epoch: this.epoch,
|
|
1373
|
+
sessionId: state.sessionId,
|
|
1374
|
+
expiresAt: state.grant.expiresAt,
|
|
1375
|
+
pending: [...state.requests.values()].map((request) => ({ id: request.requestId, delivered: request.delivered, headers: request.headers, receivedBytes: request.received, bufferedBytes: request.buffered, ageMs: Math.max(0, Date.now() - request.startedAt) })),
|
|
1376
|
+
cancelledCount: state.cancelled.length
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
poll(id2) {
|
|
1380
|
+
const state = this.#get(id2);
|
|
1381
|
+
state.grant.expiresAt = new Date(Date.now() + GRANT_IDLE_MS).toISOString();
|
|
1382
|
+
const requests = [...state.requests.values()];
|
|
1383
|
+
const next = requests.find((request) => !request.delivered) ?? requests.find((request) => !request.headers && Date.now() - (request.deliveredAt ?? request.startedAt) >= REDELIVER_UNACKNOWLEDGED_MS);
|
|
1384
|
+
if (next) {
|
|
1385
|
+
next.delivered = true;
|
|
1386
|
+
next.deliveredAt = Date.now();
|
|
1387
|
+
}
|
|
1388
|
+
const cancelled = state.cancelled.splice(0);
|
|
1389
|
+
return { grantId: id2, epoch: this.epoch, sessionId: state.sessionId, expiresAt: state.grant.expiresAt, requests: next ? [{ requestId: next.requestId, grantId: id2, epoch: this.epoch, sessionId: next.sessionId, provider: next.provider, model: next.model, body: next.body }] : [], cancelled };
|
|
1390
|
+
}
|
|
1391
|
+
#finish(state, request, error2) {
|
|
1392
|
+
if (request.ended) return;
|
|
1393
|
+
request.ended = true;
|
|
1394
|
+
clearTimeout(request.timer);
|
|
1395
|
+
request.cleanup();
|
|
1396
|
+
state.requests.delete(request.requestId);
|
|
1397
|
+
if (error2) {
|
|
1398
|
+
state.cancelled.push(request.requestId);
|
|
1399
|
+
state.cancelled = state.cancelled.slice(-64);
|
|
1400
|
+
if (!request.headers) request.reject(error2);
|
|
1401
|
+
request.controller?.error(error2);
|
|
1402
|
+
} else if (!request.headers) request.reject(new Error("Provider relay ended without response headers."));
|
|
1403
|
+
else if (request.queue.length === 0) request.controller?.close();
|
|
1404
|
+
}
|
|
1405
|
+
events(id2, requestId, input) {
|
|
1406
|
+
if (Buffer.byteLength(JSON.stringify(input)) > 512 * 1024) throw new ProviderRelayError("Provider event batch exceeds 512 KiB.", 413);
|
|
1407
|
+
const state = this.#get(id2);
|
|
1408
|
+
const value = eventsInput.parse(input);
|
|
1409
|
+
if (value.epoch !== this.epoch || value.sessionId !== state.sessionId) throw new ProviderRelayError("Provider relay ownership or epoch changed.", 409);
|
|
1410
|
+
const pending = state.requests.get(requestId);
|
|
1411
|
+
if (!pending) throw new ProviderRelayError("Provider request is no longer pending.", 410);
|
|
1412
|
+
for (const event of value.events) {
|
|
1413
|
+
if (pending.ended) throw new ProviderRelayError("Provider response has ended.", 409);
|
|
1414
|
+
if (event.kind === "headers") {
|
|
1415
|
+
if (pending.headers) throw new ProviderRelayError("Duplicate provider response headers.", 409);
|
|
1416
|
+
pending.headers = true;
|
|
1417
|
+
const stream = new ReadableStream({ start: (controller) => {
|
|
1418
|
+
pending.controller = controller;
|
|
1419
|
+
}, pull: (controller) => {
|
|
1420
|
+
const chunk = pending.queue.shift();
|
|
1421
|
+
if (chunk) {
|
|
1422
|
+
pending.buffered -= chunk.byteLength;
|
|
1423
|
+
controller.enqueue(chunk);
|
|
1424
|
+
} else if (pending.ended) controller.close();
|
|
1425
|
+
}, cancel: () => this.#finish(state, pending, new Error("Provider response reader cancelled.")) });
|
|
1426
|
+
pending.resolve(new Response(stream, { status: event.status, headers: { "content-type": event.contentType } }));
|
|
1427
|
+
} else if (event.kind === "chunk") {
|
|
1428
|
+
if (!pending.headers) throw new ProviderRelayError("Provider headers must precede body chunks.", 409);
|
|
1429
|
+
const bytes = Buffer.from(event.data, "base64");
|
|
1430
|
+
if (bytes.toString("base64") !== event.data || bytes.byteLength > 49152) throw new ProviderRelayError("Invalid provider body chunk.");
|
|
1431
|
+
pending.received += bytes.byteLength;
|
|
1432
|
+
if (pending.received > 16 * 1024 * 1024 || pending.buffered + bytes.byteLength > 2 * 1024 * 1024) {
|
|
1433
|
+
this.#finish(state, pending, new Error("Provider response exceeded relay limits."));
|
|
1434
|
+
throw new ProviderRelayError("Provider response exceeded relay limits.", 413);
|
|
1435
|
+
}
|
|
1436
|
+
if ((pending.controller?.desiredSize ?? 0) > 0 && pending.queue.length === 0) pending.controller.enqueue(bytes);
|
|
1437
|
+
else {
|
|
1438
|
+
pending.queue.push(bytes);
|
|
1439
|
+
pending.buffered += bytes.byteLength;
|
|
1440
|
+
}
|
|
1441
|
+
} else this.#finish(state, pending, event.kind === "error" ? new Error("Local model source disconnected or failed.") : void 0);
|
|
1442
|
+
}
|
|
1443
|
+
return { accepted: true };
|
|
1444
|
+
}
|
|
1445
|
+
close(id2) {
|
|
1446
|
+
const state = this.#grants.get(id2);
|
|
1447
|
+
if (!state) return;
|
|
1448
|
+
for (const pending of [...state.requests.values()]) this.#finish(state, pending, new Error("Local model source disconnected."));
|
|
1449
|
+
this.#grants.delete(id2);
|
|
1450
|
+
}
|
|
1451
|
+
dispose() {
|
|
1452
|
+
for (const id2 of [...this.#grants.keys()]) this.close(id2);
|
|
1453
|
+
this.#disposed = true;
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
async function handleRemoteProviderRelayRequest(req, res, url, broker, readBody) {
|
|
1457
|
+
const base = "/api/console/provider-relays";
|
|
1458
|
+
if (url.pathname !== base && !url.pathname.startsWith(`${base}/`)) return false;
|
|
1459
|
+
const send = (status, value) => {
|
|
1460
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
1461
|
+
res.end(JSON.stringify(value));
|
|
1462
|
+
};
|
|
1463
|
+
try {
|
|
1464
|
+
if (url.pathname === base && req.method === "POST") {
|
|
1465
|
+
send(201, { grant: broker.create(await readBody()) });
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
const status = /^\/api\/console\/provider-relays\/([a-f0-9-]{36})\/status$/.exec(url.pathname);
|
|
1469
|
+
if (status) {
|
|
1470
|
+
if (req.method !== "GET") throw new ProviderRelayError("Method not allowed.", 405);
|
|
1471
|
+
send(200, broker.status(status[1]));
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
const match = /^\/api\/console\/provider-relays\/([a-f0-9-]{36})(?:\/requests(?:\/([a-f0-9-]{36})\/events)?)?$/.exec(url.pathname);
|
|
1475
|
+
if (!match) throw new ProviderRelayError("Unknown provider relay route.", 404);
|
|
1476
|
+
if (req.method === "DELETE" && !url.pathname.endsWith("/requests") && !match[2]) {
|
|
1477
|
+
broker.close(match[1]);
|
|
1478
|
+
send(200, { closed: true });
|
|
1479
|
+
} else if (req.method === "GET" && url.pathname.endsWith("/requests")) send(200, broker.poll(match[1]));
|
|
1480
|
+
else if (req.method === "POST" && match[2]) send(200, broker.events(match[1], match[2], await readBody()));
|
|
1481
|
+
else throw new ProviderRelayError("Method not allowed.", 405);
|
|
1482
|
+
} catch (error2) {
|
|
1483
|
+
send(error2 instanceof ProviderRelayError ? error2.statusCode : 400, { error: error2 instanceof ProviderRelayError ? error2.message : "Invalid provider relay request." });
|
|
1484
|
+
}
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
659
1488
|
// packages/cli/src/web/report-artifacts.ts
|
|
660
1489
|
var ReportArtifactError = class extends Error {
|
|
661
|
-
constructor(
|
|
662
|
-
super(
|
|
1490
|
+
constructor(message, statusCode) {
|
|
1491
|
+
super(message);
|
|
663
1492
|
this.statusCode = statusCode;
|
|
664
1493
|
}
|
|
665
1494
|
statusCode;
|
|
@@ -711,13 +1540,13 @@ function retainedFindingSnapshot(ids, dbPath) {
|
|
|
711
1540
|
try {
|
|
712
1541
|
const findings = [];
|
|
713
1542
|
const scanIds = /* @__PURE__ */ new Set();
|
|
714
|
-
for (const
|
|
715
|
-
const row = db.getFinding(
|
|
1543
|
+
for (const id2 of new Set(ids)) {
|
|
1544
|
+
const row = db.getFinding(id2);
|
|
716
1545
|
if (!row) throw new ReportArtifactError("A selected finding was not found in this engine.", 404);
|
|
717
1546
|
scanIds.add(row.scanId);
|
|
718
|
-
findings.push(loadFindingFocus(
|
|
1547
|
+
findings.push(loadFindingFocus(id2, { dbPath }).finding);
|
|
719
1548
|
}
|
|
720
|
-
const scans = [...scanIds].map((
|
|
1549
|
+
const scans = [...scanIds].map((id2) => db.getScan(id2)).filter((scan) => scan !== void 0);
|
|
721
1550
|
return {
|
|
722
1551
|
target: scans.length === 1 ? scans[0].target : "Selected findings from multiple runs",
|
|
723
1552
|
scanDepth: "default",
|
|
@@ -763,7 +1592,7 @@ function handleFindingImpactRequest(pathname, method, input, options) {
|
|
|
763
1592
|
}
|
|
764
1593
|
|
|
765
1594
|
// packages/cli/src/web/findings-access-tokens.ts
|
|
766
|
-
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as
|
|
1595
|
+
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
767
1596
|
import { closeSync, constants, fstatSync, mkdirSync, openSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
768
1597
|
import { join } from "node:path";
|
|
769
1598
|
|
|
@@ -771,724 +1600,6 @@ import { join } from "node:path";
|
|
|
771
1600
|
import { readFileSync } from "node:fs";
|
|
772
1601
|
import { createHash, randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
|
|
773
1602
|
import { promisify } from "node:util";
|
|
774
|
-
|
|
775
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
776
|
-
var encoder = new TextEncoder();
|
|
777
|
-
var decoder = new TextDecoder();
|
|
778
|
-
var strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
779
|
-
var MAX_INT32 = 2 ** 32;
|
|
780
|
-
function concat(...buffers) {
|
|
781
|
-
const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size);
|
|
782
|
-
let i = 0;
|
|
783
|
-
for (const buffer of buffers)
|
|
784
|
-
buf.set(buffer, i), i += buffer.length;
|
|
785
|
-
return buf;
|
|
786
|
-
}
|
|
787
|
-
var NON_ASCII = /[^\x00-\x7f]/;
|
|
788
|
-
function encode(string) {
|
|
789
|
-
if (typeof string == "string" && string.length >= 128) {
|
|
790
|
-
if (NON_ASCII.test(string))
|
|
791
|
-
throw new TypeError("non-ASCII string encountered in encode()");
|
|
792
|
-
return encoder.encode(string);
|
|
793
|
-
}
|
|
794
|
-
const bytes = new Uint8Array(string.length);
|
|
795
|
-
for (let i = 0; i < string.length; i++) {
|
|
796
|
-
const code = string.charCodeAt(i);
|
|
797
|
-
if (code > 127)
|
|
798
|
-
throw new TypeError("non-ASCII string encountered in encode()");
|
|
799
|
-
bytes[i] = code;
|
|
800
|
-
}
|
|
801
|
-
return bytes;
|
|
802
|
-
}
|
|
803
|
-
function decodeBase64(encoded, url = false) {
|
|
804
|
-
if (Uint8Array.fromBase64)
|
|
805
|
-
return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" });
|
|
806
|
-
if (url) {
|
|
807
|
-
if (encoded.includes("+") || encoded.includes("/"))
|
|
808
|
-
throw new TypeError("Invalid base64url");
|
|
809
|
-
encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
810
|
-
}
|
|
811
|
-
const binary = atob(encoded), bytes = new Uint8Array(binary.length);
|
|
812
|
-
for (let i = 0; i < binary.length; i++)
|
|
813
|
-
bytes[i] = binary.charCodeAt(i);
|
|
814
|
-
return bytes;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/util/errors.js
|
|
818
|
-
var JOSEError = class extends Error {
|
|
819
|
-
static code = "ERR_JOSE_GENERIC";
|
|
820
|
-
code = "ERR_JOSE_GENERIC";
|
|
821
|
-
constructor(message2, options) {
|
|
822
|
-
super(message2, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor);
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
var JWTClaimValidationFailed = class extends JOSEError {
|
|
826
|
-
static code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
|
|
827
|
-
code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
|
|
828
|
-
claim;
|
|
829
|
-
reason;
|
|
830
|
-
payload;
|
|
831
|
-
constructor(message2, payload, claim = "unspecified", reason = "unspecified") {
|
|
832
|
-
super(message2, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload;
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
var JWTExpired = class extends JOSEError {
|
|
836
|
-
static code = "ERR_JWT_EXPIRED";
|
|
837
|
-
code = "ERR_JWT_EXPIRED";
|
|
838
|
-
claim;
|
|
839
|
-
reason;
|
|
840
|
-
payload;
|
|
841
|
-
constructor(message2, payload, claim = "unspecified", reason = "unspecified") {
|
|
842
|
-
super(message2, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload;
|
|
843
|
-
}
|
|
844
|
-
};
|
|
845
|
-
var JOSEAlgNotAllowed = class extends JOSEError {
|
|
846
|
-
static code = "ERR_JOSE_ALG_NOT_ALLOWED";
|
|
847
|
-
code = "ERR_JOSE_ALG_NOT_ALLOWED";
|
|
848
|
-
};
|
|
849
|
-
var JOSENotSupported = class extends JOSEError {
|
|
850
|
-
static code = "ERR_JOSE_NOT_SUPPORTED";
|
|
851
|
-
code = "ERR_JOSE_NOT_SUPPORTED";
|
|
852
|
-
};
|
|
853
|
-
var JWSInvalid = class extends JOSEError {
|
|
854
|
-
static code = "ERR_JWS_INVALID";
|
|
855
|
-
code = "ERR_JWS_INVALID";
|
|
856
|
-
};
|
|
857
|
-
var JWTInvalid = class extends JOSEError {
|
|
858
|
-
static code = "ERR_JWT_INVALID";
|
|
859
|
-
code = "ERR_JWT_INVALID";
|
|
860
|
-
};
|
|
861
|
-
var JWKSInvalid = class extends JOSEError {
|
|
862
|
-
static code = "ERR_JWKS_INVALID";
|
|
863
|
-
code = "ERR_JWKS_INVALID";
|
|
864
|
-
};
|
|
865
|
-
var JWKSNoMatchingKey = class extends JOSEError {
|
|
866
|
-
static code = "ERR_JWKS_NO_MATCHING_KEY";
|
|
867
|
-
code = "ERR_JWKS_NO_MATCHING_KEY";
|
|
868
|
-
constructor(message2 = "no applicable key found in the JSON Web Key Set", options) {
|
|
869
|
-
super(message2, options);
|
|
870
|
-
}
|
|
871
|
-
};
|
|
872
|
-
var JWKSMultipleMatchingKeys = class extends JOSEError {
|
|
873
|
-
[Symbol.asyncIterator] = async function* () {
|
|
874
|
-
};
|
|
875
|
-
static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
|
|
876
|
-
code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
|
|
877
|
-
constructor(message2 = "multiple matching keys found in the JSON Web Key Set", options) {
|
|
878
|
-
super(message2, options);
|
|
879
|
-
}
|
|
880
|
-
};
|
|
881
|
-
var JWKSTimeout = class extends JOSEError {
|
|
882
|
-
static code = "ERR_JWKS_TIMEOUT";
|
|
883
|
-
code = "ERR_JWKS_TIMEOUT";
|
|
884
|
-
constructor(message2 = "request timed out", options) {
|
|
885
|
-
super(message2, options);
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
var JWSSignatureVerificationFailed = class extends JOSEError {
|
|
889
|
-
static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
|
|
890
|
-
code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
|
|
891
|
-
constructor(message2 = "signature verification failed", options) {
|
|
892
|
-
super(message2, options);
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/util/base64url.js
|
|
897
|
-
var invalid = "The input to be decoded is not correctly encoded.";
|
|
898
|
-
function decode(input) {
|
|
899
|
-
try {
|
|
900
|
-
return decodeBase64(typeof input == "string" ? input : decoder.decode(input), true);
|
|
901
|
-
} catch (cause) {
|
|
902
|
-
throw new TypeError(invalid, { cause });
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/validate.js
|
|
907
|
-
function isObject(input) {
|
|
908
|
-
if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]")
|
|
909
|
-
return false;
|
|
910
|
-
const prototype = Object.getPrototypeOf(input);
|
|
911
|
-
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
912
|
-
}
|
|
913
|
-
function isJwkSet(input) {
|
|
914
|
-
return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject);
|
|
915
|
-
}
|
|
916
|
-
function isDisjoint(...headers) {
|
|
917
|
-
const parameters = /* @__PURE__ */ new Set();
|
|
918
|
-
for (const header of headers)
|
|
919
|
-
if (header)
|
|
920
|
-
for (const parameter of Object.keys(header)) {
|
|
921
|
-
if (parameters.has(parameter))
|
|
922
|
-
return false;
|
|
923
|
-
parameters.add(parameter);
|
|
924
|
-
}
|
|
925
|
-
return true;
|
|
926
|
-
}
|
|
927
|
-
function decodeBase64url(value, label, ErrorClass) {
|
|
928
|
-
try {
|
|
929
|
-
return decode(value);
|
|
930
|
-
} catch {
|
|
931
|
-
throw new ErrorClass(`Failed to base64url decode the ${label}`);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
function encodeBase64url(value, label, ErrorClass) {
|
|
935
|
-
try {
|
|
936
|
-
return encode(value);
|
|
937
|
-
} catch {
|
|
938
|
-
throw new ErrorClass(`The ${label} is not a valid base64url string`);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
function parseJoseHeader(b64, ErrorClass, message2) {
|
|
942
|
-
let parsed;
|
|
943
|
-
try {
|
|
944
|
-
parsed = JSON.parse(strictDecoder.decode(decode(b64)));
|
|
945
|
-
} catch {
|
|
946
|
-
throw new ErrorClass(message2);
|
|
947
|
-
}
|
|
948
|
-
if (!isObject(parsed))
|
|
949
|
-
throw new ErrorClass(message2);
|
|
950
|
-
return parsed;
|
|
951
|
-
}
|
|
952
|
-
var JWS_RECOGNIZED = { __proto__: null, b64: true };
|
|
953
|
-
function validateAlgorithms(option, algorithms) {
|
|
954
|
-
if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string")))
|
|
955
|
-
throw new TypeError(`"${option}" option must be an array of strings`);
|
|
956
|
-
return algorithms === void 0 ? void 0 : new Set(algorithms);
|
|
957
|
-
}
|
|
958
|
-
function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
|
|
959
|
-
if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0)
|
|
960
|
-
throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
|
|
961
|
-
if (!protectedHeader || protectedHeader.crit === void 0)
|
|
962
|
-
return [];
|
|
963
|
-
if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0))
|
|
964
|
-
throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
|
|
965
|
-
const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault };
|
|
966
|
-
for (const parameter of protectedHeader.crit) {
|
|
967
|
-
if (!(parameter in recognized))
|
|
968
|
-
throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
|
|
969
|
-
if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0)
|
|
970
|
-
throw new Err(`Extension Header Parameter "${parameter}" is missing`);
|
|
971
|
-
if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0))
|
|
972
|
-
throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
|
|
973
|
-
}
|
|
974
|
-
return protectedHeader.crit;
|
|
975
|
-
}
|
|
976
|
-
function validateB64(protectedHeader, extensions) {
|
|
977
|
-
if (extensions.includes("b64")) {
|
|
978
|
-
const b64 = protectedHeader.b64;
|
|
979
|
-
if (typeof b64 != "boolean")
|
|
980
|
-
throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
|
|
981
|
-
return b64;
|
|
982
|
-
}
|
|
983
|
-
return true;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/key.js
|
|
987
|
-
var tag = (key) => key[Symbol.toStringTag];
|
|
988
|
-
var jwkMatchesOp = (entry, key, usage) => {
|
|
989
|
-
const { alg } = entry;
|
|
990
|
-
if (key.use !== void 0) {
|
|
991
|
-
const expected = usage === "sign" || usage === "verify" ? "sig" : "enc";
|
|
992
|
-
if (key.use !== expected)
|
|
993
|
-
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
|
|
994
|
-
}
|
|
995
|
-
if (key.alg !== void 0 && key.alg !== alg)
|
|
996
|
-
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
|
|
997
|
-
if (Array.isArray(key.key_ops)) {
|
|
998
|
-
const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage;
|
|
999
|
-
if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp))
|
|
1000
|
-
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
|
|
1001
|
-
}
|
|
1002
|
-
};
|
|
1003
|
-
async function prepareKey(entry, key, usage) {
|
|
1004
|
-
const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign";
|
|
1005
|
-
if (secret && key instanceof Uint8Array)
|
|
1006
|
-
return key;
|
|
1007
|
-
let normalized, keyObject;
|
|
1008
|
-
if (isObject(key)) {
|
|
1009
|
-
if (normalized = normalizeJwk(key), typeof normalized.kty != "string")
|
|
1010
|
-
throw invalidKeyType(alg, key, secret);
|
|
1011
|
-
if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0)))
|
|
1012
|
-
throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`);
|
|
1013
|
-
if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct")
|
|
1014
|
-
return decode(normalized.k);
|
|
1015
|
-
if (!Object.isFrozen(key)) {
|
|
1016
|
-
const { key_ops } = key;
|
|
1017
|
-
Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key);
|
|
1018
|
-
}
|
|
1019
|
-
} else {
|
|
1020
|
-
if (!isKeyLike(key))
|
|
1021
|
-
throw invalidKeyType(alg, key, secret);
|
|
1022
|
-
const expectedType = secret ? "secret" : privateKey ? "private" : "public";
|
|
1023
|
-
if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type)))
|
|
1024
|
-
throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`);
|
|
1025
|
-
if (isCryptoKey(key))
|
|
1026
|
-
return key;
|
|
1027
|
-
if (keyObject = key, keyObject.type === "secret")
|
|
1028
|
-
return keyObject.export();
|
|
1029
|
-
}
|
|
1030
|
-
cache ||= /* @__PURE__ */ new WeakMap();
|
|
1031
|
-
const cacheKey = key;
|
|
1032
|
-
let cached = cache.get(cacheKey);
|
|
1033
|
-
if (cached?.[alg])
|
|
1034
|
-
return cached[alg];
|
|
1035
|
-
if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") {
|
|
1036
|
-
const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle;
|
|
1037
|
-
return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]);
|
|
1038
|
-
}
|
|
1039
|
-
return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized);
|
|
1040
|
-
}
|
|
1041
|
-
var cache;
|
|
1042
|
-
var nist = {
|
|
1043
|
-
__proto__: null,
|
|
1044
|
-
prime256v1: "P-256",
|
|
1045
|
-
secp384r1: "P-384",
|
|
1046
|
-
secp521r1: "P-521"
|
|
1047
|
-
};
|
|
1048
|
-
var isCryptoKey = (key) => {
|
|
1049
|
-
if (key?.[Symbol.toStringTag] === "CryptoKey")
|
|
1050
|
-
return true;
|
|
1051
|
-
try {
|
|
1052
|
-
return key instanceof CryptoKey;
|
|
1053
|
-
} catch {
|
|
1054
|
-
return false;
|
|
1055
|
-
}
|
|
1056
|
-
};
|
|
1057
|
-
var isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject";
|
|
1058
|
-
var isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
|
|
1059
|
-
function message(msg, actual, ...types) {
|
|
1060
|
-
if (types.length > 2) {
|
|
1061
|
-
const last = types.pop();
|
|
1062
|
-
msg += `one of type ${types.join(", ")}, or ${last}.`;
|
|
1063
|
-
} else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`;
|
|
1064
|
-
return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg;
|
|
1065
|
-
}
|
|
1066
|
-
function invalidKeyType(alg, actual, secret) {
|
|
1067
|
-
const types = ["CryptoKey", "KeyObject", "JSON Web Key"];
|
|
1068
|
-
return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types));
|
|
1069
|
-
}
|
|
1070
|
-
var unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
1071
|
-
function checkUsage(key, usage) {
|
|
1072
|
-
if (usage && !key.usages.includes(usage))
|
|
1073
|
-
throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
1074
|
-
}
|
|
1075
|
-
function checkModulusLength(alg, key) {
|
|
1076
|
-
const { modulusLength } = key.algorithm;
|
|
1077
|
-
if (typeof modulusLength != "number" || modulusLength < 2048)
|
|
1078
|
-
throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
|
1079
|
-
}
|
|
1080
|
-
function checkCryptoKey(key, expected, usage) {
|
|
1081
|
-
const algorithm = key.algorithm;
|
|
1082
|
-
if (algorithm.name !== expected.name)
|
|
1083
|
-
throw unusable(expected.name);
|
|
1084
|
-
if (expected.hash && algorithm.hash?.name !== expected.hash)
|
|
1085
|
-
throw unusable(expected.hash, "algorithm.hash");
|
|
1086
|
-
if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve)
|
|
1087
|
-
throw unusable(expected.namedCurve, "algorithm.namedCurve");
|
|
1088
|
-
if (expected.length !== void 0 && algorithm.length !== expected.length)
|
|
1089
|
-
throw unusable(expected.length, "algorithm.length");
|
|
1090
|
-
checkUsage(key, usage);
|
|
1091
|
-
}
|
|
1092
|
-
function snapshotJwk(jwk) {
|
|
1093
|
-
return { __proto__: null, ...jwk };
|
|
1094
|
-
}
|
|
1095
|
-
function normalizeJwk(jwk) {
|
|
1096
|
-
const normalized = snapshotJwk(jwk);
|
|
1097
|
-
if (normalized.ext !== void 0 && typeof normalized.ext != "boolean")
|
|
1098
|
-
throw new TypeError('"ext" (Extractable) Parameter must be a boolean');
|
|
1099
|
-
if (normalized.key_ops !== void 0) {
|
|
1100
|
-
const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0;
|
|
1101
|
-
if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length)
|
|
1102
|
-
throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings');
|
|
1103
|
-
normalized.key_ops = keyOps;
|
|
1104
|
-
}
|
|
1105
|
-
return normalized;
|
|
1106
|
-
}
|
|
1107
|
-
async function jwkToKey(entry, jwk, extractable) {
|
|
1108
|
-
if (!entry.kty.includes(jwk.kty))
|
|
1109
|
-
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
|
|
1110
|
-
const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext };
|
|
1111
|
-
return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]);
|
|
1112
|
-
}
|
|
1113
|
-
async function rawKey(key, expected, usage, extractable = false) {
|
|
1114
|
-
return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/key_descriptor.js
|
|
1118
|
-
function table(entries) {
|
|
1119
|
-
const out = { __proto__: null };
|
|
1120
|
-
for (const alg in entries)
|
|
1121
|
-
out[alg] = { ...entries[alg], alg };
|
|
1122
|
-
return out;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/jws_algorithms.js
|
|
1126
|
-
var sig = [["verify"], ["sign"]];
|
|
1127
|
-
function hmac(bits) {
|
|
1128
|
-
const subtle = { name: "HMAC", hash: `SHA-${bits}` };
|
|
1129
|
-
return { kty: ["oct"], secret: true, subtle, signing: subtle, usages: sig };
|
|
1130
|
-
}
|
|
1131
|
-
function rsa(bits, saltLength) {
|
|
1132
|
-
const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` };
|
|
1133
|
-
return {
|
|
1134
|
-
kty: ["RSA"],
|
|
1135
|
-
subtle,
|
|
1136
|
-
signing: saltLength ? { ...subtle, saltLength } : subtle,
|
|
1137
|
-
usages: sig,
|
|
1138
|
-
minRsaBits: 2048
|
|
1139
|
-
};
|
|
1140
|
-
}
|
|
1141
|
-
function ecdsa(crv, bits) {
|
|
1142
|
-
return {
|
|
1143
|
-
kty: ["EC"],
|
|
1144
|
-
crv,
|
|
1145
|
-
subtle: { name: "ECDSA", namedCurve: crv },
|
|
1146
|
-
signing: { name: "ECDSA", hash: `SHA-${bits}` },
|
|
1147
|
-
usages: sig
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
function eddsa() {
|
|
1151
|
-
const subtle = { name: "Ed25519" };
|
|
1152
|
-
return {
|
|
1153
|
-
kty: ["OKP"],
|
|
1154
|
-
crv: "Ed25519",
|
|
1155
|
-
subtle,
|
|
1156
|
-
signing: subtle,
|
|
1157
|
-
usages: sig
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
function mldsa(bits) {
|
|
1161
|
-
const subtle = { name: `ML-DSA-${bits}` };
|
|
1162
|
-
return {
|
|
1163
|
-
kty: ["AKP"],
|
|
1164
|
-
subtle,
|
|
1165
|
-
signing: subtle,
|
|
1166
|
-
usages: sig
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
var JWS = table({
|
|
1170
|
-
HS256: hmac(256),
|
|
1171
|
-
HS384: hmac(384),
|
|
1172
|
-
HS512: hmac(512),
|
|
1173
|
-
RS256: rsa(256),
|
|
1174
|
-
RS384: rsa(384),
|
|
1175
|
-
RS512: rsa(512),
|
|
1176
|
-
PS256: rsa(256, 32),
|
|
1177
|
-
PS384: rsa(384, 48),
|
|
1178
|
-
PS512: rsa(512, 64),
|
|
1179
|
-
ES256: ecdsa("P-256", 256),
|
|
1180
|
-
ES384: ecdsa("P-384", 384),
|
|
1181
|
-
ES512: ecdsa("P-521", 512),
|
|
1182
|
-
EdDSA: eddsa(),
|
|
1183
|
-
Ed25519: eddsa(),
|
|
1184
|
-
"ML-DSA-44": mldsa(44),
|
|
1185
|
-
"ML-DSA-65": mldsa(65),
|
|
1186
|
-
"ML-DSA-87": mldsa(87)
|
|
1187
|
-
});
|
|
1188
|
-
function jwsAlgorithm(alg) {
|
|
1189
|
-
const entry = typeof alg == "string" ? JWS[alg] : void 0;
|
|
1190
|
-
if (!entry)
|
|
1191
|
-
throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
|
1192
|
-
return entry;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/jws_verify.js
|
|
1196
|
-
function prepareVerify(options) {
|
|
1197
|
-
return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit];
|
|
1198
|
-
}
|
|
1199
|
-
function parseProtectedHeader(encodedProtected) {
|
|
1200
|
-
return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid");
|
|
1201
|
-
}
|
|
1202
|
-
function encodeCompactUnencodedPayload(payload) {
|
|
1203
|
-
try {
|
|
1204
|
-
return encode(payload);
|
|
1205
|
-
} catch {
|
|
1206
|
-
throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters");
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) {
|
|
1210
|
-
const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected);
|
|
1211
|
-
if (!isDisjoint(parsedProt, header))
|
|
1212
|
-
throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
|
|
1213
|
-
const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader;
|
|
1214
|
-
if (typeof alg != "string" || !alg)
|
|
1215
|
-
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
|
|
1216
|
-
if (shared[0] && !shared[0].has(alg))
|
|
1217
|
-
throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed');
|
|
1218
|
-
if (b64) {
|
|
1219
|
-
if (typeof inputPayload != "string")
|
|
1220
|
-
throw new JWSInvalid("JWS Payload must be a string");
|
|
1221
|
-
} else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array))
|
|
1222
|
-
throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance");
|
|
1223
|
-
const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload);
|
|
1224
|
-
let resolvedKey = false;
|
|
1225
|
-
typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = true);
|
|
1226
|
-
const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify");
|
|
1227
|
-
entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey);
|
|
1228
|
-
let verified = false;
|
|
1229
|
-
try {
|
|
1230
|
-
verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data);
|
|
1231
|
-
} catch {
|
|
1232
|
-
}
|
|
1233
|
-
if (!verified)
|
|
1234
|
-
throw new JWSSignatureVerificationFailed();
|
|
1235
|
-
const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload };
|
|
1236
|
-
return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64];
|
|
1237
|
-
}
|
|
1238
|
-
async function verifyCompact(jws, shared, key) {
|
|
1239
|
-
if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string")
|
|
1240
|
-
throw new JWSInvalid("Compact JWS must be a string or Uint8Array");
|
|
1241
|
-
const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split(".");
|
|
1242
|
-
if (length !== 3)
|
|
1243
|
-
throw new JWSInvalid("Invalid Compact JWS");
|
|
1244
|
-
return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload);
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/jwt_claims_set.js
|
|
1248
|
-
var epoch = (date) => Math.floor(date.getTime() / 1e3);
|
|
1249
|
-
var multipliers = {
|
|
1250
|
-
s: 1,
|
|
1251
|
-
m: 60,
|
|
1252
|
-
h: 3600,
|
|
1253
|
-
d: 86400,
|
|
1254
|
-
w: 604800,
|
|
1255
|
-
y: 31557600
|
|
1256
|
-
};
|
|
1257
|
-
var REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i;
|
|
1258
|
-
var checkFailed = "check_failed";
|
|
1259
|
-
function invalidDuration() {
|
|
1260
|
-
throw new TypeError("Invalid time period format");
|
|
1261
|
-
}
|
|
1262
|
-
function secs(str) {
|
|
1263
|
-
typeof str != "string" && invalidDuration();
|
|
1264
|
-
const matched = REGEX.exec(str);
|
|
1265
|
-
(!matched || matched[4] && matched[1]) && invalidDuration();
|
|
1266
|
-
const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
|
|
1267
|
-
return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2;
|
|
1268
|
-
}
|
|
1269
|
-
function validateInput(label, input) {
|
|
1270
|
-
if (!Number.isFinite(input))
|
|
1271
|
-
throw new TypeError(`Invalid ${label} input`);
|
|
1272
|
-
return input;
|
|
1273
|
-
}
|
|
1274
|
-
var normalizeTyp = (value) => {
|
|
1275
|
-
const normalized = value.toLowerCase();
|
|
1276
|
-
return value.includes("/") ? normalized : `application/${normalized}`;
|
|
1277
|
-
};
|
|
1278
|
-
var checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : false;
|
|
1279
|
-
function validateNumericDate(payload, claim, required = false) {
|
|
1280
|
-
const value = payload[claim];
|
|
1281
|
-
if (!(value === void 0 && !required)) {
|
|
1282
|
-
if (typeof value != "number")
|
|
1283
|
-
throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid");
|
|
1284
|
-
return value;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
function unexpectedClaim(payload, claim) {
|
|
1288
|
-
throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed);
|
|
1289
|
-
}
|
|
1290
|
-
function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
|
|
1291
|
-
let payload;
|
|
1292
|
-
try {
|
|
1293
|
-
payload = JSON.parse(strictDecoder.decode(encodedPayload));
|
|
1294
|
-
} catch {
|
|
1295
|
-
}
|
|
1296
|
-
if (!isObject(payload))
|
|
1297
|
-
throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
|
|
1298
|
-
const { typ } = options;
|
|
1299
|
-
if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ)))
|
|
1300
|
-
throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed);
|
|
1301
|
-
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims];
|
|
1302
|
-
maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss");
|
|
1303
|
-
for (const claim of new Set(presenceCheck.reverse()))
|
|
1304
|
-
if (!Object.hasOwn(payload, claim))
|
|
1305
|
-
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing");
|
|
1306
|
-
issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud");
|
|
1307
|
-
const { clockTolerance } = options;
|
|
1308
|
-
let tolerance = 0;
|
|
1309
|
-
if (typeof clockTolerance == "string")
|
|
1310
|
-
tolerance = secs(clockTolerance);
|
|
1311
|
-
else if (clockTolerance !== void 0) {
|
|
1312
|
-
if (typeof clockTolerance != "number")
|
|
1313
|
-
throw new TypeError("Invalid clockTolerance option type");
|
|
1314
|
-
tolerance = clockTolerance;
|
|
1315
|
-
}
|
|
1316
|
-
validateInput("clockTolerance option", tolerance);
|
|
1317
|
-
const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf");
|
|
1318
|
-
if (nbf !== void 0 && nbf > now + tolerance)
|
|
1319
|
-
throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed);
|
|
1320
|
-
const exp = validateNumericDate(payload, "exp");
|
|
1321
|
-
if (exp !== void 0 && exp <= now - tolerance)
|
|
1322
|
-
throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed);
|
|
1323
|
-
if (maxTokenAge !== void 0) {
|
|
1324
|
-
const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge));
|
|
1325
|
-
if (age - tolerance > max)
|
|
1326
|
-
throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed);
|
|
1327
|
-
if (age < -tolerance)
|
|
1328
|
-
throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed);
|
|
1329
|
-
}
|
|
1330
|
-
return payload;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/jwt/verify.js
|
|
1334
|
-
async function jwtVerify(jwt, key, options) {
|
|
1335
|
-
const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key);
|
|
1336
|
-
if (!b64)
|
|
1337
|
-
throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
|
|
1338
|
-
const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options);
|
|
1339
|
-
return { ...verified, payload };
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/jwks/local.js
|
|
1343
|
-
function isUsableJWK(jwk, entry, alg, kid) {
|
|
1344
|
-
const { kty, key_ops: keyOps, ext, kid: jwkKid, alg: jwkAlg, use, crv } = jwk;
|
|
1345
|
-
return (ext === void 0 || typeof ext == "boolean") && (keyOps === void 0 || Array.isArray(keyOps) && keyOps.every((operation, index) => typeof operation == "string" && keyOps.indexOf(operation) === index) && keyOps.includes("verify")) && entry.kty.includes(kty) && (kid === void 0 || typeof kid == "string" && kid === jwkKid) && (jwkAlg === void 0 ? kty !== "AKP" : alg === jwkAlg) && (use === void 0 || use === "sig") && (!entry.crv || crv === entry.crv);
|
|
1346
|
-
}
|
|
1347
|
-
async function importWithAlgCache(cache2, jwk, entry) {
|
|
1348
|
-
const cached = cache2.get(jwk) || cache2.set(jwk, {}).get(jwk), { alg } = entry;
|
|
1349
|
-
if (cached[alg] === void 0) {
|
|
1350
|
-
const pending = jwkToKey(entry, jwk, true).then((key) => {
|
|
1351
|
-
if (key.type !== "public")
|
|
1352
|
-
throw new JWKSInvalid("JSON Web Key Set members must be public keys");
|
|
1353
|
-
return cached[alg] = key, key;
|
|
1354
|
-
}).catch((error2) => {
|
|
1355
|
-
throw cached[alg] === pending && delete cached[alg], error2;
|
|
1356
|
-
});
|
|
1357
|
-
cached[alg] = pending;
|
|
1358
|
-
}
|
|
1359
|
-
return cached[alg];
|
|
1360
|
-
}
|
|
1361
|
-
function createLocalJWKSet(jwks) {
|
|
1362
|
-
let snapshot;
|
|
1363
|
-
try {
|
|
1364
|
-
snapshot = structuredClone(jwks);
|
|
1365
|
-
} catch {
|
|
1366
|
-
}
|
|
1367
|
-
if (!isJwkSet(snapshot))
|
|
1368
|
-
throw new JWKSInvalid("JSON Web Key Set malformed");
|
|
1369
|
-
const metadata = snapshot.keys.map((jwk) => {
|
|
1370
|
-
const normalized = snapshotJwk(jwk);
|
|
1371
|
-
return Array.isArray(normalized.key_ops) && (normalized.key_ops = [...normalized.key_ops]), normalized;
|
|
1372
|
-
}), cached = /* @__PURE__ */ new WeakMap();
|
|
1373
|
-
return Object.defineProperty(async (protectedHeader, token) => {
|
|
1374
|
-
const { alg, kid } = { ...protectedHeader, ...token?.header }, entry = typeof alg == "string" ? JWS[alg] : void 0;
|
|
1375
|
-
if (!entry || entry.secret)
|
|
1376
|
-
throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set');
|
|
1377
|
-
const candidates = snapshot.keys.filter((_, index) => isUsableJWK(metadata[index], entry, alg, kid)), { 0: jwk, length } = candidates;
|
|
1378
|
-
if (!length)
|
|
1379
|
-
throw new JWKSNoMatchingKey();
|
|
1380
|
-
if (length !== 1) {
|
|
1381
|
-
const error2 = new JWKSMultipleMatchingKeys();
|
|
1382
|
-
throw error2[Symbol.asyncIterator] = async function* () {
|
|
1383
|
-
for (const jwk2 of candidates)
|
|
1384
|
-
try {
|
|
1385
|
-
yield await importWithAlgCache(cached, jwk2, entry);
|
|
1386
|
-
} catch {
|
|
1387
|
-
}
|
|
1388
|
-
}, error2;
|
|
1389
|
-
}
|
|
1390
|
-
return importWithAlgCache(cached, jwk, entry);
|
|
1391
|
-
}, "jwks", {
|
|
1392
|
-
value: () => structuredClone(snapshot)
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/jwks/remote.js
|
|
1397
|
-
function isCloudflareWorkers() {
|
|
1398
|
-
return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel";
|
|
1399
|
-
}
|
|
1400
|
-
var USER_AGENT;
|
|
1401
|
-
(typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12");
|
|
1402
|
-
var customFetch = /* @__PURE__ */ Symbol();
|
|
1403
|
-
async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
|
|
1404
|
-
const response = await fetchImpl(url, {
|
|
1405
|
-
method: "GET",
|
|
1406
|
-
signal,
|
|
1407
|
-
redirect: "manual",
|
|
1408
|
-
headers
|
|
1409
|
-
}).catch((err) => {
|
|
1410
|
-
throw err.name === "TimeoutError" ? new JWKSTimeout() : err;
|
|
1411
|
-
});
|
|
1412
|
-
if (response.status !== 200)
|
|
1413
|
-
throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response");
|
|
1414
|
-
try {
|
|
1415
|
-
return await response.json();
|
|
1416
|
-
} catch {
|
|
1417
|
-
throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON");
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
var jwksCache = /* @__PURE__ */ Symbol();
|
|
1421
|
-
function isFreshFor(timestamp, duration) {
|
|
1422
|
-
return Number.isFinite(timestamp) && Date.now() < timestamp + duration;
|
|
1423
|
-
}
|
|
1424
|
-
function validateDuration(value, fallback, option) {
|
|
1425
|
-
if (Number.isNaN(value))
|
|
1426
|
-
throw new TypeError(`"${option}" option must not be NaN`);
|
|
1427
|
-
return typeof value == "number" ? value : fallback;
|
|
1428
|
-
}
|
|
1429
|
-
function createRemoteJWKSet(url, options) {
|
|
1430
|
-
if (!(url instanceof URL))
|
|
1431
|
-
throw new TypeError("url must be an instance of URL");
|
|
1432
|
-
const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration;
|
|
1433
|
-
if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0))
|
|
1434
|
-
throw new TypeError('"timeoutDuration" option must be a non-negative integer');
|
|
1435
|
-
const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers);
|
|
1436
|
-
USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json");
|
|
1437
|
-
const fetchImpl = opts[customFetch], cache2 = opts[jwksCache];
|
|
1438
|
-
let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local;
|
|
1439
|
-
if (cache2 && typeof cache2 == "object") {
|
|
1440
|
-
const { uat, jwks } = cache2;
|
|
1441
|
-
isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks));
|
|
1442
|
-
}
|
|
1443
|
-
const reload = async () => {
|
|
1444
|
-
if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) {
|
|
1445
|
-
const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json2) => {
|
|
1446
|
-
const next = createLocalJWKSet(json2);
|
|
1447
|
-
if (sequence <= appliedSequence)
|
|
1448
|
-
return;
|
|
1449
|
-
local = next;
|
|
1450
|
-
const updatedAt = Date.now();
|
|
1451
|
-
cache2 && (cache2.uat = updatedAt, cache2.jwks = json2), jwksTimestamp = updatedAt, appliedSequence = sequence;
|
|
1452
|
-
}).finally(() => {
|
|
1453
|
-
pendingFetch === current && (pendingFetch = void 0);
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
await pendingFetch;
|
|
1457
|
-
};
|
|
1458
|
-
return Object.defineProperties(async (protectedHeader, token) => {
|
|
1459
|
-
(!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload();
|
|
1460
|
-
try {
|
|
1461
|
-
return await local(protectedHeader, token);
|
|
1462
|
-
} catch (err) {
|
|
1463
|
-
if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration))
|
|
1464
|
-
return await reload(), local(protectedHeader, token);
|
|
1465
|
-
throw err;
|
|
1466
|
-
}
|
|
1467
|
-
}, {
|
|
1468
|
-
coolingDown: {
|
|
1469
|
-
get: () => isFreshFor(jwksTimestamp, cooldownDuration),
|
|
1470
|
-
enumerable: true
|
|
1471
|
-
},
|
|
1472
|
-
fresh: {
|
|
1473
|
-
get: () => isFreshFor(jwksTimestamp, cacheMaxAge),
|
|
1474
|
-
enumerable: true
|
|
1475
|
-
},
|
|
1476
|
-
reload: {
|
|
1477
|
-
value: reload,
|
|
1478
|
-
enumerable: true
|
|
1479
|
-
},
|
|
1480
|
-
reloading: {
|
|
1481
|
-
get: () => !!pendingFetch,
|
|
1482
|
-
enumerable: true
|
|
1483
|
-
},
|
|
1484
|
-
jwks: {
|
|
1485
|
-
value: () => local?.jwks(),
|
|
1486
|
-
enumerable: true
|
|
1487
|
-
}
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// packages/cli/src/web/team-auth.ts
|
|
1492
1603
|
var scrypt = promisify(scryptCallback);
|
|
1493
1604
|
var COOKIE = "zero_team_session";
|
|
1494
1605
|
var SSO_COOKIE = "zero_team_sso";
|
|
@@ -1510,8 +1621,8 @@ function parseTeamConfig(input) {
|
|
|
1510
1621
|
return configSchema.parse(input);
|
|
1511
1622
|
}
|
|
1512
1623
|
var TeamAuthError = class extends Error {
|
|
1513
|
-
constructor(
|
|
1514
|
-
super(
|
|
1624
|
+
constructor(message, statusCode = 401) {
|
|
1625
|
+
super(message);
|
|
1515
1626
|
this.statusCode = statusCode;
|
|
1516
1627
|
}
|
|
1517
1628
|
statusCode;
|
|
@@ -1534,8 +1645,8 @@ function parseCookies(header) {
|
|
|
1534
1645
|
return cookies;
|
|
1535
1646
|
}
|
|
1536
1647
|
function getSessionId(req) {
|
|
1537
|
-
const
|
|
1538
|
-
return
|
|
1648
|
+
const id2 = parseCookies(req.headers.cookie)[COOKIE];
|
|
1649
|
+
return id2 && /^[\w-]{43}$/.test(id2) ? id2 : void 0;
|
|
1539
1650
|
}
|
|
1540
1651
|
async function hashTeamPassword(password) {
|
|
1541
1652
|
if (!password || password.length > 1024) throw new TeamAuthError("Password must contain 1\u20131024 characters", 400);
|
|
@@ -1592,8 +1703,8 @@ var TeamAuth = class {
|
|
|
1592
1703
|
cookie(name, value, seconds) {
|
|
1593
1704
|
return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${seconds}${this.origin.startsWith("https:") ? "; Secure" : ""}`;
|
|
1594
1705
|
}
|
|
1595
|
-
lookupUser(
|
|
1596
|
-
const user = this.config?.users.find((row) => row.id ===
|
|
1706
|
+
lookupUser(id2) {
|
|
1707
|
+
const user = this.config?.users.find((row) => row.id === id2);
|
|
1597
1708
|
return user ? { workspaceId: this.config.workspace.id, userId: user.id, displayName: user.name, role: user.role } : null;
|
|
1598
1709
|
}
|
|
1599
1710
|
/** Trusted server configuration only; never serialize this through a browser API. */
|
|
@@ -1617,11 +1728,11 @@ var TeamAuth = class {
|
|
|
1617
1728
|
this.config.users.push(member);
|
|
1618
1729
|
return this.lookupUser(member.id);
|
|
1619
1730
|
}
|
|
1620
|
-
resolveSessionId(
|
|
1621
|
-
if (!
|
|
1622
|
-
const session = this.sessions.get(
|
|
1731
|
+
resolveSessionId(id2) {
|
|
1732
|
+
if (!id2) return null;
|
|
1733
|
+
const session = this.sessions.get(id2);
|
|
1623
1734
|
if (!session || session.expires <= this.now()) {
|
|
1624
|
-
this.sessions.delete(
|
|
1735
|
+
this.sessions.delete(id2);
|
|
1625
1736
|
return null;
|
|
1626
1737
|
}
|
|
1627
1738
|
return this.lookupUser(session.userId);
|
|
@@ -1633,7 +1744,7 @@ var TeamAuth = class {
|
|
|
1633
1744
|
return { enabled: this.enabled, ...this.config ? { workspace: this.config.workspace } : {}, user: this.resolveSession(req), sso: !!this.config?.oidc };
|
|
1634
1745
|
}
|
|
1635
1746
|
issueSession(userId) {
|
|
1636
|
-
for (const [
|
|
1747
|
+
for (const [id2, session] of this.sessions) if (session.expires <= this.now()) this.sessions.delete(id2);
|
|
1637
1748
|
if (this.sessions.size >= 1e4) throw new TeamAuthError("Session capacity reached", 503);
|
|
1638
1749
|
const sessionId = opaque();
|
|
1639
1750
|
this.sessions.set(sessionId, { userId, expires: this.now() + SESSION_MS });
|
|
@@ -1644,7 +1755,7 @@ var TeamAuth = class {
|
|
|
1644
1755
|
const parsed = external_exports.object({ userId: external_exports.string().min(1).max(160), password: external_exports.string().min(1).max(1024) }).strict().safeParse(input);
|
|
1645
1756
|
if (!parsed.success) throw new TeamAuthError("User ID and password are required", 400);
|
|
1646
1757
|
const key = rateKey;
|
|
1647
|
-
for (const [
|
|
1758
|
+
for (const [id2, row] of this.attempts) if (row.expires <= this.now()) this.attempts.delete(id2);
|
|
1648
1759
|
const attempt = this.attempts.get(key) ?? { count: 0, expires: this.now() + 6e4 };
|
|
1649
1760
|
if (attempt.count >= 10 || this.attempts.size >= 1e4) throw new TeamAuthError("Too many sign-in attempts; try again shortly", 429);
|
|
1650
1761
|
attempt.count++;
|
|
@@ -1655,8 +1766,8 @@ var TeamAuth = class {
|
|
|
1655
1766
|
return this.issueSession(user.id);
|
|
1656
1767
|
}
|
|
1657
1768
|
logout(req) {
|
|
1658
|
-
const
|
|
1659
|
-
if (
|
|
1769
|
+
const id2 = getSessionId(req);
|
|
1770
|
+
if (id2) this.sessions.delete(id2);
|
|
1660
1771
|
return { setCookie: this.cookie(COOKIE, "", 0) };
|
|
1661
1772
|
}
|
|
1662
1773
|
requireProviderURL(value) {
|
|
@@ -1680,7 +1791,7 @@ var TeamAuth = class {
|
|
|
1680
1791
|
async beginSSO() {
|
|
1681
1792
|
const discovery = await this.getDiscovery();
|
|
1682
1793
|
const oidc = this.config.oidc;
|
|
1683
|
-
for (const [
|
|
1794
|
+
for (const [id2, pending] of this.pending) if (pending.expires <= this.now()) this.pending.delete(id2);
|
|
1684
1795
|
if (this.pending.size >= 1e3) throw new TeamAuthError("Too many pending sign-ins", 429);
|
|
1685
1796
|
const state = opaque(), nonce = opaque(), verifier = opaque(), browser = opaque();
|
|
1686
1797
|
this.pending.set(state, { nonce, verifier, browser, expires: this.now() + SSO_MS });
|
|
@@ -1719,8 +1830,8 @@ var TeamAuth = class {
|
|
|
1719
1830
|
|
|
1720
1831
|
// packages/cli/src/web/findings-access-tokens.ts
|
|
1721
1832
|
var FindingsAccessError = class extends Error {
|
|
1722
|
-
constructor(
|
|
1723
|
-
super(
|
|
1833
|
+
constructor(message, statusCode = 400) {
|
|
1834
|
+
super(message);
|
|
1724
1835
|
this.statusCode = statusCode;
|
|
1725
1836
|
this.name = "FindingsAccessError";
|
|
1726
1837
|
}
|
|
@@ -1739,7 +1850,7 @@ var publicTokenSchema = external_exports.object({
|
|
|
1739
1850
|
}).strict();
|
|
1740
1851
|
var tokenSchema = publicTokenSchema.extend({ tokenHash: external_exports.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
1741
1852
|
var stateSchema = external_exports.object({ schemaVersion: external_exports.literal(1), workspaceId: external_exports.string(), tokens: external_exports.array(tokenSchema).max(1e3) }).strict();
|
|
1742
|
-
var
|
|
1853
|
+
var createSchema2 = external_exports.object({
|
|
1743
1854
|
name: external_exports.string().trim().min(1).max(160),
|
|
1744
1855
|
scopes: scopesSchema.optional(),
|
|
1745
1856
|
expiresAt: external_exports.string().datetime().nullable().optional()
|
|
@@ -1788,7 +1899,7 @@ var FindingsAccessTokenStore = class {
|
|
|
1788
1899
|
#write(records) {
|
|
1789
1900
|
const state = stateSchema.safeParse({ schemaVersion: 1, workspaceId: this.workspaceId, tokens: records });
|
|
1790
1901
|
if (!state.success) throw new FindingsAccessError("API credential limit reached.", 409);
|
|
1791
|
-
const temporary = this.#path + "." +
|
|
1902
|
+
const temporary = this.#path + "." + randomUUID3() + ".tmp";
|
|
1792
1903
|
try {
|
|
1793
1904
|
writeFileSync(temporary, JSON.stringify(state.data), { flag: "wx", mode: 384 });
|
|
1794
1905
|
renameSync(temporary, this.#path);
|
|
@@ -1822,22 +1933,22 @@ var FindingsAccessTokenStore = class {
|
|
|
1822
1933
|
/** Caller supplies a verified owner identity, never createdBy from the HTTP body. */
|
|
1823
1934
|
create(input, createdBy) {
|
|
1824
1935
|
return this.#mutate(() => {
|
|
1825
|
-
const parsed =
|
|
1936
|
+
const parsed = createSchema2.safeParse(input);
|
|
1826
1937
|
if (!parsed.success || !createdBy || createdBy.length > 256) throw new FindingsAccessError("Use a name, optional read:findings scope and future expiry.");
|
|
1827
1938
|
const records = this.#read();
|
|
1828
1939
|
const now = this.#now();
|
|
1829
1940
|
const expiresAt = parsed.data.expiresAt === void 0 ? new Date(now + 90 * 864e5).toISOString() : parsed.data.expiresAt;
|
|
1830
1941
|
if (expiresAt !== null && Date.parse(expiresAt) <= now) throw new FindingsAccessError("API credential expiry must be in the future.");
|
|
1831
1942
|
const token = "0_find_" + randomBytes2(32).toString("base64url");
|
|
1832
|
-
const record = { id:
|
|
1943
|
+
const record = { id: randomUUID3(), workspaceId: this.workspaceId, name: parsed.data.name, scopes: ["read:findings"], createdBy, createdAt: new Date(now).toISOString(), expiresAt, revokedAt: null, tokenHash: createHash2("sha256").update(token).digest("hex") };
|
|
1833
1944
|
this.#write([...records, record]);
|
|
1834
1945
|
return { credential: redact(record), token };
|
|
1835
1946
|
});
|
|
1836
1947
|
}
|
|
1837
|
-
revoke(
|
|
1948
|
+
revoke(id2) {
|
|
1838
1949
|
return this.#mutate(() => {
|
|
1839
1950
|
const records = this.#read();
|
|
1840
|
-
const record = records.find((token) => token.id ===
|
|
1951
|
+
const record = records.find((token) => token.id === id2);
|
|
1841
1952
|
if (!record) throw new FindingsAccessError("API credential not found.", 404);
|
|
1842
1953
|
record.revokedAt ??= new Date(this.#now()).toISOString();
|
|
1843
1954
|
this.#write(records);
|
|
@@ -1894,12 +2005,12 @@ var FindingsReadApi = class {
|
|
|
1894
2005
|
this.#dbPath = resolveOsecDbPath(options.dbPath);
|
|
1895
2006
|
this.workspaceId = options.workspaceId;
|
|
1896
2007
|
}
|
|
1897
|
-
#item(db,
|
|
1898
|
-
const row = db.getFinding(
|
|
2008
|
+
#item(db, id2) {
|
|
2009
|
+
const row = db.getFinding(id2);
|
|
1899
2010
|
if (!row) throw new FindingsAccessError("Finding not found.", 404);
|
|
1900
2011
|
const scan = db.getScan(row.scanId);
|
|
1901
2012
|
if (!scan) throw new FindingsAccessError("Finding source scan was not retained.", 404);
|
|
1902
|
-
return { scanId: row.scanId, target: scan.target, finding: findingFromRow(row, { ...db.getFindingReviewFields(
|
|
2013
|
+
return { scanId: row.scanId, target: scan.target, finding: findingFromRow(row, { ...db.getFindingReviewFields(id2) }) };
|
|
1903
2014
|
}
|
|
1904
2015
|
list(query) {
|
|
1905
2016
|
const raw = {};
|
|
@@ -1934,11 +2045,11 @@ var FindingsReadApi = class {
|
|
|
1934
2045
|
db.close();
|
|
1935
2046
|
}
|
|
1936
2047
|
}
|
|
1937
|
-
detail(
|
|
1938
|
-
if (!boundedId.safeParse(
|
|
2048
|
+
detail(id2) {
|
|
2049
|
+
if (!boundedId.safeParse(id2).success) throw new FindingsAccessError("Invalid finding ID.");
|
|
1939
2050
|
const db = new osecDB(this.#dbPath, { readOnly: true });
|
|
1940
2051
|
try {
|
|
1941
|
-
return { schemaVersion: 1, workspaceId: this.workspaceId, ...this.#item(db,
|
|
2052
|
+
return { schemaVersion: 1, workspaceId: this.workspaceId, ...this.#item(db, id2) };
|
|
1942
2053
|
} finally {
|
|
1943
2054
|
db.close();
|
|
1944
2055
|
}
|
|
@@ -1946,7 +2057,7 @@ var FindingsReadApi = class {
|
|
|
1946
2057
|
export(query) {
|
|
1947
2058
|
if ([...query.keys()].some((key) => key !== "id")) throw new FindingsAccessError("Use repeated id parameters to select export findings.");
|
|
1948
2059
|
const ids = query.getAll("id");
|
|
1949
|
-
if (!ids.length || ids.length > 500 || ids.some((
|
|
2060
|
+
if (!ids.length || ids.length > 500 || ids.some((id2) => !boundedId.safeParse(id2).success) || new Set(ids).size !== ids.length) throw new FindingsAccessError("Select 1\u2013500 distinct exact finding IDs.");
|
|
1950
2061
|
return { schemaVersion: 1, workspaceId: this.workspaceId, coverage: "selected-retained-findings", report: retainedFindingSnapshot(ids, this.#dbPath) };
|
|
1951
2062
|
}
|
|
1952
2063
|
};
|
|
@@ -2023,14 +2134,17 @@ function engagementMarkdown(report) {
|
|
|
2023
2134
|
return lines.filter((line, index) => line !== "" || lines[index - 1] !== "").join("\n");
|
|
2024
2135
|
}
|
|
2025
2136
|
|
|
2137
|
+
// packages/cli/src/web/workspace-services.ts
|
|
2138
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2139
|
+
|
|
2026
2140
|
// packages/cli/src/web/skills.ts
|
|
2027
|
-
import { createHash as createHash4, randomUUID as
|
|
2141
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
|
|
2028
2142
|
import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, readdirSync, realpathSync as realpathSync2, renameSync as renameSync2, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2029
2143
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
2030
2144
|
import { homedir as homedir2 } from "node:os";
|
|
2031
2145
|
var SkillsError = class extends Error {
|
|
2032
|
-
constructor(
|
|
2033
|
-
super(
|
|
2146
|
+
constructor(message, statusCode = 400) {
|
|
2147
|
+
super(message);
|
|
2034
2148
|
this.statusCode = statusCode;
|
|
2035
2149
|
}
|
|
2036
2150
|
statusCode;
|
|
@@ -2121,12 +2235,12 @@ var SkillsStore = class _SkillsStore {
|
|
|
2121
2235
|
#discovery() {
|
|
2122
2236
|
return discoverAgentSkills(this.discoveryOptions());
|
|
2123
2237
|
}
|
|
2124
|
-
#builtin(
|
|
2125
|
-
const builtinId =
|
|
2238
|
+
#builtin(id2) {
|
|
2239
|
+
const builtinId = id2.startsWith("builtin/") ? id2.slice(8) : id2.includes("/") ? void 0 : id2;
|
|
2126
2240
|
if (!builtinId) return;
|
|
2127
2241
|
const skill = loadSkillRegistry().get(builtinId);
|
|
2128
2242
|
if (!skill) return;
|
|
2129
|
-
|
|
2243
|
+
id2 = `builtin/${builtinId}`;
|
|
2130
2244
|
const name = skill.id;
|
|
2131
2245
|
const content = `---
|
|
2132
2246
|
name: ${name}
|
|
@@ -2136,20 +2250,20 @@ description: ${JSON.stringify(skill.description)}
|
|
|
2136
2250
|
${skill.content}
|
|
2137
2251
|
`;
|
|
2138
2252
|
const files = [{ path: "SKILL.md", bytes: Buffer.from(content) }];
|
|
2139
|
-
return { metadata: { id, name, description: skill.description, scope: "builtin", revision: revision(files), writable: false, fileCount: 1, source: "Built in" }, files };
|
|
2253
|
+
return { metadata: { id: id2, name, description: skill.description, scope: "builtin", revision: revision(files), writable: false, fileCount: 1, source: "Built in" }, files };
|
|
2140
2254
|
}
|
|
2141
|
-
#entry(
|
|
2142
|
-
const built = this.#builtin(
|
|
2255
|
+
#entry(id2) {
|
|
2256
|
+
const built = this.#builtin(id2);
|
|
2143
2257
|
if (built) return built;
|
|
2144
|
-
const skill = this.#discovery().skills.find((row) => row.id ===
|
|
2258
|
+
const skill = this.#discovery().skills.find((row) => row.id === id2);
|
|
2145
2259
|
if (!skill) throw new SkillsError("Skill not found.", 404);
|
|
2146
2260
|
return this.#describe(skill);
|
|
2147
2261
|
}
|
|
2148
2262
|
#describe(skill) {
|
|
2149
|
-
const
|
|
2263
|
+
const id2 = skill.id;
|
|
2150
2264
|
const files = readFiles(skill.directory);
|
|
2151
2265
|
const shared = skill.root === this.#shared || skill.directory.startsWith(this.#shared + "/");
|
|
2152
|
-
return { metadata: { id, name: skill.name, description: skill.description, scope: shared ? "workspace" : skill.source === "mount" ? "mounted" : skill.source, revision: revision(files), writable: shared, fileCount: files.length, source: shared ? "Workspace" : skill.directory }, files, directory: skill.directory };
|
|
2266
|
+
return { metadata: { id: id2, name: skill.name, description: skill.description, scope: shared ? "workspace" : skill.source === "mount" ? "mounted" : skill.source, revision: revision(files), writable: shared, fileCount: files.length, source: shared ? "Workspace" : skill.directory }, files, directory: skill.directory };
|
|
2153
2267
|
}
|
|
2154
2268
|
#access(metadata, access = {}) {
|
|
2155
2269
|
return { ...metadata, writable: access.canWrite !== false && metadata.scope !== "builtin" && (metadata.scope === "workspace" || access.allowHostEdits === true) };
|
|
@@ -2168,12 +2282,12 @@ ${skill.content}
|
|
|
2168
2282
|
for (const skill of loadSkillRegistry().values()) skills.push(this.#builtin(`builtin/${skill.id}`).metadata);
|
|
2169
2283
|
return { skills, mounts: this.mounts(), diagnostics, workspacePath: this.workspacePath };
|
|
2170
2284
|
}
|
|
2171
|
-
get(
|
|
2172
|
-
const row = this.#entry(
|
|
2285
|
+
get(id2, access = {}) {
|
|
2286
|
+
const row = this.#entry(id2);
|
|
2173
2287
|
return { ...this.#access(row.metadata, access), content: row.files.find((file) => file.path === "SKILL.md").bytes.toString("utf8"), files: row.files.map((file) => ({ path: file.path, size: file.bytes.length })) };
|
|
2174
2288
|
}
|
|
2175
|
-
export(
|
|
2176
|
-
const row = this.#entry(
|
|
2289
|
+
export(id2) {
|
|
2290
|
+
const row = this.#entry(id2);
|
|
2177
2291
|
return { format: "agent-skill", version: 1, name: row.metadata.name, files: row.files.map((file) => ({ path: file.path, encoding: "base64", content: file.bytes.toString("base64") })) };
|
|
2178
2292
|
}
|
|
2179
2293
|
#validate(content, name) {
|
|
@@ -2241,7 +2355,7 @@ ${skill.content}
|
|
|
2241
2355
|
} catch (cause) {
|
|
2242
2356
|
if (cause.code !== "ENOENT") throw cause;
|
|
2243
2357
|
}
|
|
2244
|
-
const temporary = join2(this.#state, `.import-${
|
|
2358
|
+
const temporary = join2(this.#state, `.import-${randomUUID4()}`);
|
|
2245
2359
|
mkdirSync2(temporary, { mode: 448 });
|
|
2246
2360
|
try {
|
|
2247
2361
|
for (const file of files) {
|
|
@@ -2280,31 +2394,31 @@ ${skill.content}
|
|
|
2280
2394
|
if (!names.has("SKILL.md")) throw new SkillsError("Skill bundle needs SKILL.md.");
|
|
2281
2395
|
return this.get(this.#write(files, bundle.name));
|
|
2282
2396
|
}
|
|
2283
|
-
update(
|
|
2284
|
-
return this.#transaction(() => this.#update(
|
|
2397
|
+
update(id2, input, access = {}) {
|
|
2398
|
+
return this.#transaction(() => this.#update(id2, input, access));
|
|
2285
2399
|
}
|
|
2286
|
-
#update(
|
|
2400
|
+
#update(id2, input, access) {
|
|
2287
2401
|
const { content, expectedRevision } = parse(external_exports.object({ content: external_exports.string().min(1).max(MAX_BYTES), expectedRevision: external_exports.string().regex(/^[a-f0-9]{64}$/) }).strict(), input);
|
|
2288
|
-
const row = this.#entry(
|
|
2402
|
+
const row = this.#entry(id2);
|
|
2289
2403
|
if (!this.#access(row.metadata, access).writable) throw new SkillsError(row.metadata.scope === "builtin" ? "Save a workspace copy to edit this built-in skill." : "This account cannot edit this skill folder. Save a workspace copy instead.", 403);
|
|
2290
2404
|
if (row.metadata.revision !== expectedRevision) throw new SkillsError("This skill changed. Reload it before saving.", 409);
|
|
2291
2405
|
this.#validate(content, row.metadata.name);
|
|
2292
2406
|
if (row.files.reduce((total, file) => total + (file.path === "SKILL.md" ? Buffer.byteLength(content) : file.bytes.length), 0) > MAX_BYTES) throw new SkillsError("Skill bundle exceeds its size limit.");
|
|
2293
2407
|
const target = join2(row.directory, "SKILL.md");
|
|
2294
|
-
const pending = join2(row.directory, `.skill-edit-${
|
|
2408
|
+
const pending = join2(row.directory, `.skill-edit-${randomUUID4()}.tmp`);
|
|
2295
2409
|
try {
|
|
2296
2410
|
writeFileSync2(pending, content, { flag: "wx", mode: lstatSync2(target).mode & 511 });
|
|
2297
2411
|
renameSync2(pending, target);
|
|
2298
2412
|
} finally {
|
|
2299
2413
|
rmSync(pending, { force: true });
|
|
2300
2414
|
}
|
|
2301
|
-
return this.get(
|
|
2415
|
+
return this.get(id2, access);
|
|
2302
2416
|
}
|
|
2303
|
-
copy(
|
|
2417
|
+
copy(id2, input, access = {}) {
|
|
2304
2418
|
if (access.canWrite === false) throw new SkillsError("This account has read-only workspace access.", 403);
|
|
2305
2419
|
return this.#transaction(() => {
|
|
2306
2420
|
const { content, expectedRevision } = parse(external_exports.object({ content: external_exports.string().min(1).max(MAX_BYTES), expectedRevision: external_exports.string().regex(/^[a-f0-9]{64}$/) }).strict(), input);
|
|
2307
|
-
const row = this.#entry(
|
|
2421
|
+
const row = this.#entry(id2);
|
|
2308
2422
|
if (row.metadata.revision !== expectedRevision) throw new SkillsError("This skill changed. Reload it before copying.", 409);
|
|
2309
2423
|
const { name } = this.#validate(content);
|
|
2310
2424
|
const files = row.files.map((file) => file.path === "SKILL.md" ? { path: file.path, bytes: Buffer.from(content) } : file);
|
|
@@ -2319,18 +2433,18 @@ ${skill.content}
|
|
|
2319
2433
|
};
|
|
2320
2434
|
return {
|
|
2321
2435
|
list: () => this.list(access),
|
|
2322
|
-
read: (
|
|
2436
|
+
read: (id2) => this.get(id2, access),
|
|
2323
2437
|
create: (content) => {
|
|
2324
2438
|
assertWrite();
|
|
2325
2439
|
return this.create({ content });
|
|
2326
2440
|
},
|
|
2327
|
-
update: (
|
|
2441
|
+
update: (id2, input) => {
|
|
2328
2442
|
assertWrite();
|
|
2329
|
-
return this.update(
|
|
2443
|
+
return this.update(id2, input, access);
|
|
2330
2444
|
},
|
|
2331
|
-
copy: (
|
|
2445
|
+
copy: (id2, input) => {
|
|
2332
2446
|
assertWrite();
|
|
2333
|
-
return this.copy(
|
|
2447
|
+
return this.copy(id2, input, access);
|
|
2334
2448
|
}
|
|
2335
2449
|
};
|
|
2336
2450
|
}
|
|
@@ -2349,24 +2463,24 @@ ${skill.content}
|
|
|
2349
2463
|
const mounts = this.mounts();
|
|
2350
2464
|
if (mounts.length >= 100) throw new SkillsError("At most 100 folders can be mounted.", 409);
|
|
2351
2465
|
if (mounts.some((item) => item.path === canonical)) throw new SkillsError("This folder is already mounted.", 409);
|
|
2352
|
-
const mount = { id:
|
|
2466
|
+
const mount = { id: randomUUID4(), path: canonical };
|
|
2353
2467
|
const found = discoverAgentSkills({ homeDir: null, mounts: [mount] });
|
|
2354
2468
|
if (!found.skills.length) throw new SkillsError("No valid SKILL.md folders found.");
|
|
2355
2469
|
mounts.push(mount);
|
|
2356
2470
|
this.#saveMounts(mounts);
|
|
2357
2471
|
return mount;
|
|
2358
2472
|
}
|
|
2359
|
-
unmount(
|
|
2360
|
-
return this.#transaction(() => this.#unmount(
|
|
2473
|
+
unmount(id2) {
|
|
2474
|
+
return this.#transaction(() => this.#unmount(id2));
|
|
2361
2475
|
}
|
|
2362
|
-
#unmount(
|
|
2476
|
+
#unmount(id2) {
|
|
2363
2477
|
const mounts = this.mounts();
|
|
2364
|
-
if (!mounts.some((item) => item.id ===
|
|
2365
|
-
this.#saveMounts(mounts.filter((item) => item.id !==
|
|
2478
|
+
if (!mounts.some((item) => item.id === id2)) throw new SkillsError("Mount not found.", 404);
|
|
2479
|
+
this.#saveMounts(mounts.filter((item) => item.id !== id2));
|
|
2366
2480
|
}
|
|
2367
2481
|
#saveMounts(mounts) {
|
|
2368
2482
|
mkdirSync2(this.#state, { recursive: true, mode: 448 });
|
|
2369
|
-
const temporary = join2(this.#state, `.mounts-${
|
|
2483
|
+
const temporary = join2(this.#state, `.mounts-${randomUUID4()}`);
|
|
2370
2484
|
try {
|
|
2371
2485
|
writeFileSync2(temporary, JSON.stringify(mounts), { flag: "wx", mode: 384 });
|
|
2372
2486
|
renameSync2(temporary, join2(this.#state, "mounts.json"));
|
|
@@ -2377,12 +2491,12 @@ ${skill.content}
|
|
|
2377
2491
|
};
|
|
2378
2492
|
|
|
2379
2493
|
// packages/cli/src/web/engagements.ts
|
|
2380
|
-
import { createHash as createHash5, randomUUID as
|
|
2494
|
+
import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
|
|
2381
2495
|
import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync4, realpathSync as realpathSync3, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2382
2496
|
import { join as join3 } from "node:path";
|
|
2383
2497
|
var EngagementError = class extends Error {
|
|
2384
|
-
constructor(
|
|
2385
|
-
super(
|
|
2498
|
+
constructor(message, statusCode = 400) {
|
|
2499
|
+
super(message);
|
|
2386
2500
|
this.statusCode = statusCode;
|
|
2387
2501
|
this.name = "EngagementError";
|
|
2388
2502
|
}
|
|
@@ -2438,7 +2552,7 @@ var EngagementStore = class {
|
|
|
2438
2552
|
const body = JSON.stringify({ schemaVersion: 1, workspace: this.workspace, engagements });
|
|
2439
2553
|
if (Buffer.byteLength(body) > MAX_STATE_BYTES) throw new EngagementError("Engagement storage limit reached.", 409);
|
|
2440
2554
|
mkdirSync3(this.#directory, { recursive: true, mode: 448 });
|
|
2441
|
-
const temporary = `${this.#path}.${
|
|
2555
|
+
const temporary = `${this.#path}.${randomUUID5()}.tmp`;
|
|
2442
2556
|
try {
|
|
2443
2557
|
writeFileSync3(temporary, body, { flag: "wx", mode: 384 });
|
|
2444
2558
|
renameSync3(temporary, this.#path);
|
|
@@ -2454,7 +2568,7 @@ var EngagementStore = class {
|
|
|
2454
2568
|
if (!ids.length) return;
|
|
2455
2569
|
const db = new osecDB(this.#dbPath);
|
|
2456
2570
|
try {
|
|
2457
|
-
for (const
|
|
2571
|
+
for (const id2 of ids) if (!db.getScan(id2)) throw new EngagementError(`Selected scan was not found: ${id2}`, 404);
|
|
2458
2572
|
} finally {
|
|
2459
2573
|
db.close();
|
|
2460
2574
|
}
|
|
@@ -2514,8 +2628,8 @@ var EngagementStore = class {
|
|
|
2514
2628
|
list() {
|
|
2515
2629
|
return this.#read().sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2516
2630
|
}
|
|
2517
|
-
get(
|
|
2518
|
-
const row = this.#read().find((item) => item.id ===
|
|
2631
|
+
get(id2) {
|
|
2632
|
+
const row = this.#read().find((item) => item.id === id2);
|
|
2519
2633
|
if (!row) throw new EngagementError("Engagement not found in this workspace.", 404);
|
|
2520
2634
|
return row;
|
|
2521
2635
|
}
|
|
@@ -2526,17 +2640,17 @@ var EngagementStore = class {
|
|
|
2526
2640
|
return this.#transaction((rows) => {
|
|
2527
2641
|
if (rows.length >= 200) throw new EngagementError("Engagement limit reached.", 409);
|
|
2528
2642
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2529
|
-
const row = { ...fields, id:
|
|
2643
|
+
const row = { ...fields, id: randomUUID5(), revision: 1, createdAt: now, updatedAt: now, ...attribution ? { createdBy: attribution, updatedBy: attribution } : {} };
|
|
2530
2644
|
rows.push(row);
|
|
2531
2645
|
return structuredClone(row);
|
|
2532
2646
|
});
|
|
2533
2647
|
}
|
|
2534
|
-
update(
|
|
2648
|
+
update(id2, input, actor) {
|
|
2535
2649
|
const { expectedRevision, ...patch } = parse2(updateSchema, input);
|
|
2536
2650
|
const attribution = actor ? parse2(actorSchema, actor) : void 0;
|
|
2537
2651
|
if (patch.scanIds) this.#validateScans(patch.scanIds);
|
|
2538
2652
|
return this.#transaction((rows) => {
|
|
2539
|
-
const index = rows.findIndex((row2) => row2.id ===
|
|
2653
|
+
const index = rows.findIndex((row2) => row2.id === id2);
|
|
2540
2654
|
if (index < 0) throw new EngagementError("Engagement not found in this workspace.", 404);
|
|
2541
2655
|
if (attribution && expectedRevision === void 0) throw new EngagementError("Review the latest report before saving. Your draft has been kept.", 409);
|
|
2542
2656
|
if (expectedRevision !== void 0 && rows[index].revision !== expectedRevision) throw new EngagementError("Report changed. Your draft has been kept. Reload the latest version before saving.", 409);
|
|
@@ -2545,8 +2659,8 @@ var EngagementStore = class {
|
|
|
2545
2659
|
return structuredClone(row);
|
|
2546
2660
|
});
|
|
2547
2661
|
}
|
|
2548
|
-
report(
|
|
2549
|
-
const engagement = this.get(
|
|
2662
|
+
report(id2) {
|
|
2663
|
+
const engagement = this.get(id2);
|
|
2550
2664
|
const db = new osecDB(this.#dbPath);
|
|
2551
2665
|
try {
|
|
2552
2666
|
const sources = engagement.scanIds.map((scanId) => {
|
|
@@ -2568,22 +2682,22 @@ function reviewStatus(finding) {
|
|
|
2568
2682
|
function assembleEngagementReport(engagement, sources) {
|
|
2569
2683
|
const record = parse2(recordSchema, engagement);
|
|
2570
2684
|
const byScan = new Map(sources.map((source) => [source.scan.id, source]));
|
|
2571
|
-
if (byScan.size !== sources.length || sources.length !== record.scanIds.length || record.scanIds.some((
|
|
2685
|
+
if (byScan.size !== sources.length || sources.length !== record.scanIds.length || record.scanIds.some((id2) => !byScan.has(id2))) throw new EngagementError("Report sources must match exactly the selected scan IDs.", 404);
|
|
2572
2686
|
const groups = /* @__PURE__ */ new Map();
|
|
2573
2687
|
let findingCount = 0;
|
|
2574
|
-
const scans = record.scanIds.map((
|
|
2575
|
-
const source = byScan.get(
|
|
2688
|
+
const scans = record.scanIds.map((id2) => {
|
|
2689
|
+
const source = byScan.get(id2);
|
|
2576
2690
|
for (const finding of source.report.findings) {
|
|
2577
2691
|
findingCount++;
|
|
2578
2692
|
const fingerprint = finding.fingerprint?.trim();
|
|
2579
|
-
const key = fingerprint ? `fingerprint:${fingerprint}` : `source:${
|
|
2693
|
+
const key = fingerprint ? `fingerprint:${fingerprint}` : `source:${id2}:${finding.id}`;
|
|
2580
2694
|
const status = reviewStatus(finding);
|
|
2581
2695
|
let group = groups.get(key);
|
|
2582
2696
|
if (!group) {
|
|
2583
2697
|
group = { key, ...fingerprint ? { fingerprint } : {}, reviewStatus: status, occurrences: [] };
|
|
2584
2698
|
groups.set(key, group);
|
|
2585
2699
|
} else if (group.reviewStatus !== status) group.reviewStatus = "unreviewed";
|
|
2586
|
-
group.occurrences.push({ scanId:
|
|
2700
|
+
group.occurrences.push({ scanId: id2, findingId: finding.id, finding: structuredClone(finding) });
|
|
2587
2701
|
}
|
|
2588
2702
|
return { ...source.scan, warnings: structuredClone(source.report.warnings), executionSuccessful: source.report.executionSuccessful, exitReason: source.report.exitReason, ...source.report.error ? { error: source.report.error } : {} };
|
|
2589
2703
|
});
|
|
@@ -2606,8 +2720,8 @@ function assembleEngagementReport(engagement, sources) {
|
|
|
2606
2720
|
}
|
|
2607
2721
|
|
|
2608
2722
|
// packages/cli/src/web/workflow-triggers.ts
|
|
2609
|
-
var
|
|
2610
|
-
var toggleSchema = external_exports.object({ enabled: external_exports.boolean(), approval: external_exports.literal("enable-reviewed-trigger").optional() }).strict();
|
|
2723
|
+
var createSchema3 = external_exports.object({ workflowId: external_exports.string().min(1).max(160), workflowRevision: external_exports.number().int().positive(), sessionId: external_exports.string().min(1).max(160), cadence: external_exports.enum(["hourly", "daily", "weekly"]), startAt: external_exports.string().datetime({ offset: true }), timezone: external_exports.string().min(1).max(100), enabled: external_exports.boolean(), approval: external_exports.literal("enable-reviewed-trigger").optional() }).strict();
|
|
2724
|
+
var toggleSchema = external_exports.object({ enabled: external_exports.boolean().optional(), cadence: external_exports.enum(["hourly", "daily", "weekly"]).optional(), startAt: external_exports.string().datetime({ offset: true }).optional(), timezone: external_exports.string().min(1).max(100).optional(), workflowRevision: external_exports.number().int().positive().optional(), expectedVersion: external_exports.number().int().positive().optional(), approval: external_exports.literal("enable-reviewed-trigger").optional() }).strict().refine((value) => ["enabled", "cadence", "startAt", "timezone", "workflowRevision"].some((key) => key in value), "Provide a schedule change.");
|
|
2611
2725
|
var PREFIX = "/api/console/workflow-triggers";
|
|
2612
2726
|
var terminal = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "interrupted"]);
|
|
2613
2727
|
var WorkflowTriggerService = class {
|
|
@@ -2632,10 +2746,10 @@ var WorkflowTriggerService = class {
|
|
|
2632
2746
|
async handle(method, url, input) {
|
|
2633
2747
|
if (url.pathname !== PREFIX && !url.pathname.startsWith(`${PREFIX}/`)) return null;
|
|
2634
2748
|
try {
|
|
2635
|
-
const
|
|
2749
|
+
const id2 = url.pathname.slice(PREFIX.length + 1);
|
|
2636
2750
|
if (url.pathname === PREFIX && method === "GET") return { status: 200, data: { triggers: this.#store.list(url.searchParams.get("workflowId") ?? void 0).map(publicTrigger) } };
|
|
2637
2751
|
if (url.pathname === PREFIX && method === "POST") {
|
|
2638
|
-
const parsed =
|
|
2752
|
+
const parsed = createSchema3.parse(input);
|
|
2639
2753
|
try {
|
|
2640
2754
|
new Intl.DateTimeFormat("en", { timeZone: parsed.timezone });
|
|
2641
2755
|
} catch {
|
|
@@ -2654,22 +2768,42 @@ var WorkflowTriggerService = class {
|
|
|
2654
2768
|
}
|
|
2655
2769
|
return { status: 201, data: { trigger: publicTrigger(this.#store.create({ ...fields, reviewedConnectionIdentity: identity?.connectionIdentity, reviewedModel: identity?.model, reviewedProviderId: identity?.providerId })) } };
|
|
2656
2770
|
}
|
|
2657
|
-
const trigger = this.#store.get(
|
|
2771
|
+
const trigger = this.#store.get(id2);
|
|
2658
2772
|
if (!trigger) return error(404, "Schedule not found.");
|
|
2659
2773
|
if (method === "PATCH") {
|
|
2660
2774
|
const patch = toggleSchema.parse(input);
|
|
2775
|
+
if (patch.expectedVersion !== void 0 && patch.expectedVersion !== (trigger.version ?? 1)) return error(409, "Schedule changed. Review it again before enabling.");
|
|
2776
|
+
if (trigger.activeClaim && ["cadence", "startAt", "timezone", "workflowRevision"].some((key) => key in patch)) return error(409, "Pause the schedule and wait for its active execution before editing.");
|
|
2777
|
+
const changedTiming = patch.cadence !== void 0 || patch.startAt !== void 0 || patch.timezone !== void 0 || patch.workflowRevision !== void 0;
|
|
2778
|
+
const enabled = patch.enabled ?? (changedTiming ? false : trigger.enabled);
|
|
2779
|
+
const next = { ...trigger, ...patch, enabled };
|
|
2780
|
+
try {
|
|
2781
|
+
new Intl.DateTimeFormat("en", { timeZone: next.timezone });
|
|
2782
|
+
} catch {
|
|
2783
|
+
return error(400, "Choose a valid timezone.");
|
|
2784
|
+
}
|
|
2661
2785
|
let identity = void 0;
|
|
2662
|
-
if (
|
|
2786
|
+
if (enabled) {
|
|
2663
2787
|
if (!patch.approval) return error(400, "Review this schedule before enabling it.");
|
|
2664
2788
|
const definition = this.#workflows.get(trigger.workflowId);
|
|
2665
|
-
if (definition?.revision !==
|
|
2666
|
-
identity = await this.#adapter.validate(
|
|
2667
|
-
this.#verifyIdentity(trigger, identity);
|
|
2789
|
+
if (definition?.revision !== next.workflowRevision) return error(409, "Workflow changed. Review its current revision before scheduling.");
|
|
2790
|
+
identity = await this.#adapter.validate(next);
|
|
2791
|
+
if (!changedTiming) this.#verifyIdentity(trigger, identity);
|
|
2668
2792
|
}
|
|
2669
|
-
return { status: 200, data: { trigger: publicTrigger(this.#store.update(
|
|
2793
|
+
return { status: 200, data: { trigger: publicTrigger(this.#store.update(id2, {
|
|
2794
|
+
cadence: next.cadence,
|
|
2795
|
+
startAt: next.startAt,
|
|
2796
|
+
timezone: next.timezone,
|
|
2797
|
+
workflowRevision: next.workflowRevision,
|
|
2798
|
+
...enabled ? { reviewedModel: identity?.model, reviewedProviderId: identity?.providerId, reviewedConnectionIdentity: identity?.connectionIdentity } : {},
|
|
2799
|
+
enabled,
|
|
2800
|
+
lastStatus: enabled ? "scheduled" : "paused",
|
|
2801
|
+
lastError: void 0,
|
|
2802
|
+
nextFireAt: nextWorkflowTriggerFire(next.startAt, next.cadence, Date.now())
|
|
2803
|
+
}, enabled ? trigger.version ?? 1 : patch.expectedVersion)) } };
|
|
2670
2804
|
}
|
|
2671
2805
|
if (method === "DELETE") {
|
|
2672
|
-
if (!this.#store.delete(
|
|
2806
|
+
if (!this.#store.delete(id2)) return error(409, "Pause the schedule and wait for its active execution before deleting.");
|
|
2673
2807
|
return { status: 200, data: { deleted: true } };
|
|
2674
2808
|
}
|
|
2675
2809
|
return error(405, "Unsupported schedule action.");
|
|
@@ -2677,6 +2811,43 @@ var WorkflowTriggerService = class {
|
|
|
2677
2811
|
return error(cause instanceof external_exports.ZodError ? 400 : 409, cause instanceof Error ? cause.message.slice(0, 1e3) : "Schedule could not be updated.");
|
|
2678
2812
|
}
|
|
2679
2813
|
}
|
|
2814
|
+
/** Chat transport shares the same validation and scheduler as the HTTP editor. */
|
|
2815
|
+
async invokeLifecycle(sessionId, name, args, context) {
|
|
2816
|
+
const identifier = external_exports.string().min(1).max(160);
|
|
2817
|
+
if (name === "list_schedules") {
|
|
2818
|
+
const request = external_exports.object({ workflowId: identifier.optional() }).strict().parse(args);
|
|
2819
|
+
if (context?.workflowId && request.workflowId && context.workflowId !== request.workflowId) throw new Error("This editor is bound to another workflow.");
|
|
2820
|
+
return { schedules: this.#store.list(context?.workflowId ?? request.workflowId).filter((trigger) => context?.workflowId || trigger.sessionId === sessionId).map(publicTrigger) };
|
|
2821
|
+
}
|
|
2822
|
+
let method;
|
|
2823
|
+
let path = PREFIX;
|
|
2824
|
+
let input;
|
|
2825
|
+
if (name === "create_schedule") {
|
|
2826
|
+
const request = createSchema3.omit({ sessionId: true, approval: true }).extend({ enabled: external_exports.boolean().default(false) }).parse(args);
|
|
2827
|
+
if (context?.workflowId && context.workflowId !== request.workflowId) throw new Error("This editor is bound to another workflow.");
|
|
2828
|
+
if (request.enabled && !context?.activationApproved) throw new Error("Schedule activation requires operator approval.");
|
|
2829
|
+
method = "POST";
|
|
2830
|
+
input = { ...request, sessionId, ...request.enabled ? { approval: "enable-reviewed-trigger" } : {} };
|
|
2831
|
+
} else if (name === "update_schedule" || name === "delete_schedule") {
|
|
2832
|
+
const { id: id2, ...fields } = external_exports.object({ id: identifier }).passthrough().parse(args);
|
|
2833
|
+
const trigger = this.#store.get(id2);
|
|
2834
|
+
if (!trigger || (context?.workflowId ? trigger.workflowId !== context.workflowId : trigger.sessionId !== sessionId)) throw new Error("Schedule not found for this editor.");
|
|
2835
|
+
path += `/${encodeURIComponent(id2)}`;
|
|
2836
|
+
if (name === "delete_schedule") {
|
|
2837
|
+
external_exports.object({}).strict().parse(fields);
|
|
2838
|
+
method = "DELETE";
|
|
2839
|
+
input = void 0;
|
|
2840
|
+
} else {
|
|
2841
|
+
const patch = toggleSchema.parse(fields);
|
|
2842
|
+
if (patch.enabled && !context?.activationApproved) throw new Error("Schedule activation requires operator approval.");
|
|
2843
|
+
method = "PATCH";
|
|
2844
|
+
input = { ...patch, ...patch.enabled ? { approval: "enable-reviewed-trigger" } : {} };
|
|
2845
|
+
}
|
|
2846
|
+
} else throw new Error("Unknown schedule action.");
|
|
2847
|
+
const result = await this.handle(method, new URL(path, "http://localhost"), input);
|
|
2848
|
+
if (!result || result.status >= 400) throw new Error(result?.data?.error ?? "Schedule could not be updated.");
|
|
2849
|
+
return result.data;
|
|
2850
|
+
}
|
|
2680
2851
|
async tick(now = Date.now()) {
|
|
2681
2852
|
if (this.#closed || this.#polling) return;
|
|
2682
2853
|
this.#polling = true;
|
|
@@ -2732,8 +2903,8 @@ var WorkflowTriggerService = class {
|
|
|
2732
2903
|
this.#workflows.close();
|
|
2733
2904
|
}
|
|
2734
2905
|
};
|
|
2735
|
-
function error(status,
|
|
2736
|
-
return { status, data: { error:
|
|
2906
|
+
function error(status, message) {
|
|
2907
|
+
return { status, data: { error: message } };
|
|
2737
2908
|
}
|
|
2738
2909
|
function isProcessAlive(pid) {
|
|
2739
2910
|
try {
|
|
@@ -2852,7 +3023,10 @@ var GitHubPublicationAuth = class {
|
|
|
2852
3023
|
// packages/cli/src/web/workspace-services.ts
|
|
2853
3024
|
async function createWorkspaceServices(options) {
|
|
2854
3025
|
const skills = new SkillsStore({ workspace: options.workspace ?? process.cwd(), stateDir: options.stateDir ?? homeStateDir(), team: options.auth.enabled });
|
|
3026
|
+
const remoteProviderRelays = new RemoteProviderRelayBroker({ epoch: options.relayEpoch ?? randomUUID6() });
|
|
2855
3027
|
const gateway = new ConsoleGateway({
|
|
3028
|
+
allowChatGptPlan: !options.auth.enabled,
|
|
3029
|
+
remoteProviderRelays,
|
|
2856
3030
|
dbPath: options.dbPath,
|
|
2857
3031
|
projectPath: options.workspace,
|
|
2858
3032
|
skillAuthoring: (author, workspaceRoot) => {
|
|
@@ -2863,13 +3037,13 @@ async function createWorkspaceServices(options) {
|
|
|
2863
3037
|
...options.stateDir ? { homeDir: options.stateDir } : {}
|
|
2864
3038
|
});
|
|
2865
3039
|
const engagements = new EngagementStore({ workspace: options.workspace ?? process.cwd(), dbPath: options.dbPath, ...options.stateDir ? { stateDir: options.stateDir } : {} });
|
|
2866
|
-
const operator = new WebOperatorServices({ isTurnActive: () => gateway.hasActiveTurns() });
|
|
3040
|
+
const operator = new WebOperatorServices({ allowChatGptPlan: !options.auth.enabled, isTurnActive: () => gateway.hasActiveTurns() });
|
|
2867
3041
|
let workflows;
|
|
2868
3042
|
let engine;
|
|
2869
3043
|
let triggers;
|
|
2870
3044
|
let github;
|
|
2871
3045
|
const dispose = async () => {
|
|
2872
|
-
const actions = [() => engine?.dispose(), () => triggers?.dispose(), () => gateway.closeAll(), () => workflows?.dispose(), () => operator.dispose(), () => github?.dispose()];
|
|
3046
|
+
const actions = [() => remoteProviderRelays.dispose(), () => engine?.dispose(), () => triggers?.dispose(), () => gateway.closeAll(), () => workflows?.dispose(), () => operator.dispose(), () => github?.dispose()];
|
|
2873
3047
|
let failure;
|
|
2874
3048
|
for (const action of actions) {
|
|
2875
3049
|
try {
|
|
@@ -2900,8 +3074,10 @@ async function createWorkspaceServices(options) {
|
|
|
2900
3074
|
return { executionId: result.execution.id };
|
|
2901
3075
|
}
|
|
2902
3076
|
} });
|
|
3077
|
+
const schedules = triggers;
|
|
3078
|
+
gateway.attachScheduleLifecycle({ invoke: (sessionId, name, args, context) => schedules.invokeLifecycle(sessionId, name, args, context) });
|
|
2903
3079
|
github = new GitHubPublicationAuth();
|
|
2904
|
-
return { skills, gateway, engagements, operator, workflows: workflow, engine, triggers, github, dispose };
|
|
3080
|
+
return { remoteProviderRelays, skills, gateway, engagements, operator, workflows: workflow, engine, triggers, github, dispose };
|
|
2905
3081
|
} catch (cause) {
|
|
2906
3082
|
await dispose().catch(() => {
|
|
2907
3083
|
});
|
|
@@ -2911,7 +3087,7 @@ async function createWorkspaceServices(options) {
|
|
|
2911
3087
|
|
|
2912
3088
|
// packages/cli/src/web/team-setup.ts
|
|
2913
3089
|
import { closeSync as closeSync4, constants as constants4, existsSync, fstatSync as fstatSync4, fsyncSync, mkdirSync as mkdirSync4, openSync as openSync4, readFileSync as readFileSync5, realpathSync as realpathSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2914
|
-
import { createHash as createHash6, randomUUID as
|
|
3090
|
+
import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
|
|
2915
3091
|
import { dirname, join as join4, resolve as resolve3 } from "node:path";
|
|
2916
3092
|
var setupSchema = external_exports.object({
|
|
2917
3093
|
workspaceName: external_exports.string().trim().min(1).max(160),
|
|
@@ -2977,7 +3153,7 @@ var TeamSetupService = class {
|
|
|
2977
3153
|
if (auth.members().length >= 1e3) throw new TeamAuthError("Workspace member limit reached", 409);
|
|
2978
3154
|
this.busy = true;
|
|
2979
3155
|
const lockPath = `${this.configPath}.lock`;
|
|
2980
|
-
const tempPath = `${this.configPath}.${
|
|
3156
|
+
const tempPath = `${this.configPath}.${randomUUID7()}.tmp`;
|
|
2981
3157
|
let lockFD;
|
|
2982
3158
|
let temporaryFD;
|
|
2983
3159
|
try {
|
|
@@ -3034,7 +3210,7 @@ var TeamSetupService = class {
|
|
|
3034
3210
|
try {
|
|
3035
3211
|
const data = parsed.data;
|
|
3036
3212
|
const config = {
|
|
3037
|
-
workspace: { id:
|
|
3213
|
+
workspace: { id: randomUUID7(), name: data.workspaceName },
|
|
3038
3214
|
users: [{ id: data.userId, name: data.displayName, role: "owner", passwordHash: await hashTeamPassword(data.password) }]
|
|
3039
3215
|
};
|
|
3040
3216
|
const auth = new TeamAuth({ config, origin: this.options.origin() });
|
|
@@ -3087,12 +3263,12 @@ async function handleTeamSetupRequest(req, url, setup, readBody) {
|
|
|
3087
3263
|
}
|
|
3088
3264
|
|
|
3089
3265
|
// packages/cli/src/web/collaboration.ts
|
|
3090
|
-
import { createHash as createHash7, randomUUID as
|
|
3266
|
+
import { createHash as createHash7, randomUUID as randomUUID8 } from "node:crypto";
|
|
3091
3267
|
import { closeSync as closeSync5, constants as constants5, fstatSync as fstatSync5, mkdirSync as mkdirSync5, openSync as openSync5, readFileSync as readFileSync6, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3092
3268
|
import { join as join5 } from "node:path";
|
|
3093
3269
|
var CollaborationError = class extends Error {
|
|
3094
|
-
constructor(
|
|
3095
|
-
super(
|
|
3270
|
+
constructor(message, statusCode = 400) {
|
|
3271
|
+
super(message);
|
|
3096
3272
|
this.statusCode = statusCode;
|
|
3097
3273
|
this.name = "CollaborationError";
|
|
3098
3274
|
}
|
|
@@ -3172,9 +3348,9 @@ var CollaborationService = class {
|
|
|
3172
3348
|
});
|
|
3173
3349
|
}
|
|
3174
3350
|
/** Trusted post-write adapter only: the caller proves this resource exists in its workspace. */
|
|
3175
|
-
notifyChanged(kind,
|
|
3176
|
-
if (!external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id }).success) throw new CollaborationError("Invalid changed resource.");
|
|
3177
|
-
this.#publish({ type: "changed", kind, id });
|
|
3351
|
+
notifyChanged(kind, id2) {
|
|
3352
|
+
if (!external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id: id2 }).success) throw new CollaborationError("Invalid changed resource.");
|
|
3353
|
+
this.#publish({ type: "changed", kind, id: id2 });
|
|
3178
3354
|
}
|
|
3179
3355
|
#actor(sessionId) {
|
|
3180
3356
|
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 4096) throw new CollaborationError("An authenticated team session is required.", 401);
|
|
@@ -3201,7 +3377,7 @@ var CollaborationService = class {
|
|
|
3201
3377
|
if (bytes.length > 32 * 1024 * 1024) throw new Error("size");
|
|
3202
3378
|
let source = JSON.parse(bytes.toString("utf8"));
|
|
3203
3379
|
const legacy = external_exports.object({ schemaVersion: external_exports.literal(1), workspaceId: external_exports.string(), conversations: external_exports.array(external_exports.object({ id: external_exports.string(), revision: external_exports.number(), createdBy: attributionSchema, createdAt: external_exports.number(), members: external_exports.array(memberSchema2) }).passthrough()).max(1e3) }).strict().safeParse(source);
|
|
3204
|
-
if (legacy.success) source = { schemaVersion: 2, workspaceId: legacy.data.workspaceId, rooms: legacy.data.conversations.map(({ id, revision: revision2, createdBy, createdAt, members }) => ({ kind: "conversation", id, revision: revision2, createdBy, createdAt, members })) };
|
|
3380
|
+
if (legacy.success) source = { schemaVersion: 2, workspaceId: legacy.data.workspaceId, rooms: legacy.data.conversations.map(({ id: id2, revision: revision2, createdBy, createdAt, members }) => ({ kind: "conversation", id: id2, revision: revision2, createdBy, createdAt, members })) };
|
|
3205
3381
|
const parsed = stateSchema3.parse(source);
|
|
3206
3382
|
if (parsed.workspaceId !== this.#options.workspaceId || new Set(parsed.rooms.map((row) => this.#roomKey(row.kind, row.id))).size !== parsed.rooms.length) throw new Error("workspace");
|
|
3207
3383
|
return parsed.rooms;
|
|
@@ -3216,7 +3392,7 @@ var CollaborationService = class {
|
|
|
3216
3392
|
if (!state.success) throw new CollaborationError("Collaboration record limit reached.", 409);
|
|
3217
3393
|
const data = JSON.stringify(state.data);
|
|
3218
3394
|
if (Buffer.byteLength(data) > 32 * 1024 * 1024) throw new CollaborationError("Collaboration storage limit reached.", 409);
|
|
3219
|
-
const temporary = this.#path + "." +
|
|
3395
|
+
const temporary = this.#path + "." + randomUUID8() + ".tmp";
|
|
3220
3396
|
try {
|
|
3221
3397
|
writeFileSync5(temporary, data, { flag: "wx", mode: 384 });
|
|
3222
3398
|
renameSync5(temporary, this.#path);
|
|
@@ -3278,11 +3454,11 @@ var CollaborationService = class {
|
|
|
3278
3454
|
unlinkSync5(this.#lock);
|
|
3279
3455
|
}
|
|
3280
3456
|
}
|
|
3281
|
-
#roomKey(kind,
|
|
3282
|
-
return kind + ":" +
|
|
3457
|
+
#roomKey(kind, id2) {
|
|
3458
|
+
return kind + ":" + id2;
|
|
3283
3459
|
}
|
|
3284
|
-
#room(rows, kind,
|
|
3285
|
-
const row = rows.find((item) => item.kind === kind && item.id ===
|
|
3460
|
+
#room(rows, kind, id2) {
|
|
3461
|
+
const row = rows.find((item) => item.kind === kind && item.id === id2);
|
|
3286
3462
|
if (!row) throw new CollaborationError("Shared resource room not found.", 404);
|
|
3287
3463
|
return row;
|
|
3288
3464
|
}
|
|
@@ -3304,8 +3480,8 @@ var CollaborationService = class {
|
|
|
3304
3480
|
const { workspaceId: _workspace, ...viewer } = actor;
|
|
3305
3481
|
return viewer;
|
|
3306
3482
|
}
|
|
3307
|
-
#presenceFor(kind,
|
|
3308
|
-
const entries = this.#presence.get(this.#roomKey(kind,
|
|
3483
|
+
#presenceFor(kind, id2) {
|
|
3484
|
+
const entries = this.#presence.get(this.#roomKey(kind, id2));
|
|
3309
3485
|
const presence = /* @__PURE__ */ new Map();
|
|
3310
3486
|
for (const [key, entry] of entries ?? []) {
|
|
3311
3487
|
if (!entry.viewing || entry.updatedAt + this.#presenceMs <= this.#now()) {
|
|
@@ -3332,15 +3508,15 @@ var CollaborationService = class {
|
|
|
3332
3508
|
return [...presence.values()];
|
|
3333
3509
|
}
|
|
3334
3510
|
/** Routing must validate that the underlying resource exists before registration. */
|
|
3335
|
-
registerRoom(kind,
|
|
3511
|
+
registerRoom(kind, id2, sessionId) {
|
|
3336
3512
|
const actor = this.#actor(sessionId);
|
|
3337
|
-
const valid = external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id });
|
|
3513
|
+
const valid = external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id: id2 });
|
|
3338
3514
|
if (!valid.success) throw new CollaborationError("Invalid resource room.");
|
|
3339
3515
|
let joined = false;
|
|
3340
3516
|
this.#transaction((rows) => {
|
|
3341
|
-
let row = rows.find((item) => item.kind === kind && item.id ===
|
|
3517
|
+
let row = rows.find((item) => item.kind === kind && item.id === id2);
|
|
3342
3518
|
if (!row) {
|
|
3343
|
-
row = { kind, id, revision: 0, createdBy: this.#attribution(actor), createdAt: this.#now(), members: [] };
|
|
3519
|
+
row = { kind, id: id2, revision: 0, createdBy: this.#attribution(actor), createdAt: this.#now(), members: [] };
|
|
3344
3520
|
rows.push(row);
|
|
3345
3521
|
}
|
|
3346
3522
|
const revision2 = row.revision;
|
|
@@ -3348,7 +3524,7 @@ var CollaborationService = class {
|
|
|
3348
3524
|
joined = row.revision !== revision2;
|
|
3349
3525
|
});
|
|
3350
3526
|
if (joined) this.#notifyPresence();
|
|
3351
|
-
return this.roomSnapshot(kind,
|
|
3527
|
+
return this.roomSnapshot(kind, id2, sessionId);
|
|
3352
3528
|
}
|
|
3353
3529
|
register(conversationId, sessionId) {
|
|
3354
3530
|
this.registerRoom("conversation", conversationId, sessionId);
|
|
@@ -3357,10 +3533,10 @@ var CollaborationService = class {
|
|
|
3357
3533
|
hasConversation(conversationId) {
|
|
3358
3534
|
return this.#read().some((row) => row.kind === "conversation" && row.id === conversationId);
|
|
3359
3535
|
}
|
|
3360
|
-
roomSnapshot(kind,
|
|
3536
|
+
roomSnapshot(kind, id2, sessionId) {
|
|
3361
3537
|
const actor = this.#actor(sessionId);
|
|
3362
|
-
const row = this.#room(this.#read(), kind,
|
|
3363
|
-
return { kind, id, workspaceId: this.#options.workspaceId, revision: row.revision, viewer: this.#viewer(actor), canWrite: actor.role !== "viewer", members: structuredClone(row.members), presence: this.#presenceFor(kind,
|
|
3538
|
+
const row = this.#room(this.#read(), kind, id2);
|
|
3539
|
+
return { kind, id: id2, workspaceId: this.#options.workspaceId, revision: row.revision, viewer: this.#viewer(actor), canWrite: actor.role !== "viewer", members: structuredClone(row.members), presence: this.#presenceFor(kind, id2) };
|
|
3364
3540
|
}
|
|
3365
3541
|
snapshot(conversationId, sessionId) {
|
|
3366
3542
|
const { kind: _kind, id: _id, ...snapshot } = this.roomSnapshot("conversation", conversationId, sessionId);
|
|
@@ -3374,19 +3550,19 @@ var CollaborationService = class {
|
|
|
3374
3550
|
});
|
|
3375
3551
|
return { workspaceId: this.#options.workspaceId, viewer: this.#viewer(actor), rooms };
|
|
3376
3552
|
}
|
|
3377
|
-
heartbeatRoom(kind,
|
|
3553
|
+
heartbeatRoom(kind, id2, sessionId, input) {
|
|
3378
3554
|
const actor = this.#actor(sessionId);
|
|
3379
3555
|
const parsed = external_exports.object({ typing: external_exports.boolean(), viewing: external_exports.boolean().default(true), clientId: external_exports.string().uuid().optional() }).strict().safeParse(input);
|
|
3380
3556
|
if (!parsed.success) throw new CollaborationError("Invalid presence update.");
|
|
3381
|
-
this.#presenceFor(kind,
|
|
3557
|
+
this.#presenceFor(kind, id2);
|
|
3382
3558
|
let joined = false;
|
|
3383
3559
|
this.#transaction((rows) => {
|
|
3384
|
-
const row = this.#room(rows, kind,
|
|
3560
|
+
const row = this.#room(rows, kind, id2);
|
|
3385
3561
|
const revision2 = row.revision;
|
|
3386
3562
|
this.#join(row, actor);
|
|
3387
3563
|
joined = row.revision !== revision2;
|
|
3388
3564
|
});
|
|
3389
|
-
const key = this.#roomKey(kind,
|
|
3565
|
+
const key = this.#roomKey(kind, id2);
|
|
3390
3566
|
let entries = this.#presence.get(key);
|
|
3391
3567
|
if (!entries) {
|
|
3392
3568
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -3398,7 +3574,7 @@ var CollaborationService = class {
|
|
|
3398
3574
|
if (parsed.data.viewing) entries.set(entryKey, { ...this.#attribution(actor), sessionId, viewing: true, typing: parsed.data.typing, updatedAt: this.#now() });
|
|
3399
3575
|
else entries.delete(entryKey);
|
|
3400
3576
|
if (joined || previous?.typing !== (parsed.data.viewing ? parsed.data.typing : void 0) || previous?.displayName !== (parsed.data.viewing ? actor.displayName : void 0)) this.#notifyPresence();
|
|
3401
|
-
return this.roomSnapshot(kind,
|
|
3577
|
+
return this.roomSnapshot(kind, id2, sessionId);
|
|
3402
3578
|
}
|
|
3403
3579
|
heartbeat(conversationId, sessionId, input) {
|
|
3404
3580
|
this.heartbeatRoom("conversation", conversationId, sessionId, input);
|
|
@@ -3457,29 +3633,133 @@ async function handleTeamRequest(req, url, auth, collaboration, gateway, readBod
|
|
|
3457
3633
|
if (resource2) {
|
|
3458
3634
|
if (method !== "POST") throw new TeamAuthError("Method not allowed.", 405);
|
|
3459
3635
|
const kind = resource2[1];
|
|
3460
|
-
const
|
|
3461
|
-
if (kind === "conversation") gateway.get(
|
|
3462
|
-
else if (validateResource) await validateResource(kind,
|
|
3636
|
+
const id3 = resource2[2];
|
|
3637
|
+
if (kind === "conversation") gateway.get(id3);
|
|
3638
|
+
else if (validateResource) await validateResource(kind, id3);
|
|
3463
3639
|
else throw new TeamAuthError("Resource presence is not configured for this engine.", 404);
|
|
3464
|
-
collaboration.registerRoom(kind,
|
|
3465
|
-
return { status: 200, data: collaboration.heartbeatRoom(kind,
|
|
3640
|
+
collaboration.registerRoom(kind, id3, session);
|
|
3641
|
+
return { status: 200, data: collaboration.heartbeatRoom(kind, id3, session, await readBody()) };
|
|
3466
3642
|
}
|
|
3467
3643
|
const match = /^conversations\/([A-Za-z0-9_.:-]+)(?:\/(presence))?$/.exec(path);
|
|
3468
3644
|
if (!match) throw new TeamAuthError("Team resource not found.", 404);
|
|
3469
|
-
const
|
|
3470
|
-
gateway.get(
|
|
3471
|
-
if (!collaboration.hasConversation(
|
|
3472
|
-
if (!match[2] && method === "GET") return { status: 200, data: collaboration.snapshot(
|
|
3473
|
-
if (match[2] === "presence" && method === "POST") return { status: 200, data: collaboration.heartbeat(
|
|
3645
|
+
const id2 = match[1];
|
|
3646
|
+
gateway.get(id2);
|
|
3647
|
+
if (!collaboration.hasConversation(id2)) collaboration.register(id2, session);
|
|
3648
|
+
if (!match[2] && method === "GET") return { status: 200, data: collaboration.snapshot(id2, session) };
|
|
3649
|
+
if (match[2] === "presence" && method === "POST") return { status: 200, data: collaboration.heartbeat(id2, session, await readBody()) };
|
|
3474
3650
|
throw new TeamAuthError("Method not allowed.", 405);
|
|
3475
3651
|
}
|
|
3476
|
-
function assertTeamConsoleMutation(req,
|
|
3652
|
+
function assertTeamConsoleMutation(req, id2, auth, collaboration, gateway) {
|
|
3477
3653
|
if (!auth.enabled) return;
|
|
3478
|
-
gateway.get(
|
|
3654
|
+
gateway.get(id2);
|
|
3479
3655
|
const session = getSessionId(req);
|
|
3480
3656
|
if (!session || !collaboration) throw new TeamAuthError("Sign in to this workspace.");
|
|
3481
|
-
if (!collaboration.hasConversation(
|
|
3482
|
-
collaboration.assertWriter(
|
|
3657
|
+
if (!collaboration.hasConversation(id2)) collaboration.register(id2, session);
|
|
3658
|
+
collaboration.assertWriter(id2, session);
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
// packages/cli/src/web/browser-live-routes.ts
|
|
3662
|
+
var registry = { list: listBrowserViews, subscribe: subscribeBrowserView };
|
|
3663
|
+
var HEARTBEAT_MS = 15e3;
|
|
3664
|
+
var STREAM_LIFETIME_MS = 5 * 6e4;
|
|
3665
|
+
async function handleBrowserLiveRequest(req, res, url, ownerId, pageId, source = registry) {
|
|
3666
|
+
const reply = (status, body) => {
|
|
3667
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
3668
|
+
res.end(JSON.stringify(body));
|
|
3669
|
+
};
|
|
3670
|
+
if (url.search) {
|
|
3671
|
+
reply(400, { error: "Browser view endpoints do not accept query parameters." });
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
if (req.method !== "GET") {
|
|
3675
|
+
reply(405, { error: "Use GET for browser views." });
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
const views = source.list(ownerId);
|
|
3679
|
+
if (!pageId) {
|
|
3680
|
+
reply(200, { views });
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
const view = views.find((item) => item.pageId === pageId);
|
|
3684
|
+
if (!view) {
|
|
3685
|
+
reply(404, { error: "Browser view was closed or does not belong to this conversation." });
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
if (!view.liveAvailable) {
|
|
3689
|
+
reply(409, { error: "Live browser view is unavailable for this execution backend." });
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
res.writeHead(200, {
|
|
3693
|
+
"Content-Type": "text/event-stream",
|
|
3694
|
+
"Cache-Control": "no-cache, no-store, no-transform",
|
|
3695
|
+
"X-Accel-Buffering": "no",
|
|
3696
|
+
Connection: "keep-alive"
|
|
3697
|
+
});
|
|
3698
|
+
res.flushHeaders?.();
|
|
3699
|
+
let closed = false;
|
|
3700
|
+
let blocked = false;
|
|
3701
|
+
let unsubscribe;
|
|
3702
|
+
let heartbeat;
|
|
3703
|
+
let lifetime;
|
|
3704
|
+
const cleanup = () => {
|
|
3705
|
+
if (closed) return;
|
|
3706
|
+
closed = true;
|
|
3707
|
+
clearInterval(heartbeat);
|
|
3708
|
+
clearTimeout(lifetime);
|
|
3709
|
+
unsubscribe?.();
|
|
3710
|
+
unsubscribe = void 0;
|
|
3711
|
+
res.off("drain", drain);
|
|
3712
|
+
res.off("close", cleanup);
|
|
3713
|
+
res.off("error", cleanup);
|
|
3714
|
+
req.off("aborted", cleanup);
|
|
3715
|
+
};
|
|
3716
|
+
const drain = () => {
|
|
3717
|
+
blocked = false;
|
|
3718
|
+
};
|
|
3719
|
+
const write = (event, data) => {
|
|
3720
|
+
if (closed || blocked || res.destroyed || res.writableEnded) return;
|
|
3721
|
+
blocked = !res.write(`event: ${event}
|
|
3722
|
+
data: ${JSON.stringify(data)}
|
|
3723
|
+
|
|
3724
|
+
`);
|
|
3725
|
+
};
|
|
3726
|
+
const finish = (status) => {
|
|
3727
|
+
write("state", { status });
|
|
3728
|
+
cleanup();
|
|
3729
|
+
if (!res.destroyed && !res.writableEnded) res.end();
|
|
3730
|
+
};
|
|
3731
|
+
res.on("drain", drain);
|
|
3732
|
+
res.on("close", cleanup);
|
|
3733
|
+
res.on("error", cleanup);
|
|
3734
|
+
req.on("aborted", cleanup);
|
|
3735
|
+
write("state", { status: "connecting" });
|
|
3736
|
+
heartbeat = setInterval(() => {
|
|
3737
|
+
try {
|
|
3738
|
+
if (!source.list(ownerId).some((item) => item.pageId === pageId)) {
|
|
3739
|
+
finish("closed");
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (!closed && !blocked) blocked = !res.write(": heartbeat\n\n");
|
|
3743
|
+
} catch {
|
|
3744
|
+
finish("unavailable");
|
|
3745
|
+
}
|
|
3746
|
+
}, HEARTBEAT_MS);
|
|
3747
|
+
lifetime = setTimeout(() => finish("expired"), STREAM_LIFETIME_MS);
|
|
3748
|
+
heartbeat.unref?.();
|
|
3749
|
+
lifetime.unref?.();
|
|
3750
|
+
try {
|
|
3751
|
+
const stop = await source.subscribe(ownerId, pageId, (frame) => {
|
|
3752
|
+
if (closed || blocked || frame.mimeType !== "image/jpeg" || frame.data.length > 4 * 1024 * 1024) return;
|
|
3753
|
+
write("frame", frame);
|
|
3754
|
+
});
|
|
3755
|
+
if (closed) stop();
|
|
3756
|
+
else {
|
|
3757
|
+
unsubscribe = stop;
|
|
3758
|
+
write("state", { status: "live" });
|
|
3759
|
+
}
|
|
3760
|
+
} catch {
|
|
3761
|
+
finish("unavailable");
|
|
3762
|
+
}
|
|
3483
3763
|
}
|
|
3484
3764
|
|
|
3485
3765
|
// packages/cli/src/web/team-presence-stream.ts
|
|
@@ -3643,8 +3923,8 @@ function discoverSshHosts(configPath = join6(homedir3(), ".ssh", "config")) {
|
|
|
3643
3923
|
|
|
3644
3924
|
// packages/cli/src/commands/dashboard.ts
|
|
3645
3925
|
var WebRequestError = class extends Error {
|
|
3646
|
-
constructor(
|
|
3647
|
-
super(
|
|
3926
|
+
constructor(message, statusCode) {
|
|
3927
|
+
super(message);
|
|
3648
3928
|
this.statusCode = statusCode;
|
|
3649
3929
|
}
|
|
3650
3930
|
statusCode;
|
|
@@ -3701,7 +3981,7 @@ function openBrowser(url) {
|
|
|
3701
3981
|
execFile2(cmd, args, () => {
|
|
3702
3982
|
});
|
|
3703
3983
|
}
|
|
3704
|
-
function
|
|
3984
|
+
function json2(res, status, body) {
|
|
3705
3985
|
res.writeHead(status, {
|
|
3706
3986
|
"Content-Type": "application/json; charset=utf-8",
|
|
3707
3987
|
"Cache-Control": "no-store"
|
|
@@ -4364,7 +4644,11 @@ function resolveDashboardAssetDir(explicitAssetDir) {
|
|
|
4364
4644
|
}
|
|
4365
4645
|
const embedded = materializeEmbeddedDashboardAssets(DASHBOARD_ASSETS);
|
|
4366
4646
|
if (embedded) return embedded;
|
|
4647
|
+
const checkoutPackage = resolve4(moduleDir, "..", "..");
|
|
4367
4648
|
for (const candidate of [
|
|
4649
|
+
// A source-built CLI can run from a different project directory. Resolve
|
|
4650
|
+
// its sibling app from the checkout, never from an unrelated installed CLI.
|
|
4651
|
+
...existsSync2(join7(checkoutPackage, "src", "index.ts")) ? [join7(checkoutPackage, "..", "dashboard", "dist")] : [],
|
|
4368
4652
|
join7(process.cwd(), "packages", "dashboard", "dist"),
|
|
4369
4653
|
join7(process.cwd(), "dist", "dashboard")
|
|
4370
4654
|
]) {
|
|
@@ -4387,7 +4671,7 @@ function requireControlToken(req, res, controlToken, engineBearer) {
|
|
|
4387
4671
|
const matches = (a, b) => typeof a === "string" && Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual3(Buffer.from(a), Buffer.from(b));
|
|
4388
4672
|
const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice(7) : void 0;
|
|
4389
4673
|
if (!matches(provided, controlToken) && !matches(bearer, controlToken) && !(engineBearer && matches(bearer, engineBearer))) {
|
|
4390
|
-
|
|
4674
|
+
json2(res, 403, { error: "Invalid or missing control token" });
|
|
4391
4675
|
return false;
|
|
4392
4676
|
}
|
|
4393
4677
|
return true;
|
|
@@ -4407,59 +4691,59 @@ async function handleWebConsoleApiRequest(req, res, requestUrl, gateway, operato
|
|
|
4407
4691
|
const input = method === "GET" || method === "HEAD" ? void 0 : await readJson(req);
|
|
4408
4692
|
const member = team?.auth.resolveSession(req);
|
|
4409
4693
|
const author = member ? { userId: member.userId, displayName: member.displayName } : void 0;
|
|
4410
|
-
if (team && method !== "GET" && method !== "HEAD") {
|
|
4694
|
+
if (team?.auth.enabled && method !== "GET" && method !== "HEAD") {
|
|
4411
4695
|
const readResume = /^saved\/[^/]+\/resume$/.test(path) && input !== null && typeof input === "object" && Object.keys(input).length === 0;
|
|
4412
4696
|
if (/^saved\/[^/]+\/resume$/.test(path) && team.auth.resolveSession(req)?.role !== "owner" && !readResume) throw new WebRequestError("Only workspace owners can override a saved conversation's configuration.", 403);
|
|
4413
4697
|
const liveId = /^sessions\/([^/]+)/.exec(path)?.[1];
|
|
4414
4698
|
const bodyId = input && typeof input === "object" && "sessionId" in input && typeof input.sessionId === "string" ? input.sessionId : void 0;
|
|
4415
4699
|
const savedId = /^saved\/([^/]+)/.exec(path)?.[1];
|
|
4416
4700
|
const savedLiveId = savedId ? gateway.list().find((item) => item.id === savedId || item.savedId === savedId)?.id : void 0;
|
|
4417
|
-
const
|
|
4418
|
-
if (
|
|
4701
|
+
const id2 = liveId ?? bodyId ?? savedLiveId;
|
|
4702
|
+
if (id2 && !readResume) assertTeamConsoleMutation(req, id2, team.auth, team.collaboration, gateway);
|
|
4419
4703
|
}
|
|
4420
4704
|
try {
|
|
4421
4705
|
if (path === "execution" && method === "GET") {
|
|
4422
|
-
|
|
4706
|
+
json2(res, 200, await workbenchStatus());
|
|
4423
4707
|
return true;
|
|
4424
4708
|
}
|
|
4425
4709
|
if (path === "github" && method === "GET") {
|
|
4426
|
-
|
|
4710
|
+
json2(res, 200, { github: await github.status() });
|
|
4427
4711
|
return true;
|
|
4428
4712
|
}
|
|
4429
4713
|
if (path === "github/connect" && method === "POST") {
|
|
4430
|
-
|
|
4714
|
+
json2(res, 200, { github: await github.connect(input) });
|
|
4431
4715
|
return true;
|
|
4432
4716
|
}
|
|
4433
4717
|
if (path === "github/device-auth" && method === "POST") {
|
|
4434
|
-
|
|
4718
|
+
json2(res, 202, { github: await github.start() });
|
|
4435
4719
|
return true;
|
|
4436
4720
|
}
|
|
4437
4721
|
if (path === "github/device-auth" && method === "DELETE") {
|
|
4438
|
-
|
|
4722
|
+
json2(res, 200, { github: await github.cancel() });
|
|
4439
4723
|
return true;
|
|
4440
4724
|
}
|
|
4441
4725
|
const trigger = await triggers.handle(method, requestUrl, input);
|
|
4442
4726
|
if (trigger) {
|
|
4443
|
-
|
|
4727
|
+
json2(res, trigger.status, trigger.data);
|
|
4444
4728
|
return true;
|
|
4445
4729
|
}
|
|
4446
4730
|
const workflow = await workflows.handle(requestUrl.pathname, method, input, requestUrl.searchParams);
|
|
4447
4731
|
if (workflow) {
|
|
4448
4732
|
if (team?.collaboration && method !== "GET" && method !== "HEAD" && workflow.status >= 200 && workflow.status < 300) {
|
|
4449
4733
|
const definition = workflow.data && typeof workflow.data === "object" && "definition" in workflow.data ? workflow.data.definition : void 0;
|
|
4450
|
-
const
|
|
4451
|
-
if (
|
|
4734
|
+
const id3 = definition && typeof definition === "object" && "id" in definition && typeof definition.id === "string" ? definition.id : /^\/api\/console\/workflow-definitions\/([^/]+)/.exec(requestUrl.pathname)?.[1];
|
|
4735
|
+
if (id3) team.collaboration.notifyChanged("workflow", decodeURIComponent(id3));
|
|
4452
4736
|
}
|
|
4453
|
-
|
|
4737
|
+
json2(res, workflow.status, workflow.data);
|
|
4454
4738
|
return true;
|
|
4455
4739
|
}
|
|
4456
4740
|
const service = await operator.handle(requestUrl.pathname, method, input, requestUrl.searchParams);
|
|
4457
4741
|
if (service) {
|
|
4458
|
-
|
|
4742
|
+
json2(res, service.status, service.data);
|
|
4459
4743
|
return true;
|
|
4460
4744
|
}
|
|
4461
4745
|
if (path === "search" && method === "GET") {
|
|
4462
|
-
|
|
4746
|
+
json2(res, 200, await gateway.search({
|
|
4463
4747
|
q: requestUrl.searchParams.get("q") ?? "",
|
|
4464
4748
|
...requestUrl.searchParams.has("limit") ? { limit: Number(requestUrl.searchParams.get("limit")) } : {},
|
|
4465
4749
|
...requestUrl.searchParams.has("offset") ? { offset: Number(requestUrl.searchParams.get("offset")) } : {}
|
|
@@ -4467,84 +4751,99 @@ async function handleWebConsoleApiRequest(req, res, requestUrl, gateway, operato
|
|
|
4467
4751
|
return true;
|
|
4468
4752
|
}
|
|
4469
4753
|
if (path === "sessions") {
|
|
4470
|
-
if (method === "GET")
|
|
4754
|
+
if (method === "GET") json2(res, 200, { sessions: gateway.list() });
|
|
4471
4755
|
else if (method === "POST") {
|
|
4472
4756
|
const session = gateway.create(input);
|
|
4473
4757
|
if (team?.auth.enabled && team.collaboration) team.collaboration.register(session.id, getSessionId(req));
|
|
4474
|
-
|
|
4475
|
-
} else
|
|
4758
|
+
json2(res, 201, { session });
|
|
4759
|
+
} else json2(res, 405, { error: "Method not allowed." });
|
|
4476
4760
|
return true;
|
|
4477
4761
|
}
|
|
4478
4762
|
const parts = path.split("/").map((part) => decodeURIComponent(part));
|
|
4479
4763
|
if (parts.some((part) => !/^[A-Za-z0-9_.:-]+$/.test(part))) throw new ConsoleGatewayError("Invalid resource identifier.", 400);
|
|
4480
4764
|
if (parts[0] === "saved") {
|
|
4481
|
-
const
|
|
4482
|
-
if (!
|
|
4765
|
+
const id3 = parts[1];
|
|
4766
|
+
if (!id3 && method === "GET") {
|
|
4483
4767
|
const limit = requestUrl.searchParams.get("limit");
|
|
4484
|
-
|
|
4768
|
+
json2(res, 200, { sessions: gateway.listSaved({
|
|
4485
4769
|
...requestUrl.searchParams.get("cwd") ? { cwd: requestUrl.searchParams.get("cwd") } : {},
|
|
4486
4770
|
...limit ? { limit: Number(limit) } : {}
|
|
4487
4771
|
}) });
|
|
4488
|
-
} else if (
|
|
4489
|
-
else if (
|
|
4490
|
-
gateway.deleteSaved(
|
|
4491
|
-
|
|
4492
|
-
} else if (
|
|
4493
|
-
else if (
|
|
4494
|
-
else if (
|
|
4495
|
-
else
|
|
4772
|
+
} else if (id3 && parts.length === 2 && method === "GET") json2(res, 200, gateway.loadSaved(id3));
|
|
4773
|
+
else if (id3 && parts.length === 2 && method === "DELETE") {
|
|
4774
|
+
gateway.deleteSaved(id3);
|
|
4775
|
+
json2(res, 200, { ok: true });
|
|
4776
|
+
} else if (id3 && parts[2] === "archive" && method === "POST") json2(res, 200, { session: gateway.archiveSaved(id3, input) });
|
|
4777
|
+
else if (id3 && parts[2] === "resume" && method === "POST") json2(res, 201, { session: await gateway.resume(id3, input) });
|
|
4778
|
+
else if (id3 && parts[2] === "export" && method === "GET") json2(res, 200, gateway.exportSaved(id3));
|
|
4779
|
+
else json2(res, 405, { error: "Method not allowed." });
|
|
4496
4780
|
return true;
|
|
4497
4781
|
}
|
|
4498
4782
|
if (parts[0] !== "sessions" || !parts[1]) return false;
|
|
4499
|
-
const
|
|
4783
|
+
const id2 = parts[1];
|
|
4500
4784
|
const action = parts[2];
|
|
4501
|
-
if (
|
|
4785
|
+
if (action === "browser" && parts[3] === "views" && (parts.length === 4 || parts.length === 6 && parts[5] === "stream")) {
|
|
4786
|
+
gateway.get(id2);
|
|
4787
|
+
if (team?.auth.enabled) {
|
|
4788
|
+
const sessionId = getSessionId(req);
|
|
4789
|
+
if (!sessionId || !team.collaboration) throw new WebRequestError("Sign in to this workspace.", 403);
|
|
4790
|
+
if (!team.collaboration.hasConversation(id2)) team.collaboration.register(id2, sessionId);
|
|
4791
|
+
team.collaboration.snapshot(id2, sessionId);
|
|
4792
|
+
}
|
|
4793
|
+
await handleBrowserLiveRequest(req, res, requestUrl, id2, parts[4]);
|
|
4794
|
+
return true;
|
|
4795
|
+
}
|
|
4796
|
+
if (!action && method === "GET") json2(res, 200, { snapshot: gateway.get(id2) });
|
|
4502
4797
|
else if (!action && method === "DELETE") {
|
|
4503
|
-
workflows.cancelSession(
|
|
4504
|
-
await gateway.close(
|
|
4505
|
-
|
|
4798
|
+
workflows.cancelSession(id2);
|
|
4799
|
+
await gateway.close(id2);
|
|
4800
|
+
json2(res, 200, { ok: true });
|
|
4506
4801
|
} else if (action === "events" && method === "GET") {
|
|
4507
|
-
|
|
4802
|
+
json2(res, 200, gateway.eventsAfter(id2, consoleEventsAfter(requestUrl.searchParams.get("after"))));
|
|
4508
4803
|
} else if (action === "messages" && method === "POST") {
|
|
4509
|
-
const
|
|
4510
|
-
|
|
4511
|
-
|
|
4804
|
+
const message = author && input && typeof input === "object" && !("workerId" in input) && (!("mode" in input) || input.mode === "send") ? { ...input, mode: "queue" } : input;
|
|
4805
|
+
const { queuedMessageId, ...session } = await gateway.send(id2, message, author);
|
|
4806
|
+
json2(res, 202, { session, ...queuedMessageId ? { queuedMessageId } : {} });
|
|
4807
|
+
} else if (action === "cancel" && method === "POST") json2(res, 200, { session: await gateway.cancel(id2) });
|
|
4512
4808
|
else if (action === "connection" && parts[3] === "reload" && parts.length === 4 && method === "POST") {
|
|
4513
4809
|
external_exports.object({}).strict().parse(input ?? {});
|
|
4514
|
-
|
|
4515
|
-
} else if (action === "configuration" && method === "PATCH")
|
|
4516
|
-
else if (action === "clear" && method === "POST")
|
|
4517
|
-
else if (action === "continue" && method === "POST")
|
|
4518
|
-
else if (action === "harness" && method === "POST")
|
|
4519
|
-
else if (action === "save" && method === "POST")
|
|
4810
|
+
json2(res, 200, { session: await gateway.reloadConnection(id2) });
|
|
4811
|
+
} else if (action === "configuration" && method === "PATCH") json2(res, 200, { session: await gateway.configure(id2, input) });
|
|
4812
|
+
else if (action === "clear" && method === "POST") json2(res, 200, { session: await gateway.clear(id2) });
|
|
4813
|
+
else if (action === "continue" && method === "POST") json2(res, 202, { session: await gateway.continue(id2, input, author) });
|
|
4814
|
+
else if (action === "harness" && method === "POST") json2(res, 200, await gateway.harness(id2, input));
|
|
4815
|
+
else if (action === "save" && method === "POST") json2(res, 200, { session: gateway.save(id2) });
|
|
4520
4816
|
else if (action === "archive" && method === "POST") {
|
|
4521
|
-
workflows.cancelSession(
|
|
4522
|
-
|
|
4817
|
+
workflows.cancelSession(id2);
|
|
4818
|
+
json2(res, 200, { session: await gateway.archive(id2) });
|
|
4523
4819
|
} else if (action === "delete" && method === "POST") {
|
|
4524
|
-
workflows.cancelSession(
|
|
4525
|
-
|
|
4526
|
-
} else if (action === "export" && method === "GET")
|
|
4527
|
-
else if (action === "queue" &&
|
|
4528
|
-
|
|
4820
|
+
workflows.cancelSession(id2);
|
|
4821
|
+
json2(res, 200, await gateway.delete(id2));
|
|
4822
|
+
} else if (action === "export" && method === "GET") json2(res, 200, gateway.export(id2));
|
|
4823
|
+
else if (action === "queue" && parts[3] && parts[4] === "send" && parts.length === 5 && method === "POST") {
|
|
4824
|
+
external_exports.object({}).strict().parse(input ?? {});
|
|
4825
|
+
json2(res, 202, { session: gateway.sendQueuedNow(id2, parts[3]) });
|
|
4826
|
+
} else if (action === "queue" && method === "DELETE") json2(res, 200, { session: gateway.removeQueued(id2, parts[3]) });
|
|
4827
|
+
else if (action === "decisions" && parts[3] && method === "POST") json2(res, 200, { session: gateway.resolveDecision(id2, parts[3], input) });
|
|
4529
4828
|
else if (action === "workers") {
|
|
4530
4829
|
const workerId = parts[3];
|
|
4531
|
-
if (!workerId && method === "GET")
|
|
4532
|
-
else if (workerId === "stop" && method === "POST")
|
|
4533
|
-
else if (workerId && parts[4] === "export" && method === "GET")
|
|
4534
|
-
else if (workerId && parts[4] === "stop" && method === "POST")
|
|
4830
|
+
if (!workerId && method === "GET") json2(res, 200, { workers: gateway.workers(id2) });
|
|
4831
|
+
else if (workerId === "stop" && method === "POST") json2(res, 200, { session: await gateway.drainWorkers(id2) });
|
|
4832
|
+
else if (workerId && parts[4] === "export" && method === "GET") json2(res, 200, gateway.exportWorker(id2, workerId));
|
|
4833
|
+
else if (workerId && parts[4] === "stop" && method === "POST") json2(res, 200, { worker: await gateway.stopWorker(id2, workerId) });
|
|
4535
4834
|
else if (workerId && parts[4] === "messages" && method === "POST") {
|
|
4536
4835
|
const body = external_exports.object({ text: external_exports.unknown() }).strict().parse(input);
|
|
4537
|
-
|
|
4538
|
-
} else
|
|
4539
|
-
} else
|
|
4836
|
+
json2(res, 202, { session: gateway.sendWorker(id2, workerId, body.text, author) });
|
|
4837
|
+
} else json2(res, 405, { error: "Method not allowed." });
|
|
4838
|
+
} else json2(res, 405, { error: "Method not allowed." });
|
|
4540
4839
|
return true;
|
|
4541
4840
|
} catch (error2) {
|
|
4542
4841
|
if (error2 instanceof external_exports.ZodError) {
|
|
4543
|
-
|
|
4842
|
+
json2(res, 400, { error: "Invalid console request payload." });
|
|
4544
4843
|
return true;
|
|
4545
4844
|
}
|
|
4546
4845
|
if (error2 instanceof ConsoleGatewayError) {
|
|
4547
|
-
|
|
4846
|
+
json2(res, error2.statusCode, { error: error2.message });
|
|
4548
4847
|
return true;
|
|
4549
4848
|
}
|
|
4550
4849
|
throw error2;
|
|
@@ -4572,10 +4871,10 @@ data: ${JSON.stringify(event)}
|
|
|
4572
4871
|
`);
|
|
4573
4872
|
}
|
|
4574
4873
|
async function handleApiRequest(req, res, pathname, dbPath, controlToken, engineBearer) {
|
|
4575
|
-
const { osecDB: osecDB2 } = await import("./dist-
|
|
4874
|
+
const { osecDB: osecDB2 } = await import("./dist-QLMPTFT7.js");
|
|
4576
4875
|
if (isPresentationEventsStreamPath(pathname)) {
|
|
4577
4876
|
if (req.method !== "GET") {
|
|
4578
|
-
|
|
4877
|
+
json2(res, 405, { error: "Method not allowed" });
|
|
4579
4878
|
return true;
|
|
4580
4879
|
}
|
|
4581
4880
|
const db = new osecDB2(dbPath);
|
|
@@ -4619,27 +4918,27 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4619
4918
|
const controlPath = parseControlPath(pathname);
|
|
4620
4919
|
if (controlPath) {
|
|
4621
4920
|
if (req.method !== "POST") {
|
|
4622
|
-
|
|
4921
|
+
json2(res, 405, { error: "Method not allowed" });
|
|
4623
4922
|
return true;
|
|
4624
4923
|
}
|
|
4625
4924
|
if (!requireControlToken(req, res, controlToken, engineBearer)) {
|
|
4626
4925
|
return true;
|
|
4627
4926
|
}
|
|
4628
4927
|
if (controlPath.action === "recover-stale-workers") {
|
|
4629
|
-
const { recoverStaleWorkers } = await import("./orchestrate-
|
|
4928
|
+
const { recoverStaleWorkers } = await import("./orchestrate-ZF6ZTSIQ.js");
|
|
4630
4929
|
const body = await readJson(req);
|
|
4631
4930
|
const recovered = recoverStaleWorkers(
|
|
4632
4931
|
dbPath,
|
|
4633
4932
|
typeof body.staleAfterMs === "number" ? body.staleAfterMs : 3e4
|
|
4634
4933
|
);
|
|
4635
|
-
|
|
4934
|
+
json2(res, 200, { ok: true, recovered });
|
|
4636
4935
|
return true;
|
|
4637
4936
|
}
|
|
4638
4937
|
if (controlPath.action === "prune-stopped-workers") {
|
|
4639
4938
|
const db = new osecDB2(dbPath);
|
|
4640
4939
|
try {
|
|
4641
4940
|
const deleted = db.deleteWorkersByStatus("stopped");
|
|
4642
|
-
|
|
4941
|
+
json2(res, 200, { ok: true, deleted });
|
|
4643
4942
|
} finally {
|
|
4644
4943
|
db.close();
|
|
4645
4944
|
}
|
|
@@ -4653,7 +4952,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4653
4952
|
(worker) => worker.status !== "stopped" && Date.now() - Date.parse(worker.heartbeatAt) < 2e4
|
|
4654
4953
|
);
|
|
4655
4954
|
if (activeWorkers.length > 0) {
|
|
4656
|
-
|
|
4955
|
+
json2(res, 409, { error: "An active daemon is already heartbeating. Stop it before starting another local daemon." });
|
|
4657
4956
|
return true;
|
|
4658
4957
|
}
|
|
4659
4958
|
} finally {
|
|
@@ -4664,21 +4963,21 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4664
4963
|
label: typeof body.label === "string" && body.label.trim() ? body.label.trim() : "control-plane-1",
|
|
4665
4964
|
pollIntervalMs: typeof body.pollIntervalMs === "number" ? body.pollIntervalMs : 2e3
|
|
4666
4965
|
});
|
|
4667
|
-
|
|
4966
|
+
json2(res, 200, { ok: true, ...started });
|
|
4668
4967
|
return true;
|
|
4669
4968
|
}
|
|
4670
4969
|
if (controlPath.action === "stop-daemon") {
|
|
4671
4970
|
const stopped = stopDaemonWorkers(osecDB2, dbPath);
|
|
4672
|
-
|
|
4971
|
+
json2(res, 200, { ok: true, stopped });
|
|
4673
4972
|
return true;
|
|
4674
4973
|
}
|
|
4675
4974
|
if (controlPath.action === "reset-database") {
|
|
4676
|
-
const { resetOsecDatabase } = await import("./dist-
|
|
4677
|
-
const { seedVerificationWorkbench } = await import("./db-
|
|
4975
|
+
const { resetOsecDatabase } = await import("./dist-QLMPTFT7.js");
|
|
4976
|
+
const { seedVerificationWorkbench } = await import("./db-FU3OIGQK.js");
|
|
4678
4977
|
const body = await readJson(req);
|
|
4679
4978
|
const seed = typeof body.seed === "string" ? body.seed.trim().toLowerCase() : "verification";
|
|
4680
4979
|
if (!["verification", "empty"].includes(seed)) {
|
|
4681
|
-
|
|
4980
|
+
json2(res, 400, { error: `Unsupported seed preset: ${seed}` });
|
|
4682
4981
|
return true;
|
|
4683
4982
|
}
|
|
4684
4983
|
const activeWorkerDb = new osecDB2(dbPath);
|
|
@@ -4687,7 +4986,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4687
4986
|
(worker) => ["idle", "claiming", "running", "sleeping"].includes(worker.status) && Date.now() - Date.parse(worker.heartbeatAt) < 2e4 && (worker.pid ? isLiveLocalPid(worker.pid) : true)
|
|
4688
4987
|
);
|
|
4689
4988
|
if (hasActiveWorker) {
|
|
4690
|
-
|
|
4989
|
+
json2(res, 409, { error: "Stop active orchestration daemons before resetting the local database." });
|
|
4691
4990
|
return true;
|
|
4692
4991
|
}
|
|
4693
4992
|
} finally {
|
|
@@ -4697,7 +4996,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4697
4996
|
const resetDb = new osecDB2(dbPath);
|
|
4698
4997
|
try {
|
|
4699
4998
|
const seeded = seed === "verification" ? seedVerificationWorkbench(resetDb) : { scans: 0, families: 0, workers: 0 };
|
|
4700
|
-
|
|
4999
|
+
json2(res, 200, { ok: true, path, seed, ...seeded });
|
|
4701
5000
|
} finally {
|
|
4702
5001
|
resetDb.close();
|
|
4703
5002
|
}
|
|
@@ -4733,7 +5032,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4733
5032
|
});
|
|
4734
5033
|
const casesById = new Map(cases.map((item) => [item.id, item]));
|
|
4735
5034
|
const workItemsById = new Map(workItems.map((item) => [item.id, item]));
|
|
4736
|
-
|
|
5035
|
+
json2(res, 200, {
|
|
4737
5036
|
scans: scans.map(summarizeScan),
|
|
4738
5037
|
cases,
|
|
4739
5038
|
groups,
|
|
@@ -4749,7 +5048,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4749
5048
|
const db = new osecDB2(dbPath);
|
|
4750
5049
|
try {
|
|
4751
5050
|
const scans = db.listScans(100);
|
|
4752
|
-
|
|
5051
|
+
json2(res, 200, { scans: scans.map(summarizeScan) });
|
|
4753
5052
|
} finally {
|
|
4754
5053
|
db.close();
|
|
4755
5054
|
}
|
|
@@ -4783,7 +5082,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4783
5082
|
presentation
|
|
4784
5083
|
};
|
|
4785
5084
|
});
|
|
4786
|
-
|
|
5085
|
+
json2(res, 200, { events });
|
|
4787
5086
|
} finally {
|
|
4788
5087
|
db.close();
|
|
4789
5088
|
}
|
|
@@ -4795,7 +5094,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4795
5094
|
try {
|
|
4796
5095
|
const scan = db.getScan(scanPath.scanId);
|
|
4797
5096
|
if (!scan) {
|
|
4798
|
-
|
|
5097
|
+
json2(res, 404, { error: "Scan not found" });
|
|
4799
5098
|
return true;
|
|
4800
5099
|
}
|
|
4801
5100
|
if (scanPath.suffix === "events") {
|
|
@@ -4803,7 +5102,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4803
5102
|
...event,
|
|
4804
5103
|
payload: parsePayload(event.payload)
|
|
4805
5104
|
}));
|
|
4806
|
-
|
|
5105
|
+
json2(res, 200, { scan: summarizeScan(scan), events });
|
|
4807
5106
|
return true;
|
|
4808
5107
|
}
|
|
4809
5108
|
if (scanPath.suffix === "findings") {
|
|
@@ -4813,7 +5112,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4813
5112
|
scanIds: [scanPath.scanId],
|
|
4814
5113
|
status: "running"
|
|
4815
5114
|
});
|
|
4816
|
-
|
|
5115
|
+
json2(res, 200, {
|
|
4817
5116
|
scan: summarizeScan(scan),
|
|
4818
5117
|
findings,
|
|
4819
5118
|
groups: groupFindings(
|
|
@@ -4824,7 +5123,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4824
5123
|
});
|
|
4825
5124
|
return true;
|
|
4826
5125
|
}
|
|
4827
|
-
|
|
5126
|
+
json2(res, 200, { scan: summarizeScan(scan) });
|
|
4828
5127
|
} finally {
|
|
4829
5128
|
db.close();
|
|
4830
5129
|
}
|
|
@@ -4844,7 +5143,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4844
5143
|
normalizeTriageStatus(body.triageStatus),
|
|
4845
5144
|
typeof body.triageNote === "string" ? body.triageNote : void 0
|
|
4846
5145
|
);
|
|
4847
|
-
|
|
5146
|
+
json2(res, 200, { ok: true });
|
|
4848
5147
|
return true;
|
|
4849
5148
|
}
|
|
4850
5149
|
if (req.method === "POST" && familyPath.action === "workflow") {
|
|
@@ -4857,12 +5156,12 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4857
5156
|
normalizeWorkflowStatus(body.workflowStatus),
|
|
4858
5157
|
typeof body.workflowAssignee === "string" ? body.workflowAssignee.trim() : null
|
|
4859
5158
|
);
|
|
4860
|
-
|
|
5159
|
+
json2(res, 200, { ok: true });
|
|
4861
5160
|
return true;
|
|
4862
5161
|
}
|
|
4863
5162
|
const rows = db.getRelatedFindings(familyPath.fingerprint).map(normalizeFinding);
|
|
4864
5163
|
if (rows.length === 0) {
|
|
4865
|
-
|
|
5164
|
+
json2(res, 404, { error: "Not found" });
|
|
4866
5165
|
return true;
|
|
4867
5166
|
}
|
|
4868
5167
|
const verdicts = db.listVerdicts(rows.map((row) => row.id));
|
|
@@ -4904,7 +5203,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4904
5203
|
sessions,
|
|
4905
5204
|
events: allEvents
|
|
4906
5205
|
});
|
|
4907
|
-
|
|
5206
|
+
json2(res, 200, {
|
|
4908
5207
|
consoleCommand: buildFindingConsoleCommand(rows[0], dbPath),
|
|
4909
5208
|
fingerprint: familyPath.fingerprint,
|
|
4910
5209
|
case: familyCase,
|
|
@@ -4972,7 +5271,7 @@ function registerDashboardCommand(program) {
|
|
|
4972
5271
|
const timeCapMs = opts.engineTimeCap === void 0 ? 6e5 : Number(opts.engineTimeCap);
|
|
4973
5272
|
const costCapUsd = opts.engineCostCap === void 0 ? 5 : Number(opts.engineCostCap);
|
|
4974
5273
|
if (!Number.isInteger(timeCapMs) || timeCapMs < 1 || timeCapMs > 864e5 || !Number.isFinite(costCapUsd) || costCapUsd <= 0 || costCapUsd > 1e3) throw new Error("Engine limits must be positive, with time at most 86400000 ms and cost at most $1000.");
|
|
4975
|
-
const controlToken =
|
|
5274
|
+
const controlToken = randomUUID9();
|
|
4976
5275
|
let teamAuth = new TeamAuth({ configPath: teamConfigPath, origin });
|
|
4977
5276
|
let teamDirectory = teamAuth.enabled ? join7(homeStateDir(), "teams", createHash8("sha256").update(teamAuth.workspaceId).digest("hex")) : void 0;
|
|
4978
5277
|
if (teamDirectory) {
|
|
@@ -4982,9 +5281,51 @@ function registerDashboardCommand(program) {
|
|
|
4982
5281
|
}
|
|
4983
5282
|
let collaboration = teamAuth.enabled ? new CollaborationService({ workspaceId: teamAuth.workspaceId, stateDir: join7(teamDirectory, "collaboration"), resolveSession: (sessionId) => teamAuth.resolveSessionId(sessionId) }) : void 0;
|
|
4984
5283
|
assertLocalEngineAvailable(opts.dbPath);
|
|
4985
|
-
const capabilities = ["sessions", "workflows", "schedules", "approvals", "events", "workspaces", "artifacts", "model-connections", "operator-services", "process-controls", "learning", "workflow-engine"];
|
|
5284
|
+
const capabilities = ["sessions", "workflows", "schedules", "approvals", "events", "workspaces", "artifacts", "model-connections", "provider-relay", "operator-services", "process-controls", "learning", "workflow-engine"];
|
|
4986
5285
|
let localHandshake = createBackendHandshake(opts.dbPath, capabilities);
|
|
4987
5286
|
let backends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake });
|
|
5287
|
+
let modelBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake });
|
|
5288
|
+
const modelRegistry = {
|
|
5289
|
+
handshake: (id2) => {
|
|
5290
|
+
modelBackends.reload();
|
|
5291
|
+
return modelBackends.handshake(id2);
|
|
5292
|
+
},
|
|
5293
|
+
request: (...args) => {
|
|
5294
|
+
modelBackends.reload();
|
|
5295
|
+
return modelBackends.request(...args);
|
|
5296
|
+
}
|
|
5297
|
+
};
|
|
5298
|
+
let localProviderSessions = new LocalProviderSessions({ registry: modelRegistry });
|
|
5299
|
+
const hostRepairs = /* @__PURE__ */ new Map();
|
|
5300
|
+
const hostRepairAbort = new AbortController();
|
|
5301
|
+
const repairManagedHost = (backendId, signal) => {
|
|
5302
|
+
const pending = hostRepairs.get(backendId);
|
|
5303
|
+
if (pending) return pending;
|
|
5304
|
+
const compiled = import.meta.url.startsWith("file:///$bunfs/") || import.meta.url.startsWith("/$bunfs/");
|
|
5305
|
+
const args = [...compiled ? [] : [resolveCliEntrypoint()], "worker", "repair", backendId];
|
|
5306
|
+
const task = new Promise((resolveRepair, reject) => {
|
|
5307
|
+
execFile2(process.execPath, args, { timeout: 75e3, maxBuffer: 65536, signal: signal ? AbortSignal.any([signal, hostRepairAbort.signal]) : hostRepairAbort.signal }, (error2, stdout) => {
|
|
5308
|
+
if (error2) {
|
|
5309
|
+
reject(new WebRequestError("This host could not reconnect. Check its SSH connection and managed engine installation.", 409));
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5312
|
+
try {
|
|
5313
|
+
resolveRepair(JSON.parse(stdout));
|
|
5314
|
+
} catch {
|
|
5315
|
+
reject(new WebRequestError("Host reconnect returned an invalid result.", 502));
|
|
5316
|
+
}
|
|
5317
|
+
});
|
|
5318
|
+
}).finally(() => {
|
|
5319
|
+
hostRepairs.delete(backendId);
|
|
5320
|
+
});
|
|
5321
|
+
hostRepairs.set(backendId, task);
|
|
5322
|
+
return task;
|
|
5323
|
+
};
|
|
5324
|
+
let stopHostReconnect;
|
|
5325
|
+
const watchManagedHosts = () => startManagedHostReconnect({ getRegistry: () => backends, repair: repairManagedHost, onRecovered: async (id2) => {
|
|
5326
|
+
backends.reload();
|
|
5327
|
+
await backends.handshake(id2);
|
|
5328
|
+
} });
|
|
4988
5329
|
let engine;
|
|
4989
5330
|
let startupGateway;
|
|
4990
5331
|
let startupOperator;
|
|
@@ -4995,8 +5336,9 @@ function registerDashboardCommand(program) {
|
|
|
4995
5336
|
try {
|
|
4996
5337
|
const { path: assetDir, cleanup: cleanupAssetDir } = resolveDashboardAssetDir(opts.assetDir);
|
|
4997
5338
|
startupAssetCleanup = cleanupAssetDir;
|
|
4998
|
-
const buildServices = (auth, dbPath = opts.dbPath, stateDir = teamDirectory) => createWorkspaceServices({ auth, dbPath, stateDir, workspace: opts.engineWorkspace, scopePath: opts.engineScope, target: opts.engineTarget, allowApply: opts.engineAllowApply, controlToken, engineBearer, timeCapMs, costCapUsd });
|
|
5339
|
+
const buildServices = (auth, dbPath = opts.dbPath, stateDir = teamDirectory, relayEpoch = localHandshake.serverInstanceId) => createWorkspaceServices({ relayEpoch, auth, dbPath, stateDir, workspace: opts.engineWorkspace, scopePath: opts.engineScope, target: opts.engineTarget, allowApply: opts.engineAllowApply, controlToken, engineBearer, timeCapMs, costCapUsd });
|
|
4999
5340
|
let services = await buildServices(teamAuth);
|
|
5341
|
+
stopHostReconnect = watchManagedHosts();
|
|
5000
5342
|
let { skills, gateway: consoleGateway, engagements, operator, workflows, triggers, github } = services;
|
|
5001
5343
|
engine = services.engine;
|
|
5002
5344
|
startupGateway = consoleGateway;
|
|
@@ -5015,11 +5357,11 @@ function registerDashboardCommand(program) {
|
|
|
5015
5357
|
currentAuth: () => teamAuth,
|
|
5016
5358
|
activate: async (prepared) => {
|
|
5017
5359
|
if (consoleGateway.hasActiveTurns() || workflows.hasActiveWork()) throw new TeamAuthError("Finish active work before creating a team workspace.", 409);
|
|
5018
|
-
const
|
|
5360
|
+
const nextHandshake = createBackendHandshake(prepared.dbPath, capabilities);
|
|
5361
|
+
const next = await buildServices(prepared.auth, prepared.dbPath, prepared.stateDir, nextHandshake.serverInstanceId);
|
|
5019
5362
|
let nextBackends;
|
|
5020
5363
|
let nextUnregister;
|
|
5021
5364
|
try {
|
|
5022
|
-
const nextHandshake = createBackendHandshake(prepared.dbPath, capabilities);
|
|
5023
5365
|
nextBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake: nextHandshake });
|
|
5024
5366
|
const nextAccess = new FindingsAccessTokenStore({ workspaceId: prepared.workspaceId, stateDir: join7(prepared.stateDir, "findings-api-access") });
|
|
5025
5367
|
const nextRead = new FindingsReadApi({ workspaceId: prepared.workspaceId, dbPath: prepared.dbPath });
|
|
@@ -5028,6 +5370,9 @@ function registerDashboardCommand(program) {
|
|
|
5028
5370
|
if (consoleGateway.hasActiveTurns() || workflows.hasActiveWork()) throw new TeamAuthError("Finish active work before creating a team workspace.", 409);
|
|
5029
5371
|
await services.dispose();
|
|
5030
5372
|
for (const response of activeResponses) if (response !== setupResponse) response.end();
|
|
5373
|
+
stopHostReconnect?.();
|
|
5374
|
+
await localProviderSessions.dispose();
|
|
5375
|
+
modelBackends.dispose();
|
|
5031
5376
|
backends.dispose();
|
|
5032
5377
|
unregisterLocalEngine?.();
|
|
5033
5378
|
services = next;
|
|
@@ -5037,6 +5382,8 @@ function registerDashboardCommand(program) {
|
|
|
5037
5382
|
collaboration = nextCollaboration;
|
|
5038
5383
|
localHandshake = nextHandshake;
|
|
5039
5384
|
backends = nextBackends;
|
|
5385
|
+
modelBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake: nextHandshake });
|
|
5386
|
+
localProviderSessions = new LocalProviderSessions({ registry: modelRegistry });
|
|
5040
5387
|
unregisterLocalEngine = nextUnregister;
|
|
5041
5388
|
({ skills, gateway: consoleGateway, engagements, operator, workflows, triggers, github } = next);
|
|
5042
5389
|
engine = next.engine;
|
|
@@ -5047,6 +5394,7 @@ function registerDashboardCommand(program) {
|
|
|
5047
5394
|
startupWorkflows = workflows;
|
|
5048
5395
|
startupTriggers = triggers;
|
|
5049
5396
|
startupGitHub = github;
|
|
5397
|
+
stopHostReconnect = watchManagedHosts();
|
|
5050
5398
|
} catch (cause) {
|
|
5051
5399
|
nextBackends?.dispose();
|
|
5052
5400
|
nextUnregister?.();
|
|
@@ -5064,12 +5412,12 @@ function registerDashboardCommand(program) {
|
|
|
5064
5412
|
try {
|
|
5065
5413
|
authorizeWebRequest(req, origin);
|
|
5066
5414
|
} catch (error2) {
|
|
5067
|
-
|
|
5415
|
+
json2(res, error2 instanceof WebRequestError ? error2.statusCode : 403, { error: error2 instanceof Error ? error2.message : "Invalid request origin." });
|
|
5068
5416
|
return;
|
|
5069
5417
|
}
|
|
5070
5418
|
let requestUrl = new URL2(req.url ?? "/", origin);
|
|
5071
5419
|
const requestId = BackendRequestIdSchema.safeParse(req.headers["x-0-request-id"]);
|
|
5072
|
-
res.setHeader("X-0-Request-ID", requestId.success ? requestId.data :
|
|
5420
|
+
res.setHeader("X-0-Request-ID", requestId.success ? requestId.data : randomUUID9());
|
|
5073
5421
|
try {
|
|
5074
5422
|
if (requestUrl.pathname.startsWith("/api/")) {
|
|
5075
5423
|
const findingsCredential = findingsAccess.authenticate(req, requestUrl);
|
|
@@ -5078,7 +5426,7 @@ function registerDashboardCommand(program) {
|
|
|
5078
5426
|
if (!findingsCredential) throw new FindingsAccessError("A findings API key is required in the Authorization header.", 401);
|
|
5079
5427
|
const reply = handleFindingsReadRequest(req, requestUrl, findingsRead);
|
|
5080
5428
|
if (!reply) throw new FindingsAccessError("Findings API route was not found.", 404);
|
|
5081
|
-
|
|
5429
|
+
json2(res, reply.status, reply.data);
|
|
5082
5430
|
return;
|
|
5083
5431
|
}
|
|
5084
5432
|
const ssoNavigation = requestUrl.pathname === "/api/team/auth/sso" || requestUrl.pathname === "/api/team/auth/callback";
|
|
@@ -5089,7 +5437,7 @@ function registerDashboardCommand(program) {
|
|
|
5089
5437
|
const reply = await handleTeamSetupRequest(req, requestUrl, setup, () => readJson(req));
|
|
5090
5438
|
if (reply) {
|
|
5091
5439
|
for (const [name, value] of Object.entries(reply.headers ?? {})) res.setHeader(name, value);
|
|
5092
|
-
|
|
5440
|
+
json2(res, reply.status, reply.data);
|
|
5093
5441
|
return;
|
|
5094
5442
|
}
|
|
5095
5443
|
} finally {
|
|
@@ -5098,12 +5446,12 @@ function registerDashboardCommand(program) {
|
|
|
5098
5446
|
}
|
|
5099
5447
|
if (setup.status().pending && requestUrl.pathname !== "/api/team/session") throw new WebRequestError("Workspace setup is in progress. Try again shortly.", 503);
|
|
5100
5448
|
if (handleTeamPresenceStream(req, res, requestUrl, teamAuth, collaboration)) return;
|
|
5101
|
-
const teamReply = await handleTeamRequest(req, requestUrl, teamAuth, collaboration, consoleGateway, () => readJson(req), async (kind,
|
|
5449
|
+
const teamReply = await handleTeamRequest(req, requestUrl, teamAuth, collaboration, consoleGateway, () => readJson(req), async (kind, id2) => {
|
|
5102
5450
|
if (kind === "report") {
|
|
5103
|
-
engagements.get(
|
|
5451
|
+
engagements.get(id2);
|
|
5104
5452
|
return;
|
|
5105
5453
|
}
|
|
5106
|
-
const definition = await workflows.handle(`/api/console/workflow-definitions/${encodeURIComponent(
|
|
5454
|
+
const definition = await workflows.handle(`/api/console/workflow-definitions/${encodeURIComponent(id2)}`, "GET", void 0, new URLSearchParams());
|
|
5107
5455
|
if (!definition || definition.status !== 200) throw new WebRequestError("Workflow was not found.", 404);
|
|
5108
5456
|
});
|
|
5109
5457
|
if (teamReply) {
|
|
@@ -5111,7 +5459,7 @@ function registerDashboardCommand(program) {
|
|
|
5111
5459
|
if (teamReply.redirect) {
|
|
5112
5460
|
res.writeHead(teamReply.status, { Location: teamReply.redirect, "Cache-Control": "no-store" });
|
|
5113
5461
|
res.end();
|
|
5114
|
-
} else
|
|
5462
|
+
} else json2(res, teamReply.status, teamReply.data);
|
|
5115
5463
|
return;
|
|
5116
5464
|
}
|
|
5117
5465
|
const bearer = req.headers.authorization;
|
|
@@ -5122,11 +5470,41 @@ function registerDashboardCommand(program) {
|
|
|
5122
5470
|
if (expectedEngineId !== void 0 && expectedEngineId !== localHandshake.engineId) throw new WebRequestError("Request belongs to a different engine identity.", 409);
|
|
5123
5471
|
const findingsAccessReply = await handleFindingsAccessTokenRequest(req, requestUrl, teamAuth, findingsAccess, () => readJson(req));
|
|
5124
5472
|
if (findingsAccessReply) {
|
|
5125
|
-
|
|
5473
|
+
json2(res, findingsAccessReply.status, findingsAccessReply.data);
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
5476
|
+
const remoteSessionRoute = /^\/api\/console\/remote-sessions(?:\/([^/]+)\/(configuration|status))?$/.exec(requestUrl.pathname);
|
|
5477
|
+
if (remoteSessionRoute) {
|
|
5478
|
+
const ownerId = engineRequest ? "engine" : teamAuth.resolveSession(req)?.userId ?? "local";
|
|
5479
|
+
try {
|
|
5480
|
+
if (remoteSessionRoute[1] && remoteSessionRoute[2] === "status" && req.method === "GET") {
|
|
5481
|
+
const backendId = BackendDescriptorSchema.shape.id.parse(requestUrl.searchParams.get("backendId"));
|
|
5482
|
+
json2(res, 200, await localProviderSessions.status(backendId, decodeURIComponent(remoteSessionRoute[1]), ownerId));
|
|
5483
|
+
} else if (!remoteSessionRoute[1] && req.method === "POST") {
|
|
5484
|
+
const body = external_exports.object({ backendId: external_exports.string().min(1).max(128), configuration: external_exports.unknown() }).strict().parse(await readJson(req));
|
|
5485
|
+
json2(res, 201, await localProviderSessions.create(body.backendId, body.configuration, ownerId));
|
|
5486
|
+
} else if (remoteSessionRoute[1] && remoteSessionRoute[2] === "configuration" && req.method === "PATCH") {
|
|
5487
|
+
const body = external_exports.object({ backendId: external_exports.string().min(1).max(128), runtime: external_exports.unknown() }).strict().parse(await readJson(req));
|
|
5488
|
+
json2(res, 200, await localProviderSessions.configure(body.backendId, decodeURIComponent(remoteSessionRoute[1]), body.runtime, ownerId));
|
|
5489
|
+
} else throw new WebRequestError("Method not allowed.", 405);
|
|
5490
|
+
} catch (error2) {
|
|
5491
|
+
if (error2 instanceof LocalProviderSessionError) {
|
|
5492
|
+
json2(res, error2.statusCode, { error: error2.message });
|
|
5493
|
+
return;
|
|
5494
|
+
}
|
|
5495
|
+
throw error2;
|
|
5496
|
+
}
|
|
5497
|
+
return;
|
|
5498
|
+
}
|
|
5499
|
+
if (requestUrl.pathname === "/api/console/provider-relays" || requestUrl.pathname.startsWith("/api/console/provider-relays/")) {
|
|
5500
|
+
if (!engineRequest) throw new WebRequestError("Provider relay transport requires an authenticated engine connection.", 403);
|
|
5501
|
+
if (await handleRemoteProviderRelayRequest(req, res, requestUrl, services.remoteProviderRelays, () => readJson(req))) return;
|
|
5502
|
+
}
|
|
5503
|
+
const backendRoute = await handleBackendConnectionRequest(req, res, requestUrl, backends, (backendId, sessionId) => localProviderSessions.release(backendId, sessionId, engineRequest ? "engine" : teamAuth.resolveSession(req)?.userId ?? "local"));
|
|
5504
|
+
if (backendRoute.handled) {
|
|
5505
|
+
modelBackends.reload();
|
|
5126
5506
|
return;
|
|
5127
5507
|
}
|
|
5128
|
-
const backendRoute = await handleBackendConnectionRequest(req, res, requestUrl, backends);
|
|
5129
|
-
if (backendRoute.handled) return;
|
|
5130
5508
|
if (backendRoute.localPath) requestUrl = new URL2(backendRoute.localPath, origin);
|
|
5131
5509
|
const skillsRoute = requestUrl.pathname.match(/^\/api\/skills(?:\/(.*))?$/);
|
|
5132
5510
|
if (skillsRoute) {
|
|
@@ -5140,21 +5518,21 @@ function registerDashboardCommand(program) {
|
|
|
5140
5518
|
throw new WebRequestError("Invalid skill identifier.", 400);
|
|
5141
5519
|
}
|
|
5142
5520
|
const method = req.method ?? "GET";
|
|
5143
|
-
if (!path && method === "GET")
|
|
5144
|
-
else if (!path && method === "POST")
|
|
5145
|
-
else if (path === "import" && method === "POST")
|
|
5146
|
-
else if (path === "mounts" && method === "POST")
|
|
5521
|
+
if (!path && method === "GET") json2(res, 200, skills.list(access));
|
|
5522
|
+
else if (!path && method === "POST") json2(res, 201, { skill: skills.create(await readJson(req)) });
|
|
5523
|
+
else if (path === "import" && method === "POST") json2(res, 201, { skill: skills.import(await readJson(req, 12 * 1024 * 1024)) });
|
|
5524
|
+
else if (path === "mounts" && method === "POST") json2(res, 201, { mount: skills.mount(await readJson(req)) });
|
|
5147
5525
|
else if (path.startsWith("mounts/") && method === "DELETE") {
|
|
5148
5526
|
skills.unmount(decodedPath.slice(7));
|
|
5149
|
-
|
|
5150
|
-
} else if (path.endsWith("/copy") && method === "POST")
|
|
5527
|
+
json2(res, 200, { ok: true });
|
|
5528
|
+
} else if (path.endsWith("/copy") && method === "POST") json2(res, 201, { skill: skills.copy(decodedPath.slice(0, -5), await readJson(req), access) });
|
|
5151
5529
|
else if (path.endsWith("/export") && method === "GET") {
|
|
5152
|
-
const
|
|
5153
|
-
const bundle = skills.export(
|
|
5530
|
+
const id2 = decodedPath.slice(0, -7);
|
|
5531
|
+
const bundle = skills.export(id2);
|
|
5154
5532
|
res.setHeader("Content-Disposition", `attachment; filename="${bundle.name}.skill.json"`);
|
|
5155
|
-
|
|
5156
|
-
} else if (path && method === "GET")
|
|
5157
|
-
else if (path && method === "PUT")
|
|
5533
|
+
json2(res, 200, bundle);
|
|
5534
|
+
} else if (path && method === "GET") json2(res, 200, { skill: skills.get(decodedPath, access) });
|
|
5535
|
+
else if (path && method === "PUT") json2(res, 200, { skill: skills.update(decodedPath, await readJson(req), access) });
|
|
5158
5536
|
else throw new WebRequestError("Method not allowed.", 405);
|
|
5159
5537
|
return;
|
|
5160
5538
|
}
|
|
@@ -5162,34 +5540,34 @@ function registerDashboardCommand(program) {
|
|
|
5162
5540
|
if (engagementRoute) {
|
|
5163
5541
|
const member = teamAuth.resolveSession(req);
|
|
5164
5542
|
const engagementActor = member ? { userId: member.userId, displayName: member.displayName } : void 0;
|
|
5165
|
-
const
|
|
5543
|
+
const id2 = engagementRoute[1] ? decodeURIComponent(engagementRoute[1]) : void 0;
|
|
5166
5544
|
if (engagementRoute[2]) {
|
|
5167
5545
|
if (req.method !== "GET") throw new WebRequestError("Use GET to read an engagement report.", 405);
|
|
5168
5546
|
const format = requestUrl.searchParams.get("format") ?? "json";
|
|
5169
5547
|
if (format !== "json" && format !== "markdown") throw new WebRequestError("Use json or markdown for an engagement report.", 400);
|
|
5170
|
-
const report = engagements.report(
|
|
5548
|
+
const report = engagements.report(id2);
|
|
5171
5549
|
if (format === "json") {
|
|
5172
|
-
res.setHeader("Content-Disposition", `attachment; filename="engagement-${
|
|
5173
|
-
|
|
5550
|
+
res.setHeader("Content-Disposition", `attachment; filename="engagement-${id2}.json"`);
|
|
5551
|
+
json2(res, 200, report);
|
|
5174
5552
|
} else {
|
|
5175
|
-
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="engagement-${
|
|
5553
|
+
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="engagement-${id2}.md"`, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
5176
5554
|
res.end(engagementMarkdown(report));
|
|
5177
5555
|
}
|
|
5178
|
-
} else if (req.method === "GET")
|
|
5179
|
-
else if (req.method === "POST" && !
|
|
5556
|
+
} else if (req.method === "GET") json2(res, 200, id2 ? { engagement: engagements.get(id2) } : { engagements: engagements.list() });
|
|
5557
|
+
else if (req.method === "POST" && !id2) {
|
|
5180
5558
|
const engagement = engagements.create(await readJson(req), engagementActor);
|
|
5181
5559
|
collaboration?.notifyChanged("report", engagement.id);
|
|
5182
|
-
|
|
5183
|
-
} else if (req.method === "PATCH" &&
|
|
5184
|
-
const engagement = engagements.update(
|
|
5560
|
+
json2(res, 201, { engagement });
|
|
5561
|
+
} else if (req.method === "PATCH" && id2) {
|
|
5562
|
+
const engagement = engagements.update(id2, await readJson(req), engagementActor);
|
|
5185
5563
|
collaboration?.notifyChanged("report", engagement.id);
|
|
5186
|
-
|
|
5564
|
+
json2(res, 200, { engagement });
|
|
5187
5565
|
} else throw new WebRequestError("Method not allowed.", 405);
|
|
5188
5566
|
return;
|
|
5189
5567
|
}
|
|
5190
5568
|
if (requestUrl.pathname === "/api/workers/hosts") {
|
|
5191
5569
|
if (req.method !== "GET") throw new WebRequestError("Method not allowed.", 405);
|
|
5192
|
-
|
|
5570
|
+
json2(res, 200, { hosts: discoverSshHosts() });
|
|
5193
5571
|
return;
|
|
5194
5572
|
}
|
|
5195
5573
|
if (requestUrl.pathname === "/api/workers/managed") {
|
|
@@ -5202,7 +5580,15 @@ function registerDashboardCommand(program) {
|
|
|
5202
5580
|
return [];
|
|
5203
5581
|
}
|
|
5204
5582
|
});
|
|
5205
|
-
|
|
5583
|
+
json2(res, 200, { workers });
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
5586
|
+
if (requestUrl.pathname === "/api/control/repair-worker") {
|
|
5587
|
+
if (req.method !== "POST") throw new WebRequestError("Use POST to reconnect a host.", 405);
|
|
5588
|
+
const body = external_exports.object({ backendId: BackendDescriptorSchema.shape.id }).strict().parse(await readJson(req));
|
|
5589
|
+
const repaired = await repairManagedHost(body.backendId);
|
|
5590
|
+
backends.reload();
|
|
5591
|
+
json2(res, 200, repaired);
|
|
5206
5592
|
return;
|
|
5207
5593
|
}
|
|
5208
5594
|
const workerStop = requestUrl.pathname.match(/^\/api\/workers\/([^/]+)\/stop$/);
|
|
@@ -5224,7 +5610,7 @@ function registerDashboardCommand(program) {
|
|
|
5224
5610
|
}
|
|
5225
5611
|
});
|
|
5226
5612
|
});
|
|
5227
|
-
|
|
5613
|
+
json2(res, 200, result);
|
|
5228
5614
|
return;
|
|
5229
5615
|
}
|
|
5230
5616
|
if (requestUrl.pathname === "/api/workers/deploy") {
|
|
@@ -5257,7 +5643,7 @@ function registerDashboardCommand(program) {
|
|
|
5257
5643
|
});
|
|
5258
5644
|
});
|
|
5259
5645
|
backends.reload();
|
|
5260
|
-
|
|
5646
|
+
json2(res, 202, result);
|
|
5261
5647
|
return;
|
|
5262
5648
|
}
|
|
5263
5649
|
if (requestUrl.pathname === "/api/control/launch-run") {
|
|
@@ -5267,15 +5653,15 @@ function registerDashboardCommand(program) {
|
|
|
5267
5653
|
if (body.ensureDaemon) throw new WebRequestError("Engine runs do not require a separate orchestration daemon. Configure a workflow schedule for recurring work.", 400);
|
|
5268
5654
|
const session = await engine.invoke("create_session", { config: { target: body.target, title: `Assessment: ${body.target}` } });
|
|
5269
5655
|
const execution = await engine.invoke("start_assessment", { sessionId: session.id, target: body.target, plan: { goal: "unknown-vulnerabilities", depth: body.depth ?? "default", runCount: 1, executionMode: "sequential", timeCapMs, costCapUsd } });
|
|
5270
|
-
|
|
5656
|
+
json2(res, 202, { ok: true, engineOwned: true, sessionId: session.id, runId: execution.id, jobId: execution.jobId });
|
|
5271
5657
|
return;
|
|
5272
5658
|
}
|
|
5273
5659
|
if (requestUrl.pathname === "/api/workflow-engine/call") {
|
|
5274
5660
|
if (!engine) throw new WebRequestError("This engine does not admit persistent workflow calls.", 403);
|
|
5275
5661
|
if (req.method !== "POST") throw new WebRequestError("Method not allowed.", 405);
|
|
5276
|
-
const input = external_exports.object({ name: external_exports.string().min(1).max(128), args: external_exports.record(external_exports.unknown()) }).strict().parse(await readJson(req));
|
|
5662
|
+
const input = external_exports.object({ name: external_exports.string().min(1).max(128), args: external_exports.record(external_exports.string(), external_exports.unknown()) }).strict().parse(await readJson(req));
|
|
5277
5663
|
if (!engineRequest && typeof input.args.sessionId === "string") assertTeamConsoleMutation(req, input.args.sessionId, teamAuth, collaboration, consoleGateway);
|
|
5278
|
-
|
|
5664
|
+
json2(res, 200, await engine.invoke(input.name, input.args));
|
|
5279
5665
|
return;
|
|
5280
5666
|
}
|
|
5281
5667
|
const resumeScan = requestUrl.pathname.match(/^\/api\/scans\/([^/]+)\/resume$/);
|
|
@@ -5283,14 +5669,14 @@ function registerDashboardCommand(program) {
|
|
|
5283
5669
|
if (req.method !== "POST") throw new WebRequestError("Use POST to resume a scan.", 405);
|
|
5284
5670
|
const request = ScanResumeRequestSchema.parse(await readJson(req));
|
|
5285
5671
|
const result = await workflows.resumeScan(decodeURIComponent(resumeScan[1]), { ...request, timeCapMs: Math.min(request.timeCapMs ?? timeCapMs, timeCapMs), costCapUsd: Math.min(request.costCapUsd ?? costCapUsd, costCapUsd) });
|
|
5286
|
-
|
|
5672
|
+
json2(res, result.status, result.data);
|
|
5287
5673
|
return;
|
|
5288
5674
|
}
|
|
5289
5675
|
if (/^\/api\/findings\/[^/]+\/impact-assessment$/.test(requestUrl.pathname)) {
|
|
5290
5676
|
const input = req.method === "POST" || req.method === "DELETE" ? await readJson(req) : void 0;
|
|
5291
5677
|
const result = handleFindingImpactRequest(requestUrl.pathname, req.method ?? "GET", input, { dbPath: opts.dbPath });
|
|
5292
5678
|
if (result) {
|
|
5293
|
-
|
|
5679
|
+
json2(res, result.status, result.data);
|
|
5294
5680
|
return;
|
|
5295
5681
|
}
|
|
5296
5682
|
}
|
|
@@ -5309,7 +5695,7 @@ function registerDashboardCommand(program) {
|
|
|
5309
5695
|
const consoleHandled = await handleWebConsoleApiRequest(req, res, requestUrl, consoleGateway, operator, workflows, triggers, github, engineRequest ? void 0 : { auth: teamAuth, collaboration });
|
|
5310
5696
|
if (consoleHandled) return;
|
|
5311
5697
|
const handled = await handleApiRequest(req, res, requestUrl.pathname, opts.dbPath, controlToken, engineBearer);
|
|
5312
|
-
if (!handled)
|
|
5698
|
+
if (!handled) json2(res, 404, { error: "Not found" });
|
|
5313
5699
|
return;
|
|
5314
5700
|
}
|
|
5315
5701
|
if (devUrl) {
|
|
@@ -5330,12 +5716,12 @@ function registerDashboardCommand(program) {
|
|
|
5330
5716
|
return;
|
|
5331
5717
|
}
|
|
5332
5718
|
if (extname(requestUrl.pathname)) {
|
|
5333
|
-
|
|
5719
|
+
json2(res, 404, { error: "Asset not found" });
|
|
5334
5720
|
return;
|
|
5335
5721
|
}
|
|
5336
5722
|
sendFile(res, join7(assetDir, "index.html"), controlToken);
|
|
5337
5723
|
} catch (err) {
|
|
5338
|
-
|
|
5724
|
+
json2(res, errorStatusCode(err), { error: err instanceof Error ? err.message : "Web application request failed." });
|
|
5339
5725
|
}
|
|
5340
5726
|
});
|
|
5341
5727
|
let dashboardAssetsCleaned = false;
|
|
@@ -5383,19 +5769,31 @@ function registerDashboardCommand(program) {
|
|
|
5383
5769
|
const shutdown = () => {
|
|
5384
5770
|
if (shuttingDown) return;
|
|
5385
5771
|
shuttingDown = true;
|
|
5386
|
-
|
|
5387
|
-
|
|
5772
|
+
stopHostReconnect?.();
|
|
5773
|
+
hostRepairAbort.abort();
|
|
5774
|
+
services.remoteProviderRelays.dispose();
|
|
5775
|
+
void localProviderSessions.dispose().then(() => {
|
|
5776
|
+
modelBackends.dispose();
|
|
5777
|
+
backends.dispose();
|
|
5778
|
+
return engine?.dispose();
|
|
5779
|
+
}).then(() => triggers.dispose()).then(() => consoleGateway.closeAll()).then(() => workflows.dispose()).finally(() => {
|
|
5388
5780
|
operator.dispose();
|
|
5389
5781
|
github.dispose();
|
|
5782
|
+
for (const response of activeResponses) response.end();
|
|
5390
5783
|
server.close(() => {
|
|
5391
5784
|
cleanupDashboardAssets();
|
|
5392
5785
|
process.exit(0);
|
|
5393
5786
|
});
|
|
5787
|
+
server.closeAllConnections();
|
|
5394
5788
|
});
|
|
5395
5789
|
};
|
|
5396
5790
|
process.once("SIGINT", shutdown);
|
|
5397
5791
|
process.once("SIGTERM", shutdown);
|
|
5398
5792
|
} catch (error2) {
|
|
5793
|
+
stopHostReconnect?.();
|
|
5794
|
+
hostRepairAbort.abort();
|
|
5795
|
+
await localProviderSessions.dispose();
|
|
5796
|
+
modelBackends.dispose();
|
|
5399
5797
|
backends.dispose();
|
|
5400
5798
|
await startupTriggers?.dispose().catch(() => {
|
|
5401
5799
|
});
|