agent-whiteboard 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +13 -2
- package/dist/client/assets/abnfDiagram-VCTEODGH-CcuBIZ5h.js +1 -0
- package/dist/client/assets/{arc-4691b008.js → arc-Aq92EeUK.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CpMRoWIY.js +36 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Dee0m4lz.js +129 -0
- package/dist/client/assets/{bootstrap.min-21652813.js → bootstrap.min-UtnB-TxT.js} +1 -2
- package/dist/client/assets/c4Diagram-7LVT6UL2-D_qs3YTM.js +38 -0
- package/dist/client/assets/channel-9qggtmY_.js +1 -0
- package/dist/client/assets/{chunk-QZHKN3VN-0f554985.js → chunk-2Q5K7J3B-CBwQ-uNF.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-23d48b24.js → chunk-5VM5RSS4-yGvVHapR.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-1104c326.js → chunk-F27PBJKO-BzFUf98n.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-D-Zj1GaL.js +231 -0
- package/dist/client/assets/{chunk-4BX2VUAB-3c93511d.js → chunk-JWPE2WC7-C8Aj4MVR.js} +1 -1
- package/dist/client/assets/chunk-POPQ4Y6H-DZP2a-yV.js +1 -0
- package/dist/client/assets/chunk-SVP7TREG-COihS1MQ.js +88 -0
- package/dist/client/assets/chunk-TICWLB2K-DNZTRqyJ.js +206 -0
- package/dist/client/assets/{chunk-55IACEB6-0243aefd.js → chunk-XXDRQBXY-BC_9L4PF.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-BUnGzX-0.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BUnGzX-0.js +1 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-DrJ8c1cV.js +1 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CMVAR4fd.js +178 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CdAQ0Lst.js +62 -0
- package/dist/client/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/dist/client/assets/dagre-GXQ25YYZ-CybK0L_u.js +4 -0
- package/dist/client/assets/diagram-S7CK7UJ4-CHjTv5Sr.js +30 -0
- package/dist/client/assets/diagram-UQ7AKVKN-DfypPsGR.js +41 -0
- package/dist/client/assets/{diagram-KO2AKTUF-81f42eeb.js → diagram-VSXAHHWV-DHsmmCug.js} +3 -3
- package/dist/client/assets/diagram-VX7I27RA-CHZoTi9k.js +24 -0
- package/dist/client/assets/diagram-Z3DM3KII-Ceb2h13y.js +24 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BlZ247wB.js +1 -0
- package/dist/client/assets/embed-DcNeILcO.js +211 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-BQdw-U-S.js +99 -0
- package/dist/client/assets/flowDiagram-HODETNUW-DjUWHA94.js +168 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CXu2s5vq.js +292 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-CwlsB28k.js +106 -0
- package/dist/client/assets/index-CFelVSRu.js +19 -0
- package/dist/client/assets/index-GF8Uj64S.css +1 -0
- package/dist/client/assets/infoDiagram-27XIBGKW-C7nvTbsQ.js +2 -0
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-50b43d03.js → ishikawaDiagram-5VMMS53U-DGFXG874.js} +2 -2
- package/dist/client/assets/{journeyDiagram-JHISSGLW-dce41bdf.js → journeyDiagram-3NMN7TZE-BDdRMcmq.js} +4 -4
- package/dist/client/assets/{kanban-definition-UN3LZRKU-92401a86.js → kanban-definition-UXKFOSKX-C3fnvfeO.js} +1 -1
- package/dist/client/assets/{katex-4c9ea27f.js → katex-BTI-AXqd.js} +3 -3
- package/dist/client/assets/{katex-0cba7652.js → katex-C5jXJg4s.js} +3 -3
- package/dist/client/assets/layout-BfegUiqt.js +1 -0
- package/dist/client/assets/linear-D1EL1Azg.js +1 -0
- package/dist/client/assets/mermaid.core-DioArgDk.js +310 -0
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-66fe4544.js → mindmap-definition-YA3MSWOX-CFCbAUC0.js} +12 -12
- package/dist/client/assets/{ordinal-db88ae6f.js → ordinal-BeghXfj9.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-B1ULJH6S.js +1 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-B9H3HosU.js +39 -0
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-f64a7ed7.js → quadrantDiagram-AXDQQJYC-eWc3HHkM.js} +4 -4
- package/dist/client/assets/railroadDiagram-O6MQD6OU-DT7c-KwW.js +1 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-Dk1yVWV5.js +84 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B5a__hiX.js +40 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CkCIByRS.js +162 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CnZIW94r.js +1 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-89Sfaoz9.js +1 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CP--cdl-.js +1 -0
- package/dist/client/assets/step-BEQzeS8V.js +1 -0
- package/dist/client/assets/swimlanes-42K2YHIH-C7m88XIh.js +1 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DmG2scPu.js +8 -0
- package/dist/client/assets/{time-043b3464.js → time-BaoZouO5.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-b0c81243.js → timeline-definition-24CTP7MA-BHKHgHq9.js} +10 -10
- package/dist/client/assets/vennDiagram-4TSXK5OY-BqKaRdWu.js +34 -0
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-8d4d7626.js → wardleyDiagram-VM6X3IG4-XiDVRZV5.js} +3 -3
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-CxP4dIVd.js +7 -0
- package/dist/client/index.html +2 -3
- package/dist/server/app.js +29 -397
- package/dist/server/channel.js +5 -3
- package/dist/server/export-html.js +26 -60
- package/dist/server/export-zip.js +83 -0
- package/dist/server/import-zip.js +196 -0
- package/dist/server/index.js +2 -1
- package/dist/server/mcp.js +9 -9
- package/dist/server/port.js +18 -0
- package/dist/server/render-core.js +5 -5
- package/dist/server/routes/export.js +95 -0
- package/dist/server/routes/import.js +24 -0
- package/dist/server/routes/render.js +184 -0
- package/dist/server/routes/slideshow.js +52 -0
- package/dist/server/routes/snapshots.js +119 -0
- package/dist/server/snapshot-reader.js +58 -20
- package/dist/server/snapshot-writer.js +18 -16
- package/dist/server/viewport-cache.js +97 -25
- package/package.json +13 -7
- package/dist/client/assets/architecture-7EHR7CIX-1221aa61.js +0 -1
- package/dist/client/assets/architectureDiagram-3BPJPVTR-1eef4d3c.js +0 -36
- package/dist/client/assets/blockDiagram-GPEHLZMM-313fb06c.js +0 -132
- package/dist/client/assets/c4Diagram-AAUBKEIU-92b023ad.js +0 -10
- package/dist/client/assets/channel-5ee4a8a7.js +0 -1
- package/dist/client/assets/chunk-2J33WTMH-fb021fdc.js +0 -1
- package/dist/client/assets/chunk-727SXJPM-8e0e0228.js +0 -206
- package/dist/client/assets/chunk-AQP2D5EJ-2ccc13c2.js +0 -231
- package/dist/client/assets/classDiagram-4FO5ZUOK-42ffecb6.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-42ffecb6.js +0 -1
- package/dist/client/assets/cose-bilkent-S5V4N54A-45ec2e52.js +0 -1
- package/dist/client/assets/cytoscape.esm-1491e259.js +0 -331
- package/dist/client/assets/dagre-BM42HDAG-ed748885.js +0 -4
- package/dist/client/assets/diagram-2AECGRRQ-ccc0ddec.js +0 -43
- package/dist/client/assets/diagram-5GNKFQAL-342efdea.js +0 -10
- package/dist/client/assets/diagram-LMA3HP47-39e66739.js +0 -24
- package/dist/client/assets/diagram-OG6HWLK6-b3e61903.js +0 -24
- package/dist/client/assets/embed-2b4adf34.js +0 -201
- package/dist/client/assets/erDiagram-TEJ5UH35-281f5cc7.js +0 -85
- package/dist/client/assets/eventmodeling-FCH6USID-6508120e.js +0 -1
- package/dist/client/assets/flowDiagram-I6XJVG4X-c3a4269e.js +0 -162
- package/dist/client/assets/ganttDiagram-6RSMTGT7-013b07ec.js +0 -292
- package/dist/client/assets/gitGraph-WXDBUCRP-02827359.js +0 -1
- package/dist/client/assets/gitGraphDiagram-PVQCEYII-de184acb.js +0 -106
- package/dist/client/assets/graph-f3dfaf53.js +0 -1
- package/dist/client/assets/index-d674a6a4.css +0 -1
- package/dist/client/assets/index-ffc5af4b.js +0 -5
- package/dist/client/assets/info-J43DQDTF-81e32294.js +0 -1
- package/dist/client/assets/infoDiagram-5YYISTIA-196101ee.js +0 -2
- package/dist/client/assets/layout-3a4eff80.js +0 -1
- package/dist/client/assets/linear-fed00341.js +0 -1
- package/dist/client/assets/mermaid-parser.core-52f9cd92.js +0 -173
- package/dist/client/assets/mermaid.core-7dfc221a.js +0 -306
- package/dist/client/assets/packet-YPE3B663-278a272a.js +0 -1
- package/dist/client/assets/pie-LRSECV5Y-2e55d9ae.js +0 -1
- package/dist/client/assets/pieDiagram-4H26LBE5-65b1bcbd.js +0 -30
- package/dist/client/assets/radar-GUYGQ44K-608e459f.js +0 -1
- package/dist/client/assets/requirementDiagram-4Y6WPE33-90ebf889.js +0 -84
- package/dist/client/assets/sankeyDiagram-5OEKKPKP-2d1a83b3.js +0 -40
- package/dist/client/assets/sequenceDiagram-3UESZ5HK-aece3633.js +0 -162
- package/dist/client/assets/stateDiagram-AJRCARHV-9bd5b5f4.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-7c16c251.js +0 -1
- package/dist/client/assets/step-171ad54a.js +0 -1
- package/dist/client/assets/treeView-BLDUP644-e54b9ddb.js +0 -1
- package/dist/client/assets/treemap-LRROVOQU-6a81775b.js +0 -1
- package/dist/client/assets/vennDiagram-CIIHVFJN-8eaf0464.js +0 -34
- package/dist/client/assets/wardley-L42UT6IY-7abd921f.js +0 -1
- package/dist/client/assets/xychartDiagram-2RQKCTM6-450f333c.js +0 -7
- /package/dist/client/assets/{KaTeX_AMS-Regular-0cdd387c.woff2 → KaTeX_AMS-Regular-BQhdFMY1.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_AMS-Regular-30da91e8.woff → KaTeX_AMS-Regular-DMm9YOAa.woff} +0 -0
- /package/dist/client/assets/{KaTeX_AMS-Regular-68534840.ttf → KaTeX_AMS-Regular-DRggAlZN.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Bold-07d8e303.ttf → KaTeX_Caligraphic-Bold-ATXxdsX0.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Bold-1ae6bd74.woff → KaTeX_Caligraphic-Bold-BEiXGLvX.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Bold-de7701e4.woff2 → KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Regular-3398dd02.woff → KaTeX_Caligraphic-Regular-CTRA-rTL.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Regular-5d53e70a.woff2 → KaTeX_Caligraphic-Regular-Di6jR-x-.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Regular-ed0b7437.ttf → KaTeX_Caligraphic-Regular-wX97UBjC.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Bold-9163df9c.ttf → KaTeX_Fraktur-Bold-BdnERNNW.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Bold-9be7ceb8.woff → KaTeX_Fraktur-Bold-BsDP51OF.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Bold-74444efd.woff2 → KaTeX_Fraktur-Bold-CL6g_b3V.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Regular-1e6f9579.ttf → KaTeX_Fraktur-Regular-CB_wures.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Regular-51814d27.woff2 → KaTeX_Fraktur-Regular-CTYiF6lA.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Regular-5e28753b.woff → KaTeX_Fraktur-Regular-Dxdc4cR9.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Bold-0f60d1b8.woff2 → KaTeX_Main-Bold-Cx986IdX.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Bold-c76c5d69.woff → KaTeX_Main-Bold-Jm3AIy58.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Bold-138ac28d.ttf → KaTeX_Main-Bold-waoOVXN0.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Main-BoldItalic-99cd42a3.woff2 → KaTeX_Main-BoldItalic-DxDJ3AOS.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-BoldItalic-70ee1f64.ttf → KaTeX_Main-BoldItalic-DzxPMmG6.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Main-BoldItalic-a6f7ec0d.woff → KaTeX_Main-BoldItalic-SpSLRI95.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Italic-0d85ae7c.ttf → KaTeX_Main-Italic-3WenGoN9.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Italic-f1d6ef86.woff → KaTeX_Main-Italic-BMLOBm91.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Italic-97479ca6.woff2 → KaTeX_Main-Italic-NWA7e6Wa.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Regular-c2342cd8.woff2 → KaTeX_Main-Regular-B22Nviop.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Regular-c6368d87.woff → KaTeX_Main-Regular-Dr94JaBh.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Regular-d0332f52.ttf → KaTeX_Main-Regular-ypZvNtVU.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Math-BoldItalic-f9377ab0.ttf → KaTeX_Math-BoldItalic-B3XSjfu4.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Math-BoldItalic-dc47344d.woff2 → KaTeX_Math-BoldItalic-CZnvNsCZ.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Math-BoldItalic-850c0af5.woff → KaTeX_Math-BoldItalic-iY-2wyZ7.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Math-Italic-8a8d2445.woff → KaTeX_Math-Italic-DA0__PXp.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Math-Italic-08ce98e5.ttf → KaTeX_Math-Italic-flOr_0UB.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Math-Italic-7af58c5e.woff2 → KaTeX_Math-Italic-t53AETM-.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Bold-1ece03f7.ttf → KaTeX_SansSerif-Bold-CFMepnvq.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Bold-e99ae511.woff2 → KaTeX_SansSerif-Bold-D1sUS0GD.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Bold-ece03cfd.woff → KaTeX_SansSerif-Bold-DbIhKOiC.woff} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Italic-00b26ac8.woff2 → KaTeX_SansSerif-Italic-C3H0VqGB.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Italic-91ee6750.woff → KaTeX_SansSerif-Italic-DN2j7dab.woff} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Italic-3931dd81.ttf → KaTeX_SansSerif-Italic-YYjJ1zSn.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Regular-f36ea897.ttf → KaTeX_SansSerif-Regular-BNo7hRIc.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Regular-11e4dc8a.woff → KaTeX_SansSerif-Regular-CS6fqUqJ.woff} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Regular-68e8c73e.woff2 → KaTeX_SansSerif-Regular-DDBCnlJ7.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Script-Regular-1c67f068.ttf → KaTeX_Script-Regular-C5JkGWo-.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Script-Regular-036d4e95.woff2 → KaTeX_Script-Regular-D3wIWfF6.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Script-Regular-d96cdf2b.woff → KaTeX_Script-Regular-D5yQViql.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size1-Regular-c943cc98.woff → KaTeX_Size1-Regular-C195tn64.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size1-Regular-95b6d2f1.ttf → KaTeX_Size1-Regular-Dbsnue_I.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size1-Regular-6b47c401.woff2 → KaTeX_Size1-Regular-mCD8mA8B.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Size2-Regular-a6b2099f.ttf → KaTeX_Size2-Regular-B7gKUWhC.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size2-Regular-d04c5421.woff2 → KaTeX_Size2-Regular-Dy4dx90m.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Size2-Regular-2014c523.woff → KaTeX_Size2-Regular-oD1tc_U0.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size3-Regular-6ab6b62e.woff → KaTeX_Size3-Regular-CTq5MqoE.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size3-Regular-500e04d5.ttf → KaTeX_Size3-Regular-DgpXs0kz.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size4-Regular-99f9c675.woff → KaTeX_Size4-Regular-BF-4gkZK.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size4-Regular-c647367d.ttf → KaTeX_Size4-Regular-DWFBv043.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size4-Regular-a4af7d41.woff2 → KaTeX_Size4-Regular-Dl5lxZxV.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Typewriter-Regular-e14fed02.woff → KaTeX_Typewriter-Regular-C0xS9mPB.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Typewriter-Regular-71d517d6.woff2 → KaTeX_Typewriter-Regular-CO6r4hn1.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Typewriter-Regular-f01f3e87.ttf → KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf} +0 -0
- /package/dist/client/assets/{colors-98f238a0.js → colors-Cc3OSVma.js} +0 -0
- /package/dist/client/assets/{defaultLocale-a8476f12.js → defaultLocale-CzDG__Ur.js} +0 -0
- /package/dist/client/assets/{index-a1656aec.js → index-Bi7T_FA4.js} +0 -0
- /package/dist/client/assets/{init-a5b10ee5.js → init-Dmth1JHB.js} +0 -0
- /package/dist/client/assets/{range-6bc2a9a0.js → range-OtVwhkKS.js} +0 -0
|
@@ -1,47 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { JSDOM } from "jsdom";
|
|
5
5
|
import katex from "katex";
|
|
6
6
|
import * as vl from "vega-lite";
|
|
7
7
|
import * as vega from "vega";
|
|
8
8
|
import DOMPurify from "dompurify";
|
|
9
9
|
import { badgeType } from "./snapshot-reader.js";
|
|
10
|
-
function saveGlobals() {
|
|
11
|
-
const keys = [
|
|
12
|
-
"document", "window", "CSSStyleSheet", "SVGElement", "HTMLElement",
|
|
13
|
-
"Element", "DOMParser", "MutationObserver",
|
|
14
|
-
"requestAnimationFrame", "cancelAnimationFrame",
|
|
15
|
-
];
|
|
16
|
-
const saved = new Map();
|
|
17
|
-
for (const k of keys)
|
|
18
|
-
saved.set(k, global[k]);
|
|
19
|
-
return saved;
|
|
20
|
-
}
|
|
21
|
-
function setGlobals(win) {
|
|
22
|
-
const g = global;
|
|
23
|
-
const w = win;
|
|
24
|
-
g["document"] = win.document;
|
|
25
|
-
g["window"] = win;
|
|
26
|
-
g["CSSStyleSheet"] = w["CSSStyleSheet"];
|
|
27
|
-
g["SVGElement"] = w["SVGElement"];
|
|
28
|
-
g["HTMLElement"] = w["HTMLElement"];
|
|
29
|
-
g["Element"] = w["Element"];
|
|
30
|
-
g["DOMParser"] = w["DOMParser"];
|
|
31
|
-
g["MutationObserver"] = w["MutationObserver"];
|
|
32
|
-
g["requestAnimationFrame"] = (fn) => setTimeout(() => fn(Date.now()), 16);
|
|
33
|
-
g["cancelAnimationFrame"] = clearTimeout;
|
|
34
|
-
}
|
|
35
|
-
function restoreGlobals(saved) {
|
|
36
|
-
for (const [k, v] of saved) {
|
|
37
|
-
if (v === undefined) {
|
|
38
|
-
delete global[k];
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
global[k] = v;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
10
|
// ── Renderers ──────────────────────────────────────────────────────────────
|
|
46
11
|
function renderMermaidContainer(payload) {
|
|
47
12
|
return `<pre class="mermaid">${escapeHtml(payload)}</pre>`;
|
|
@@ -59,7 +24,13 @@ function renderSvgPayload(payload, purify) {
|
|
|
59
24
|
return purify.sanitize(payload, { USE_PROFILES: { svg: true } });
|
|
60
25
|
}
|
|
61
26
|
function renderHtmlPayload(payload, purify) {
|
|
62
|
-
|
|
27
|
+
// ADD_TAGS: the "html" profile's default allowlist excludes <style> (unlike
|
|
28
|
+
// the "svg" profile below, which already allows it) — explicit here since
|
|
29
|
+
// scopeEmbeddedStyles() (B20) depends on <style> surviving sanitization.
|
|
30
|
+
// FORCE_BODY: without it, a leading <style> is parsed as implicit <head>
|
|
31
|
+
// content and silently dropped from DOMPurify's body-only output — ADD_TAGS
|
|
32
|
+
// alone isn't enough.
|
|
33
|
+
return purify.sanitize(payload, { USE_PROFILES: { html: true }, ADD_TAGS: ["style"], FORCE_BODY: true });
|
|
63
34
|
}
|
|
64
35
|
/**
|
|
65
36
|
* Pure string-transform primitive: wraps a CSS string in `@scope (<selector-list>)
|
|
@@ -412,10 +383,24 @@ function itemsIncludeType(items, type) {
|
|
|
412
383
|
return items.some(({ record }) => record.frames.some((f) => f.type === type));
|
|
413
384
|
}
|
|
414
385
|
// ── Public entrypoint ──────────────────────────────────────────────────────
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Each call builds its own jsdom `Window` and passes it explicitly to
|
|
388
|
+
* `DOMPurify(win)` — sanitization runs entirely against that instance, never
|
|
389
|
+
* against Node's global object. KaTeX's `renderToString()` and Vega's
|
|
390
|
+
* `View.toSVG()` (via `renderer: "none"`) need no DOM at all. Because
|
|
391
|
+
* nothing here reads or writes `global.document`/`window`, concurrent calls
|
|
392
|
+
* (reachable from both `POST /export-html` and the `export_html` MCP tool,
|
|
393
|
+
* see bug B14 in `01`) can never stomp on each other's state — no
|
|
394
|
+
* serialization queue is needed.
|
|
395
|
+
*
|
|
396
|
+
* jsdom, not happy-dom: DOMPurify 3.4.8+ silently fails to sanitize
|
|
397
|
+
* descendant nodes against a happy-dom `Window` (upstream incompatibility,
|
|
398
|
+
* capricorn86/happy-dom#1810) — <script> tags and on* attributes pass
|
|
399
|
+
* through unstripped. DOMPurify's own maintainers recommend jsdom for
|
|
400
|
+
* server-side use; confirmed this sanitizes correctly against jsdom.
|
|
401
|
+
*/
|
|
402
|
+
export async function generateExportHtml(items, mode) {
|
|
403
|
+
const win = new JSDOM("").window;
|
|
419
404
|
const purify = DOMPurify(win);
|
|
420
405
|
const rendered = [];
|
|
421
406
|
try {
|
|
@@ -431,7 +416,6 @@ async function generateExportHtmlInner(items, mode) {
|
|
|
431
416
|
}
|
|
432
417
|
}
|
|
433
418
|
finally {
|
|
434
|
-
restoreGlobals(savedGlobals);
|
|
435
419
|
win.close();
|
|
436
420
|
}
|
|
437
421
|
const hasKatex = itemsIncludeType(items, "katex");
|
|
@@ -441,21 +425,3 @@ async function generateExportHtmlInner(items, mode) {
|
|
|
441
425
|
const downloadFilename = buildDownloadFilename(uniqueWorkspaces);
|
|
442
426
|
return { html, downloadFilename };
|
|
443
427
|
}
|
|
444
|
-
// generateExportHtmlInner() patches global DOM state (document, window, ...)
|
|
445
|
-
// for the duration of a call so happy-dom-backed rendering works, then
|
|
446
|
-
// restores it in a `finally`. That save/set/restore isn't reentrant: two
|
|
447
|
-
// overlapping calls (POST /export-html and the export_html MCP tool can run
|
|
448
|
-
// concurrently) each save/restore against whatever the *other* call happened
|
|
449
|
-
// to have in place at that moment, which can leave global DOM state pointing
|
|
450
|
-
// at an already-closed Window once both settle (B14). A simple queue forces
|
|
451
|
-
// calls to run one at a time, in the order they were made, so only one
|
|
452
|
-
// call's globals are ever active at once.
|
|
453
|
-
let exportQueue = Promise.resolve();
|
|
454
|
-
export function generateExportHtml(items, mode) {
|
|
455
|
-
const result = exportQueue.then(() => generateExportHtmlInner(items, mode));
|
|
456
|
-
// Chain the queue off a version that never rejects, so one failed export
|
|
457
|
-
// doesn't permanently wedge every export queued after it; the caller of
|
|
458
|
-
// generateExportHtml() still sees the original rejection via `result`.
|
|
459
|
-
exportQueue = result.then(() => undefined, () => undefined);
|
|
460
|
-
return result;
|
|
461
|
-
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import archiver from "archiver";
|
|
3
|
+
import { findSnapshotFileByIdInWorkspace } from "./snapshot-reader.js";
|
|
4
|
+
/**
|
|
5
|
+
* Reads own package.json version via createRequire (works whether this
|
|
6
|
+
* module runs as server/export-zip.ts under tsx, one level below repo root,
|
|
7
|
+
* or as the compiled dist/server/export-zip.js, two levels below).
|
|
8
|
+
*/
|
|
9
|
+
function getAppVersion() {
|
|
10
|
+
const req = createRequire(import.meta.url);
|
|
11
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = req(rel);
|
|
14
|
+
if (typeof pkg.version === "string")
|
|
15
|
+
return pkg.version;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// try next candidate
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
function buildZipDownloadFilename(workspace) {
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
26
|
+
const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
|
|
27
|
+
`-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
28
|
+
const sanitized = workspace.replace(/[^a-zA-Z0-9_.-]/g, "-").slice(0, 24);
|
|
29
|
+
return `${sanitized}-${ts}.zip`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds a downloadable zip (F36) of the given snapshots' raw JSON files
|
|
33
|
+
* plus a generated manifest.json — no rendering/sanitization pass, unlike
|
|
34
|
+
* generateExportHtml(). Every item must share the same workspace (the
|
|
35
|
+
* manifest format is single-workspace); a mixed-workspace list is rejected
|
|
36
|
+
* rather than silently exporting a subset. Ids that don't resolve to a file
|
|
37
|
+
* on disk are silently skipped (matching /export-html's per-item leniency);
|
|
38
|
+
* an all-miss request is rejected. Viewport-cache data is never included —
|
|
39
|
+
* this only ever touches the snapshot JSON files themselves.
|
|
40
|
+
*/
|
|
41
|
+
export async function generateExportZip(items, root) {
|
|
42
|
+
if (items.length === 0) {
|
|
43
|
+
return { ok: false, error: "items must be a non-empty array" };
|
|
44
|
+
}
|
|
45
|
+
const workspace = items[0].workspace;
|
|
46
|
+
if (items.some((item) => item.workspace !== workspace)) {
|
|
47
|
+
return { ok: false, error: "all items must belong to the same workspace" };
|
|
48
|
+
}
|
|
49
|
+
const files = [];
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const found = await findSnapshotFileByIdInWorkspace(workspace, item.id, root);
|
|
52
|
+
if (found !== null)
|
|
53
|
+
files.push(found);
|
|
54
|
+
}
|
|
55
|
+
if (files.length === 0) {
|
|
56
|
+
return { ok: false, error: "no valid items to export" };
|
|
57
|
+
}
|
|
58
|
+
const manifest = {
|
|
59
|
+
formatVersion: 1,
|
|
60
|
+
workspace,
|
|
61
|
+
exportedAt: new Date().toISOString(),
|
|
62
|
+
appVersion: getAppVersion(),
|
|
63
|
+
snapshots: files.map((f) => f.filename),
|
|
64
|
+
};
|
|
65
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
66
|
+
const chunks = [];
|
|
67
|
+
archive.on("data", (chunk) => chunks.push(chunk));
|
|
68
|
+
const done = new Promise((resolve, reject) => {
|
|
69
|
+
archive.on("end", resolve);
|
|
70
|
+
archive.on("error", reject);
|
|
71
|
+
});
|
|
72
|
+
for (const f of files) {
|
|
73
|
+
archive.append(f.raw, { name: f.filename });
|
|
74
|
+
}
|
|
75
|
+
archive.append(JSON.stringify(manifest, null, 2), { name: "manifest.json" });
|
|
76
|
+
await archive.finalize();
|
|
77
|
+
await done;
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
zip: Buffer.concat(chunks),
|
|
81
|
+
downloadFilename: buildZipDownloadFilename(workspace),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import extract from "extract-zip";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { isValidSnapshotFilename, validateWorkspaceInput } from "./validate.js";
|
|
7
|
+
import { findSnapshotFileByIdInWorkspace } from "./snapshot-reader.js";
|
|
8
|
+
const DEFAULT_LIMITS = {
|
|
9
|
+
maxEntryUncompressedBytes: 20 * 1024 * 1024, // 20MB — generous for a snapshot JSON file
|
|
10
|
+
maxTotalUncompressedBytes: 200 * 1024 * 1024, // 200MB total across the whole archive
|
|
11
|
+
maxEntries: 2000,
|
|
12
|
+
};
|
|
13
|
+
const manifestSchema = z.object({
|
|
14
|
+
formatVersion: z.number(),
|
|
15
|
+
workspace: z.string().min(1),
|
|
16
|
+
exportedAt: z.string(),
|
|
17
|
+
appVersion: z.string(),
|
|
18
|
+
snapshots: z.array(z.string()),
|
|
19
|
+
});
|
|
20
|
+
function parseIdTimestamp(raw) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return {
|
|
24
|
+
id: typeof parsed.id === "string" ? parsed.id : undefined,
|
|
25
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* F40 — picks the filename of the newest (by ISO-8601 timestamp string
|
|
34
|
+
* comparison, same convention as snapshot-reader.ts's own sort) entry among
|
|
35
|
+
* everything actually added/updated by this import, so the caller can load
|
|
36
|
+
* it and switch the client's active workspace as a side effect.
|
|
37
|
+
*/
|
|
38
|
+
function pickNewestFilename(entries) {
|
|
39
|
+
const withTimestamp = entries.filter((e) => e.timestamp !== undefined);
|
|
40
|
+
if (withTimestamp.length === 0)
|
|
41
|
+
return undefined;
|
|
42
|
+
return withTimestamp.reduce((newest, e) => (e.timestamp > newest.timestamp ? e : newest)).filename;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* F39/NF55 — imports an uploaded workspace-export zip (F36's format).
|
|
46
|
+
* Extracts to a fresh temp directory first (never directly into the live
|
|
47
|
+
* snapshots tree) via `extract-zip`, which rejects any entry whose resolved
|
|
48
|
+
* path would escape that directory (zip-slip) as part of its own
|
|
49
|
+
* extraction — confirmed empirically, not just by reading its docs. Only
|
|
50
|
+
* `manifest.json` and the files it explicitly lists are ever trusted; a
|
|
51
|
+
* listed filename is re-validated with `isValidSnapshotFilename()` before
|
|
52
|
+
* being joined into any path, since the manifest itself is just JSON text
|
|
53
|
+
* an attacker could otherwise use to point outside the temp/destination
|
|
54
|
+
* directory even though the zip *entries* themselves are already safe.
|
|
55
|
+
*/
|
|
56
|
+
export async function importZip(zipBuffer, targetWorkspaceInput, mode, root, limits = {}) {
|
|
57
|
+
const { maxEntryUncompressedBytes, maxTotalUncompressedBytes, maxEntries } = { ...DEFAULT_LIMITS, ...limits };
|
|
58
|
+
const validated = validateWorkspaceInput(targetWorkspaceInput);
|
|
59
|
+
if (!validated.ok) {
|
|
60
|
+
return { ok: false, error: validated.error };
|
|
61
|
+
}
|
|
62
|
+
const targetWorkspace = validated.workspace;
|
|
63
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "agent-whiteboard-import-"));
|
|
64
|
+
const zipPath = join(tempRoot, "upload.zip");
|
|
65
|
+
const extractDir = join(tempRoot, "extracted");
|
|
66
|
+
try {
|
|
67
|
+
await writeFile(zipPath, zipBuffer);
|
|
68
|
+
await mkdir(extractDir, { recursive: true });
|
|
69
|
+
let totalUncompressed = 0;
|
|
70
|
+
let entryCount = 0;
|
|
71
|
+
try {
|
|
72
|
+
await extract(zipPath, {
|
|
73
|
+
dir: extractDir,
|
|
74
|
+
onEntry: (entry) => {
|
|
75
|
+
entryCount++;
|
|
76
|
+
if (entryCount > maxEntries) {
|
|
77
|
+
throw new Error("zip contains too many entries");
|
|
78
|
+
}
|
|
79
|
+
if (entry.uncompressedSize > maxEntryUncompressedBytes) {
|
|
80
|
+
throw new Error("zip contains an entry that is too large");
|
|
81
|
+
}
|
|
82
|
+
totalUncompressed += entry.uncompressedSize;
|
|
83
|
+
if (totalUncompressed > maxTotalUncompressedBytes) {
|
|
84
|
+
throw new Error("zip's total uncompressed size is too large");
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return { ok: false, error: err instanceof Error ? err.message : "failed to extract zip" };
|
|
91
|
+
}
|
|
92
|
+
let manifestRaw;
|
|
93
|
+
try {
|
|
94
|
+
manifestRaw = await readFile(join(extractDir, "manifest.json"), "utf-8");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { ok: false, error: "manifest.json is missing from the zip" };
|
|
98
|
+
}
|
|
99
|
+
let manifestJson;
|
|
100
|
+
try {
|
|
101
|
+
manifestJson = JSON.parse(manifestRaw);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { ok: false, error: "manifest.json is not valid JSON" };
|
|
105
|
+
}
|
|
106
|
+
const manifestResult = manifestSchema.safeParse(manifestJson);
|
|
107
|
+
if (!manifestResult.success) {
|
|
108
|
+
return { ok: false, error: "manifest.json is malformed" };
|
|
109
|
+
}
|
|
110
|
+
const manifest = manifestResult.data;
|
|
111
|
+
const destDir = join(root, targetWorkspace);
|
|
112
|
+
const destExists = await stat(destDir).then(() => true, () => false);
|
|
113
|
+
// F40 — every added/updated file's {filename, timestamp}, so the newest
|
|
114
|
+
// one (by timestamp) can be reported back for the client's post-import
|
|
115
|
+
// POST /snapshots/load. Skipped files are deliberately excluded — nothing
|
|
116
|
+
// about them changed.
|
|
117
|
+
const changed = [];
|
|
118
|
+
if (mode === "create") {
|
|
119
|
+
if (destExists) {
|
|
120
|
+
return { ok: false, error: `workspace "${targetWorkspace}" already exists` };
|
|
121
|
+
}
|
|
122
|
+
await mkdir(destDir, { recursive: true });
|
|
123
|
+
let added = 0;
|
|
124
|
+
for (const filename of manifest.snapshots) {
|
|
125
|
+
if (!isValidSnapshotFilename(filename))
|
|
126
|
+
continue;
|
|
127
|
+
const srcPath = join(extractDir, filename);
|
|
128
|
+
let raw;
|
|
129
|
+
try {
|
|
130
|
+
raw = await readFile(srcPath, "utf-8");
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
await writeFile(join(destDir, filename), raw, "utf-8");
|
|
136
|
+
added++;
|
|
137
|
+
changed.push({ filename, timestamp: parseIdTimestamp(raw).timestamp });
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, workspace: targetWorkspace, added, updated: 0, skipped: 0, newestFilename: pickNewestFilename(changed) };
|
|
140
|
+
}
|
|
141
|
+
// mode === "merge"
|
|
142
|
+
if (!destExists) {
|
|
143
|
+
return { ok: false, error: `workspace "${targetWorkspace}" does not exist` };
|
|
144
|
+
}
|
|
145
|
+
let added = 0;
|
|
146
|
+
let updated = 0;
|
|
147
|
+
let skipped = 0;
|
|
148
|
+
for (const filename of manifest.snapshots) {
|
|
149
|
+
if (!isValidSnapshotFilename(filename))
|
|
150
|
+
continue;
|
|
151
|
+
let raw;
|
|
152
|
+
try {
|
|
153
|
+
raw = await readFile(join(extractDir, filename), "utf-8");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const incoming = parseIdTimestamp(raw);
|
|
159
|
+
if (incoming.id === undefined) {
|
|
160
|
+
// Legacy/id-less (pre-v0.11) — always imported as new, never matched.
|
|
161
|
+
await writeFile(join(destDir, filename), raw, "utf-8");
|
|
162
|
+
added++;
|
|
163
|
+
changed.push({ filename, timestamp: incoming.timestamp });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const existing = await findSnapshotFileByIdInWorkspace(targetWorkspace, incoming.id, root);
|
|
167
|
+
if (existing === null) {
|
|
168
|
+
await writeFile(join(destDir, filename), raw, "utf-8");
|
|
169
|
+
added++;
|
|
170
|
+
changed.push({ filename, timestamp: incoming.timestamp });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const existingTimestamp = parseIdTimestamp(existing.raw).timestamp;
|
|
174
|
+
if (existingTimestamp === incoming.timestamp) {
|
|
175
|
+
skipped++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (existingTimestamp !== undefined && incoming.timestamp !== undefined && existingTimestamp > incoming.timestamp) {
|
|
179
|
+
// Destination already has the newer version — nothing to do.
|
|
180
|
+
skipped++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
// Incoming is newer — overwrite (at most one file per id afterward).
|
|
184
|
+
if (existing.filename !== filename) {
|
|
185
|
+
await unlink(join(destDir, existing.filename)).catch(() => { });
|
|
186
|
+
}
|
|
187
|
+
await writeFile(join(destDir, filename), raw, "utf-8");
|
|
188
|
+
updated++;
|
|
189
|
+
changed.push({ filename, timestamp: incoming.timestamp });
|
|
190
|
+
}
|
|
191
|
+
return { ok: true, workspace: targetWorkspace, added, updated, skipped, newestFilename: pickNewestFilename(changed) };
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { pathToFileURL } from "node:url";
|
|
|
11
11
|
import { WebSocketServer } from "ws";
|
|
12
12
|
import { createApp } from "./app.js";
|
|
13
13
|
import { createMcpServer } from "./mcp.js";
|
|
14
|
+
import { parsePort } from "./port.js";
|
|
14
15
|
import { addClient } from "./ws.js";
|
|
15
16
|
// F27 (v1.0) — the server has no authentication on any endpoint (accepted
|
|
16
17
|
// trade-off, see docs/02_assumptions-and-risks.md), so single-user/local-only
|
|
@@ -26,7 +27,7 @@ function assertLoopbackHost(host) {
|
|
|
26
27
|
"Set ALLOW_NON_LOOPBACK=1 if you understand the risk and want to override this.");
|
|
27
28
|
}
|
|
28
29
|
export function startServer(options = {}) {
|
|
29
|
-
const PORT =
|
|
30
|
+
const PORT = parsePort(process.env.PORT, 3000, "PORT");
|
|
30
31
|
const HOST = process.env.HOST ?? "localhost";
|
|
31
32
|
assertLoopbackHost(HOST);
|
|
32
33
|
const app = createApp({ staticRoot: options.staticRoot });
|
package/dist/server/mcp.js
CHANGED
|
@@ -76,9 +76,9 @@ export function createMcpServer() {
|
|
|
76
76
|
.enum(["next", "prev"])
|
|
77
77
|
.describe('"next" to advance, "prev" to rewind.'),
|
|
78
78
|
}),
|
|
79
|
-
}, ({ direction }) => {
|
|
79
|
+
}, async ({ direction }) => {
|
|
80
80
|
return {
|
|
81
|
-
content: [{ type: "text", text: JSON.stringify(stepAndBroadcast(direction)) }],
|
|
81
|
+
content: [{ type: "text", text: JSON.stringify(await stepAndBroadcast(direction)) }],
|
|
82
82
|
};
|
|
83
83
|
});
|
|
84
84
|
// seek(frame) — jump the step cursor to an arbitrary frame index.
|
|
@@ -90,9 +90,9 @@ export function createMcpServer() {
|
|
|
90
90
|
inputSchema: z.object({
|
|
91
91
|
frame: z.number().int().nonnegative().describe("Zero-based frame index to jump to."),
|
|
92
92
|
}),
|
|
93
|
-
}, ({ frame }) => {
|
|
93
|
+
}, async ({ frame }) => {
|
|
94
94
|
return {
|
|
95
|
-
content: [{ type: "text", text: JSON.stringify(seekAndBroadcast(frame)) }],
|
|
95
|
+
content: [{ type: "text", text: JSON.stringify(await seekAndBroadcast(frame)) }],
|
|
96
96
|
};
|
|
97
97
|
});
|
|
98
98
|
// clear() — reset the canvas.
|
|
@@ -317,10 +317,10 @@ export function createMcpServer() {
|
|
|
317
317
|
id: z.string().optional().describe("Optional UUID returned by a previous render() or commit_step_frames() call. " +
|
|
318
318
|
"When provided, retrieves that specific snapshot's payload instead of the current canvas state."),
|
|
319
319
|
}),
|
|
320
|
-
}, ({ id }) => {
|
|
320
|
+
}, async ({ id }) => {
|
|
321
321
|
if (id !== undefined && id !== "") {
|
|
322
322
|
const root = getSnapshotsRoot();
|
|
323
|
-
const payload = findSnapshotById(id, root);
|
|
323
|
+
const payload = await findSnapshotById(id, root);
|
|
324
324
|
if (payload === null) {
|
|
325
325
|
return {
|
|
326
326
|
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "graph not found" }) }],
|
|
@@ -349,7 +349,7 @@ export function createMcpServer() {
|
|
|
349
349
|
.describe("REQUIRED. Workspace name to list snapshots for. " +
|
|
350
350
|
"Alphanumeric, dashes, underscores, dots, spaces — no path separators or '..'."),
|
|
351
351
|
}),
|
|
352
|
-
}, ({ workspace }) => {
|
|
352
|
+
}, async ({ workspace }) => {
|
|
353
353
|
const workspaceResult = validateWorkspaceInput(workspace);
|
|
354
354
|
if (!workspaceResult.ok) {
|
|
355
355
|
return {
|
|
@@ -357,7 +357,7 @@ export function createMcpServer() {
|
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
359
|
const root = getSnapshotsRoot();
|
|
360
|
-
const snapshots = listSnapshots(workspaceResult.workspace, root);
|
|
360
|
+
const snapshots = await listSnapshots(workspaceResult.workspace, root);
|
|
361
361
|
return {
|
|
362
362
|
content: [{ type: "text", text: JSON.stringify({ ok: true, snapshots }) }],
|
|
363
363
|
};
|
|
@@ -402,7 +402,7 @@ export function createMcpServer() {
|
|
|
402
402
|
const root = getSnapshotsRoot();
|
|
403
403
|
const validItems = [];
|
|
404
404
|
for (const id of ids) {
|
|
405
|
-
const record = findSnapshotByIdInWorkspace(validatedWorkspace, id, root);
|
|
405
|
+
const record = await findSnapshotByIdInWorkspace(validatedWorkspace, id, root);
|
|
406
406
|
if (record !== null) {
|
|
407
407
|
validItems.push({ workspace: validatedWorkspace, id, record });
|
|
408
408
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared port-env-var validation (NF52) — used by index.ts's PORT and
|
|
3
|
+
* channel.ts/app.ts's CHANNEL_PORT, so an invalid value fails fast with a
|
|
4
|
+
* clear error instead of a low-level bind/listen/fetch failure.
|
|
5
|
+
*
|
|
6
|
+
* 0 is accepted (not just 1–65535): Node's listen()/serve() treat it as "let
|
|
7
|
+
* the OS assign a free port", which tests/unit/server/index.test.ts already
|
|
8
|
+
* relies on for ephemeral test binding.
|
|
9
|
+
*/
|
|
10
|
+
export function parsePort(value, fallback, envVarName) {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
return fallback;
|
|
13
|
+
const port = Number(value);
|
|
14
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
15
|
+
throw new Error(`Invalid ${envVarName} "${value}": must be an integer between 0 and 65535.`);
|
|
16
|
+
}
|
|
17
|
+
return port;
|
|
18
|
+
}
|
|
@@ -125,7 +125,7 @@ export async function applyLoadedSnapshotResult(frames, workspace, title, nodeTo
|
|
|
125
125
|
// B19/FR21 — composite id:frameIndex key). Looked up under the snapshot's
|
|
126
126
|
// own id, not a synthesized one — a synthesized id can never have a cache
|
|
127
127
|
// entry anyway.
|
|
128
|
-
const viewport = snapshotId !== undefined ? getViewport(snapshotId, 0) : undefined;
|
|
128
|
+
const viewport = snapshotId !== undefined ? await getViewport(snapshotId, 0) : undefined;
|
|
129
129
|
// A concrete id is required on every broadcast (v0.26 Sprint 42) — pre-v0.11
|
|
130
130
|
// snapshots may lack one (J1, `02`), so synthesize a fresh one here.
|
|
131
131
|
const resolvedId = snapshotId ?? generateSnapshotId();
|
|
@@ -161,7 +161,7 @@ export async function applyLoadedSnapshotResult(frames, workspace, title, nodeTo
|
|
|
161
161
|
* verbatim between app.ts and mcp.ts, the one hot-path pair render-core.ts's
|
|
162
162
|
* own NF12 header comment didn't yet cover).
|
|
163
163
|
*/
|
|
164
|
-
export function stepAndBroadcast(direction) {
|
|
164
|
+
export async function stepAndBroadcast(direction) {
|
|
165
165
|
const result = stepCursor(direction);
|
|
166
166
|
if (!result) {
|
|
167
167
|
return { ok: false, error: "no step-frames sequence is loaded" };
|
|
@@ -174,7 +174,7 @@ export function stepAndBroadcast(direction) {
|
|
|
174
174
|
// restores its own saved viewport independently (v0.26.1, bug B19/FR21) —
|
|
175
175
|
// no longer "must not re-fit" for the whole sequence.
|
|
176
176
|
const resolvedId = id ?? generateSnapshotId();
|
|
177
|
-
const viewport = getViewport(resolvedId, result.currentFrame);
|
|
177
|
+
const viewport = await getViewport(resolvedId, result.currentFrame);
|
|
178
178
|
broadcastStepFrames(frames, state.frameType, result.currentFrame, resolvedId, title, state.nodeToFrame, viewport);
|
|
179
179
|
}
|
|
180
180
|
return { ok: true, current_frame: result.currentFrame, total_frames: result.totalFrames };
|
|
@@ -183,7 +183,7 @@ export function stepAndBroadcast(direction) {
|
|
|
183
183
|
* Jumps the step cursor to an arbitrary frame index and broadcasts it.
|
|
184
184
|
* Shared by POST /seek and the MCP `seek` tool (NF19, see `stepAndBroadcast`).
|
|
185
185
|
*/
|
|
186
|
-
export function seekAndBroadcast(frame) {
|
|
186
|
+
export async function seekAndBroadcast(frame) {
|
|
187
187
|
const state = getCanvas();
|
|
188
188
|
if (!isStepSequence(state)) {
|
|
189
189
|
return { ok: false, error: "no step-frames sequence is loaded" };
|
|
@@ -207,7 +207,7 @@ export function seekAndBroadcast(frame) {
|
|
|
207
207
|
id: resolvedId,
|
|
208
208
|
// Per-frame restore (v0.26.1, bug B19/FR21) — each frame of a sequence
|
|
209
209
|
// re-fits or restores independently instead of sharing one viewport.
|
|
210
|
-
viewport: getViewport(resolvedId, frame),
|
|
210
|
+
viewport: await getViewport(resolvedId, frame),
|
|
211
211
|
});
|
|
212
212
|
return { ok: true, current_frame: frame, total_frames: total };
|
|
213
213
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { exportCanvas } from "../session.js";
|
|
2
|
+
import { findSnapshotById, findSnapshotByIdInWorkspace } from "../snapshot-reader.js";
|
|
3
|
+
import { validateWorkspaceInput } from "../render-core.js";
|
|
4
|
+
import { generateExportHtml } from "../export-html.js";
|
|
5
|
+
import { generateExportZip } from "../export-zip.js";
|
|
6
|
+
import { getSnapshotsRoot } from "../paths.js";
|
|
7
|
+
export function registerExportRoutes(app) {
|
|
8
|
+
app.get("/export", async (c) => {
|
|
9
|
+
const id = c.req.query("id");
|
|
10
|
+
if (id !== undefined && id !== "") {
|
|
11
|
+
const root = getSnapshotsRoot();
|
|
12
|
+
const payload = await findSnapshotById(id, root);
|
|
13
|
+
if (payload === null) {
|
|
14
|
+
return c.json({ ok: false, error: "graph not found" }, 404);
|
|
15
|
+
}
|
|
16
|
+
return c.json({ ok: true, data: payload });
|
|
17
|
+
}
|
|
18
|
+
return c.json({ ok: true, data: exportCanvas() });
|
|
19
|
+
});
|
|
20
|
+
// ── HTML Export (v0.13) ────────────────────────────────────────────────────
|
|
21
|
+
app.post("/export-html", async (c) => {
|
|
22
|
+
const body = await c.req.json();
|
|
23
|
+
if (!Array.isArray(body.items) || body.items.length === 0) {
|
|
24
|
+
return c.json({ ok: false, error: "items must be a non-empty array" }, 400);
|
|
25
|
+
}
|
|
26
|
+
// Default cdn (v0.32, F23): offline (fully embedded, today's pre-v0.32
|
|
27
|
+
// behavior) is opt-in via mode: "offline"; any other value falls back to cdn.
|
|
28
|
+
const mode = body.mode === "offline" ? "offline" : "cdn";
|
|
29
|
+
const root = getSnapshotsRoot();
|
|
30
|
+
const validItems = [];
|
|
31
|
+
for (const item of body.items) {
|
|
32
|
+
if (typeof item !== "object" || item === null)
|
|
33
|
+
continue;
|
|
34
|
+
const { workspace, id } = item;
|
|
35
|
+
if (typeof workspace !== "string")
|
|
36
|
+
continue;
|
|
37
|
+
const validated = validateWorkspaceInput(workspace);
|
|
38
|
+
if (!validated.ok)
|
|
39
|
+
continue;
|
|
40
|
+
if (typeof id !== "string")
|
|
41
|
+
continue;
|
|
42
|
+
const record = await findSnapshotByIdInWorkspace(validated.workspace, id, root);
|
|
43
|
+
if (record === null)
|
|
44
|
+
continue;
|
|
45
|
+
validItems.push({ workspace: validated.workspace, id, record });
|
|
46
|
+
}
|
|
47
|
+
if (validItems.length === 0) {
|
|
48
|
+
return c.json({ ok: false, error: "no valid items to export" }, 400);
|
|
49
|
+
}
|
|
50
|
+
const { html, downloadFilename } = await generateExportHtml(validItems, mode);
|
|
51
|
+
return new Response(html, {
|
|
52
|
+
status: 200,
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
55
|
+
"Content-Disposition": `attachment; filename="${downloadFilename}"`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
// ── Zip Export (v1.6, F36) ───────────────────────────────────────────────
|
|
60
|
+
app.post("/export-zip", async (c) => {
|
|
61
|
+
const body = await c.req.json();
|
|
62
|
+
if (!Array.isArray(body.items) || body.items.length === 0) {
|
|
63
|
+
return c.json({ ok: false, error: "items must be a non-empty array" }, 400);
|
|
64
|
+
}
|
|
65
|
+
const zipItems = [];
|
|
66
|
+
for (const item of body.items) {
|
|
67
|
+
if (typeof item !== "object" || item === null)
|
|
68
|
+
continue;
|
|
69
|
+
const { workspace, id } = item;
|
|
70
|
+
if (typeof workspace !== "string")
|
|
71
|
+
continue;
|
|
72
|
+
const validated = validateWorkspaceInput(workspace);
|
|
73
|
+
if (!validated.ok)
|
|
74
|
+
continue;
|
|
75
|
+
if (typeof id !== "string")
|
|
76
|
+
continue;
|
|
77
|
+
zipItems.push({ workspace: validated.workspace, id });
|
|
78
|
+
}
|
|
79
|
+
if (zipItems.length === 0) {
|
|
80
|
+
return c.json({ ok: false, error: "no valid items to export" }, 400);
|
|
81
|
+
}
|
|
82
|
+
const root = getSnapshotsRoot();
|
|
83
|
+
const result = await generateExportZip(zipItems, root);
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return c.json({ ok: false, error: result.error }, 400);
|
|
86
|
+
}
|
|
87
|
+
return new Response(new Uint8Array(result.zip), {
|
|
88
|
+
status: 200,
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/zip",
|
|
91
|
+
"Content-Disposition": `attachment; filename="${result.downloadFilename}"`,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { bodyLimit } from "hono/body-limit";
|
|
2
|
+
import { importZip } from "../import-zip.js";
|
|
3
|
+
import { getSnapshotsRoot } from "../paths.js";
|
|
4
|
+
// NF55 — a per-route override, larger than app.ts's global 10MB JSON cap
|
|
5
|
+
// (app.ts excludes this path from that global middleware so the two caps
|
|
6
|
+
// don't stack — the stricter one would otherwise always win).
|
|
7
|
+
export const IMPORT_MAX_BODY_SIZE_BYTES = 50 * 1024 * 1024;
|
|
8
|
+
export function registerImportRoutes(app) {
|
|
9
|
+
app.post("/import", bodyLimit({ maxSize: IMPORT_MAX_BODY_SIZE_BYTES }), async (c) => {
|
|
10
|
+
const body = await c.req.parseBody();
|
|
11
|
+
const file = body["file"];
|
|
12
|
+
if (!(file instanceof File)) {
|
|
13
|
+
return c.json({ ok: false, error: "file is required" }, 400);
|
|
14
|
+
}
|
|
15
|
+
const mode = body["mode"] === "merge" ? "merge" : "create";
|
|
16
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
17
|
+
const root = getSnapshotsRoot();
|
|
18
|
+
const result = await importZip(buffer, body["targetWorkspace"], mode, root);
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
return c.json({ ok: false, error: result.error }, 400);
|
|
21
|
+
}
|
|
22
|
+
return c.json(result);
|
|
23
|
+
});
|
|
24
|
+
}
|