@tequila99/githuman 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -9
- package/dist/cli/config.js +35 -2
- package/dist/cli/index.js +2 -1
- package/dist/cli/server-runtime.js +14 -11
- package/dist/cli/startup-message.js +5 -4
- package/dist/server/app.js +48 -5
- package/dist/server/db/migrations.js +19 -0
- package/dist/server/errors/devices.js +7 -0
- package/dist/server/errors/http.js +1 -0
- package/dist/server/hooks/access/local-request.js +21 -0
- package/dist/server/hooks/access/require-access.js +93 -0
- package/dist/server/hooks/agents/local-origin.js +9 -26
- package/dist/server/hooks/notes/same-origin.js +7 -34
- package/dist/server/hooks/terminal/local-origin.js +8 -22
- package/dist/server/repositories/device.repo.js +74 -0
- package/dist/server/routes/agent.js +11 -2
- package/dist/server/routes/devices.js +130 -0
- package/dist/server/routes/events.js +7 -1
- package/dist/server/routes/notes.js +3 -3
- package/dist/server/routes/terminal.js +10 -2
- package/dist/server/services/device-connections.js +79 -0
- package/dist/server/services/device.service.js +222 -0
- package/dist/server/services/notes-external-assets.service.js +5 -5
- package/dist/server/services/notes-identity.service.js +4 -2
- package/dist/server/services/terminal/tokens.js +1 -1
- package/dist/server/utils/address-key.js +33 -0
- package/dist/server/utils/allowed-hosts.js +38 -0
- package/dist/server/utils/device-name.js +27 -0
- package/dist/server/utils/lan-addresses.js +35 -0
- package/dist/server/utils/remote-asset-request.js +9 -2
- package/dist/server/utils/request-origin.js +105 -0
- package/dist/shared/devices/constants.js +16 -0
- package/dist/shared/devices/name.js +12 -0
- package/dist/shared/devices/schemas.js +67 -0
- package/dist/shared/devices/types.js +1 -0
- package/dist/shared/events/types.js +2 -1
- package/dist/shared/network/address-url.js +4 -0
- package/dist/shared/utils/date.js +3 -0
- package/dist/web/assets/AccessPage-_OKn9U0P.js +1 -0
- package/dist/web/assets/AccessPage-l5Yiiw9T.css +1 -0
- package/dist/web/assets/AppPage-oQ9Rz0be.js +1 -0
- package/dist/web/assets/CreateButton-D1EtoicG.js +1 -0
- package/dist/web/assets/CreateReviewFabButton-B2Td8Wno.css +1 -0
- package/dist/web/assets/CreateReviewFabButton-BxvT3i9e.js +1 -0
- package/dist/web/assets/ErrorNotFound-DtnO85PU.js +1 -0
- package/dist/web/assets/IndexPage-D5PvyGrX.css +1 -0
- package/dist/web/assets/IndexPage-gdD7BHxA.js +1 -0
- package/dist/web/assets/MainLayout-47PTLKZf.js +6 -0
- package/dist/web/assets/MainLayout-ikSrFqBh.css +1 -0
- package/dist/web/assets/NoteVisualEditor-BQ6mVfj0.js +1054 -0
- package/dist/web/assets/{NoteVisualEditor-IH3s3d3W.css → NoteVisualEditor-BYikrY7Q.css} +1 -1
- package/dist/web/assets/NotesPage-DNR9iW_G.js +4 -0
- package/dist/web/assets/NotesPage-_8naQciW.css +1 -0
- package/dist/web/assets/PanelResize-CAQFVc8b.js +1 -0
- package/dist/web/assets/PanelResize-XjrbOj2S.css +1 -0
- package/dist/web/assets/{PdfPreview-DgwnGJ2E.js → PdfPreview-kL_nk9my.js} +2 -2
- package/dist/web/assets/PdfPreview-l_xzsLJ_.css +1 -0
- package/dist/web/assets/{QBtn-DoggJe1Q.js → QBtn-CB-yw_iP.js} +1 -1
- package/dist/web/assets/QExpansionItem-Dr5oXO0m.js +4 -0
- package/dist/web/assets/QFab-DPz8RiP0.js +1 -0
- package/dist/web/assets/QForm-CASiKhGm.js +1 -0
- package/dist/web/assets/{QInput-DJgTh2Qy.js → QInput-k34nCPWg.js} +1 -1
- package/dist/web/assets/QSelect-CaKn9Tkg.js +1 -0
- package/dist/web/assets/QSpace-Di_bZQY6.js +1 -0
- package/dist/web/assets/QVirtualScroll-oGN_Tl5J.js +1 -0
- package/dist/web/assets/ReviewDetailPage-BKgIprY0.js +1 -0
- package/dist/web/assets/ReviewsPage-6PJ6wofN.js +1 -0
- package/dist/web/assets/{StatusSwitcher-BCv_9MtI.css → StatusSwitcher-D6i8_OfR.css} +1 -1
- package/dist/web/assets/StatusSwitcher-s9w6iKpR.js +1 -0
- package/dist/web/assets/{TerminalView-C1jFlfLH.css → TerminalView-617DZuPt.css} +1 -1
- package/dist/web/assets/TerminalView-BsO_SZut.js +19 -0
- package/dist/web/assets/{abnfDiagram-O67JEVCF-BYlDf7LI.js → abnfDiagram-O67JEVCF-qUSrlp_c.js} +1 -1
- package/dist/web/assets/access-store-Q8nkv4Nb.js +1 -0
- package/dist/web/assets/agent-store-D2jgeBgi.js +113 -0
- package/dist/web/assets/architecture-7GRP2DOG-D6PShS0_.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-CC0SmfK_.js → architectureDiagram-NJMV4G6O-D7RC6OS2.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-C1Ny76Gc.js → blockDiagram-BEXU5L5S-CmNtTpfK.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-CP2if62Y.js → c4Diagram-YGBWAQC7-CKCcu1qx.js} +1 -1
- package/dist/web/assets/channel-D6lb5DbY.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-cjM6hj3T.js → chunk-3FUC2YCW-Drz6oeKx.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-DAW_EwWQ.js → chunk-5DYCD2WN-RItzEgV_.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-nkStFRF-.js → chunk-742MDFTN-D6hKdOv7.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-EQQsEy8I.js → chunk-7INBJB4K-Dk7-Eior.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-DUJ2C21w.js → chunk-7M6MHVWA-Cw8E0Nkn.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-C1Bj5-NO.js → chunk-7PRAP22T-t-mTeNXF.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-DW39t1bj.js → chunk-GTNCS2PH-C7j2atV8.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-i49YwHnS.js → chunk-GWA4HPMP-CH9T9LZO.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-C4mXlV61.js → chunk-MBY4JIJT-yXOO37xD.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-DoRSO-kq.js → chunk-NETBCI7D-C-s2UtAR.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-D38ZePQT.js → chunk-O7XYJQB3-5Me_aV8o.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-DmOI-u6c.js → chunk-UA2S7LBM-BQaibeff.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-Dv710fF3.js → chunk-WEXAMYUT-UBcOE4Lf.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-CWHlAHIu.js → chunk-Z7XXMR3K-BsA8o3Pt.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-Bid5tGRk.js → chunk-ZIGJFQKS--KwSI1U0.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-C1770G1t.js → classDiagram-v2-NBCMYWYE-pMF8oCmq.js} +1 -1
- package/dist/web/assets/create-B9Tn5Xvi.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-CWxF4ZIN.js → cynefin-OW5HDTMX-DFgtXolF.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-DcFBTI6m.js → cynefinDiagram-VND7K2PF-CPY0JRyq.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-CP5wTvgN.js → dagre-6A5THRUB-KdjJKAB7.js} +1 -1
- package/dist/web/assets/devices-store-CrZ4MxyY.js +1 -0
- package/dist/web/assets/{diagram-22UHCM2B-mLUm1v32.js → diagram-22UHCM2B-BDcOcvF_.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-CP0OFpuS.js → diagram-3UASUU5V-6j2nlZL3.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-DolFulHH.js → diagram-ATOU4E4O-BN8cdxED.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-Bs17AhwI.js → diagram-CDSNMT55-cWPiXHkr.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-BfhsJwV7.js → diagram-MLGK6HIB-DcgkH_Vl.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-B1c-Ikp-.js → diagram-MPIPVDR6-D_e86L99.js} +1 -1
- package/dist/web/assets/diff-store-ByxMCKoa.js +93 -0
- package/dist/web/assets/{dist-CyyHluOV.js → dist-BMFtkY2y.js} +1 -1
- package/dist/web/assets/{dist-d8yBJi6g.js → dist-BP40mvwJ.js} +1 -1
- package/dist/web/assets/{dist-zqKz9Qrf.js → dist-BVVhgjWT.js} +1 -1
- package/dist/web/assets/{dist-BsjKEQwP.js → dist-Bdsq-Ze1.js} +1 -1
- package/dist/web/assets/{dist-Bm8OAr9J.js → dist-BwpyANeW.js} +1 -1
- package/dist/web/assets/{dist-DkKvAWYi.js → dist-Bz40Nd9I.js} +1 -1
- package/dist/web/assets/{dist-yQ-fH4og.js → dist-C9enVshI.js} +1 -1
- package/dist/web/assets/{dist-DuW-c5so.js → dist-CLCP7f3x.js} +1 -1
- package/dist/web/assets/{dist-C6YiC_nY.js → dist-CU43ytsh.js} +1 -1
- package/dist/web/assets/{dist-g0NXSFab.js → dist-CjkUzU4s.js} +1 -1
- package/dist/web/assets/{dist-D6WXR3TF.js → dist-MUQaJRtC.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-BvQ60ZD9.js → ebnfDiagram-ZINNZB2B-D8xHOFOi.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-C27WMGnY.js → elk-276RUBZZ-JiDwNyLy.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-CdfkOXb9.js → erDiagram-OPXOYQCR-D3J6u81a.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-QCwZLvmm.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-CgF90hDi.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-CuKNmNbv.js → ganttDiagram-FUAMR5RP-nvAVhbgI.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-DBGr6EfP.js → gitGraph-4MIJSDKK-Bv_wkB2T.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-BUZgiyYd.js → gitGraphDiagram-X574FWY7-DxxwadhH.js} +1 -1
- package/dist/web/assets/i18n-DDCqtTTW.js +1 -0
- package/dist/web/assets/index-BNHt6zmP.js +2 -0
- package/dist/web/assets/index-BwbM_RKi.css +1 -0
- package/dist/web/assets/{info-A6RAGUB7-CjnEima-.js → info-A6RAGUB7-Dw18yeIE.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-D91sa8do.js → infoDiagram-VRGFBTTK-C3BrgjYg.js} +1 -1
- package/dist/web/assets/init-app-BouXTk8p.js +1 -0
- package/dist/web/assets/{is-CE4jkrGQ.js → is-D0waCELe.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-2K-kH65E.js → ishikawaDiagram-OU5B5YK6-DU2p9szX.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CkENPVJA.js → journeyDiagram-ZHPQQLJL-Bs2zQnew.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-BHJODz7s.js → kanban-definition-PNTS6WVX-pqXd5uNi.js} +1 -1
- package/dist/web/assets/{line-C6VvIL_x.js → line-CN4Dxpei.js} +1 -1
- package/dist/web/assets/{markdown-192Ev3_O.js → markdown-jR0fhe4d.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-DMJg-lnr.js → mermaid-parser.core-DZqDOGRM.js} +3 -3
- package/dist/web/assets/{mermaid.core-Dd40xnyQ.js → mermaid.core-CUndCvkz.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-DhfAMSga.js → mindmap-definition-NLK3R4M7-DyK7YqB7.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-DZUk7rEw.js → packet-AYTQ26CC-DvlW_0BP.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-W22cBCh2.js → pegDiagram-GJSIUBJH-k2e_-FWQ.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-BMvwPiHK.js → pie-WAS4IAKB-B99zaYjO.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-CZ2EVCmx.js → pieDiagram-5QR66LMP-BPbS05lK.js} +1 -1
- package/dist/web/assets/pinia-BETEFLDG.js +1 -0
- package/dist/web/assets/pinia-DO0AdKGg.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-DBS45bbr.js → quadrantDiagram-O4NWA36T-DqNNdsd2.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-BDkD2JF1.js → radar-RG4KPBEZ-BIAAr9LO.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-BH_sA5bU.js → railroad-74A4TZTK-DYxr-1-f.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-GESjhc3t.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cybu0A7J.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-DCHxrtbI.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-CrI1TIG1.js → railroadDiagram-XR7U4H2S-TNg-LqRR.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-BbtaFtGn.js → requirementDiagram-PLB6GJNP-hKAyJR6W.js} +1 -1
- package/dist/web/assets/runtime-core.esm-bundler-CAvPiMpd.js +1 -0
- package/dist/web/assets/{safe-storage-DekXHbgc.js → safe-storage-B1Aq5CGi.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DRU9tLf3.js → sankeyDiagram-IPEJSGJF-_MSL-tES.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-RquVuniP.js → sequenceDiagram-PO4LG4MO-Byq-joBG.js} +1 -1
- package/dist/web/assets/single-flight-DLCbzLI3.js +1 -0
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-CKjC8JTS.js → stateDiagram-v2-GCMORJYK-BR2jAgaa.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-B-5FN1NY.js → swimlanes-2SLR337P-Nt8ojGKK.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-Ds1hVYqh.js +8 -0
- package/dist/web/assets/terminal-store-D0abibMa.js +1 -0
- package/dist/web/assets/theme-QEcOl0lf.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-DbG_YVkW.js → timeline-definition-EJHVYXUP-BtihLvxk.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-BgkrW_FP.js → treeView-Q6P3EWNA-D5HzXeKd.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-Dp8ISpo7.js → treemap-WGGIJYW6-Dovjuutg.js} +1 -1
- package/dist/web/assets/use-add-agent-context-BaPUIO93.js +1 -0
- package/dist/web/assets/use-app-theme-B1cdbnHn.js +1 -0
- package/dist/web/assets/use-countdown-CTag4T13.js +1 -0
- package/dist/web/assets/use-panel-width-CB-0S3zX.js +1 -0
- package/dist/web/assets/use-router-link-Dr9wnnyZ.js +1 -0
- package/dist/web/assets/use-row-height-DQHVMtUe.js +1 -0
- package/dist/web/assets/use-server-events-BrBCbeHu.js +1 -0
- package/dist/web/assets/{use-virtual-scroll-BSXYZjZ5.js → use-virtual-scroll-DG0UKjVk.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-DwRXMhnD.js → usecaseDiagram-POWQR4AR-DSwRE8zj.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-CxdeHvjU.js → vennDiagram-UO4OBE2U-C-B8yP1K.js} +1 -1
- package/dist/web/assets/{vue-i18n.runtime-BpRrzZrT.js → vue-i18n.runtime-CEtihHHL.js} +2 -2
- package/dist/web/assets/{wardley-WFR3VGLG-C8gq8AfM.js → wardley-WFR3VGLG-wWgaZuh6.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-C9Jo2KAj.js → wardleyDiagram-VNRHLVJA-DbhenrHd.js} +1 -1
- package/dist/web/assets/weighted-lru-BSxRnMHR.js +1 -0
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-D3tp7hdU.js → xychartDiagram-PMCCYNJV-CxFDmAEt.js} +1 -1
- package/dist/web/index.html +8 -8
- package/package.json +3 -1
- package/dist/web/assets/AppPage-DdmqiV2R.js +0 -1
- package/dist/web/assets/ClosePopup-DhjIPp8s.js +0 -1
- package/dist/web/assets/CreateButton-BoxoJ7Jr.js +0 -93
- package/dist/web/assets/CreateReviewFabButton-CA5OsG7d.js +0 -1
- package/dist/web/assets/CreateReviewFabButton-Cufd8hVJ.css +0 -1
- package/dist/web/assets/ErrorNotFound-Cq0wNQMu.js +0 -1
- package/dist/web/assets/IndexPage-CROp8XNF.js +0 -1
- package/dist/web/assets/IndexPage-F8MMAAkn.css +0 -1
- package/dist/web/assets/MainLayout-DM30ynlR.css +0 -1
- package/dist/web/assets/MainLayout-DvfVDB2G.js +0 -6
- package/dist/web/assets/NoteVisualEditor-VfLNFzsD.js +0 -1054
- package/dist/web/assets/NotesPage-D_NiwDh8.js +0 -4
- package/dist/web/assets/NotesPage-KaCEnfdz.css +0 -1
- package/dist/web/assets/PanelResize-9uljPbE4.js +0 -1
- package/dist/web/assets/PanelResize-C51WwwLD.css +0 -1
- package/dist/web/assets/PdfPreview-Cb0xawi0.css +0 -1
- package/dist/web/assets/QFab-D1X33Wdk.js +0 -1
- package/dist/web/assets/QForm-BQMj3m1W.js +0 -4
- package/dist/web/assets/QResizeObserver-D-9MeQ9q.js +0 -1
- package/dist/web/assets/QScrollArea-DlLel0SO.js +0 -1
- package/dist/web/assets/QSelect-mw3iS4Cp.js +0 -1
- package/dist/web/assets/QSpace-BbB0w38T.js +0 -1
- package/dist/web/assets/QVirtualScroll-FtQn3PJj.js +0 -1
- package/dist/web/assets/ReviewDetailPage-D7x0R_2N.js +0 -1
- package/dist/web/assets/ReviewsPage-C317IA1b.js +0 -1
- package/dist/web/assets/StatusSwitcher-BQXo4sss.js +0 -1
- package/dist/web/assets/TerminalView-CTsFWvWm.js +0 -19
- package/dist/web/assets/agent-store-ByB1OrjP.js +0 -113
- package/dist/web/assets/app-info-store-Gn-SNU3Y.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-DNWQGMIX.js +0 -1
- package/dist/web/assets/channel-DW-wL7ua.js +0 -1
- package/dist/web/assets/create-BSmXD9Zr.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-C1sONJTj.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-CU2TsxFx.js +0 -1
- package/dist/web/assets/i18n-Cxfq3B8n.js +0 -1
- package/dist/web/assets/index-DKirIJG5.js +0 -2
- package/dist/web/assets/index-Dma-RXBR.css +0 -1
- package/dist/web/assets/init-app-B6mIRN6f.js +0 -1
- package/dist/web/assets/pinia-Cp_DCu4m.js +0 -1
- package/dist/web/assets/pinia-DCsnCHsH.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-D6lX11kj.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-D3N0Z6uV.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DOaenw8H.js +0 -1
- package/dist/web/assets/runtime-core.esm-bundler-BKFI1w28.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-B5SL6iu0.js +0 -8
- package/dist/web/assets/theme-C1B2V2RV.js +0 -1
- package/dist/web/assets/use-add-agent-context-3-KB2D10.js +0 -1
- package/dist/web/assets/use-app-theme-D_HR0B3W.js +0 -1
- package/dist/web/assets/use-panel-width-DIzwb-yt.js +0 -1
- package/dist/web/assets/use-router-link-BGc0noIO.js +0 -1
- package/dist/web/assets/use-server-events-C13Sw-rU.js +0 -1
- package/dist/web/assets/window-store-CnPjWx6-.js +0 -1
package/README.md
CHANGED
|
@@ -61,14 +61,56 @@ node dist/cli/index.js serve
|
|
|
61
61
|
`git rev-parse --show-toplevel` от текущей директории).
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
node dist/cli/index.js serve --port 4000 --host 0.0.0.0 # LAN-доступ, см.
|
|
64
|
+
node dist/cli/index.js serve --port 4000 --host 0.0.0.0 # LAN-доступ, см. ниже
|
|
65
65
|
node dist/cli/index.js list --status in_progress # список ревью без веб-интерфейса
|
|
66
66
|
node dist/cli/index.js export last --format markdown -o review.md
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
### Браузерная совместимость
|
|
70
|
+
|
|
71
|
+
Production JS и CSS собираются с целью Chrome 103 и новее; для остальных
|
|
72
|
+
браузеров цели — Edge 111, Firefox 114, Safari/iOS 16.4. Мобильная компоновка,
|
|
73
|
+
Notes и терминал проверены в Chrome 103 на Android 13 в эмуляторе.
|
|
74
|
+
Цель сборки не добавляет полифилы всех браузерных API: отдельные возможности
|
|
75
|
+
(например, fullscreen) зависят от поддержки браузером. CSS Quasar и редактора
|
|
76
|
+
заметок (Milkdown) использует `:has()` и `dvh`; в Chrome до 105/108 эти правила
|
|
77
|
+
пропускаются, и меняется только оформление отдельных элементов.
|
|
78
|
+
|
|
79
|
+
### Доступ из локальной сети
|
|
80
|
+
|
|
81
|
+
`--host` принимает `localhost` (по умолчанию), `127.0.0.1`, `::1`, а для
|
|
82
|
+
доступа из сети — `0.0.0.0` (IPv4) или `::` (IPv4 и IPv6). Конкретный
|
|
83
|
+
LAN-адрес не принимается: без loopback-слушателя некому подтвердить
|
|
84
|
+
устройство. При запуске на всех интерфейсах консоль печатает
|
|
85
|
+
`http://localhost:<порт>` и LAN-адреса машины.
|
|
86
|
+
|
|
87
|
+
Устройство из сети получает доступ только после сопряжения:
|
|
88
|
+
|
|
89
|
+
1. Откройте LAN-адрес на телефоне или другой машине (адрес и QR-код —
|
|
90
|
+
в пункте меню **Devices** вкладки на `localhost`).
|
|
91
|
+
2. Нажмите **Request access**: устройство покажет шестизначный код.
|
|
92
|
+
3. Во вкладке на `localhost` появится запрос. Введите в нём код с экрана
|
|
93
|
+
устройства и нажмите **Approve**. Без открытой localhost-вкладки запрос
|
|
94
|
+
истекает через 2 минуты; консоль сервера печатает только факт запроса,
|
|
95
|
+
без кода.
|
|
96
|
+
|
|
97
|
+
Доверенное устройство работает со всем приложением, включая терминал и
|
|
98
|
+
агентов. Доступ хранится в cookie браузера и в
|
|
99
|
+
`.githuman/ght-reviews.db` (по базе на репозиторий) и истекает через 30 дней
|
|
100
|
+
без активности. Отзыв — в **Devices**: открытые терминал и чаты устройства
|
|
101
|
+
закрываются сразу (если тот же репозиторий открыт вторым сервером на другом порту — в течение 2 секунд). Управлять устройствами можно только с `localhost`.
|
|
102
|
+
|
|
103
|
+
Сервер принимает из сети только адреса своих интерфейсов в заголовке
|
|
104
|
+
`Host` — это защищает от DNS rebinding. Другое имя (например, имя машины в
|
|
105
|
+
Tailscale) разрешается флагом `--allowed-host <имя>`, его можно повторять.
|
|
106
|
+
Прокси на той же машине должен передавать `X-Forwarded-For`, иначе его
|
|
107
|
+
запросы выглядят локальными; `ssh -L` — локальный доступ самого пользователя.
|
|
108
|
+
|
|
109
|
+
> **Осторожно:** соединение идёт по обычному HTTP. Тот, кто слушает ту же
|
|
110
|
+
> сеть Wi-Fi, может перехватить cookie доверенного устройства и получить
|
|
111
|
+
> доступ к терминалу. Используйте LAN-доступ только в сети, которой доверяете.
|
|
112
|
+
> Cookie не разделяются по портам: другой веб-сервер на этой же машине тоже
|
|
113
|
+
> получит cookie устройства.
|
|
72
114
|
|
|
73
115
|
### Агенты
|
|
74
116
|
|
|
@@ -94,10 +136,9 @@ node dist/cli/index.js export last --format markdown -o review.md
|
|
|
94
136
|
`{ "cursor": { "args": ["--model", "<модель>", "acp"] } }`. Допустимы поля
|
|
95
137
|
`command`, `args`, `env`; любое другое (в том числе опечатка) — ошибка
|
|
96
138
|
при запуске сервера.
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`http://localhost:<порт>`.
|
|
139
|
+
- **Кто может запускать.** Агентские маршруты принимают запросы с этой
|
|
140
|
+
машины и с доверенных устройств (см. «Доступ из локальной сети») и
|
|
141
|
+
отклоняют чужие `Origin`/`Host`.
|
|
101
142
|
|
|
102
143
|
### Данные и совместимость с оригинальным mcollina/githuman
|
|
103
144
|
|
|
@@ -200,7 +241,7 @@ MIT, см. [LICENSE](LICENSE).
|
|
|
200
241
|
|
|
201
242
|
Повтор команды текущей половины возвращает обычный размер и позицию. Перетаскивание заголовка из половины тоже возвращает обычный размер; ручной resize снимает режим половины. Сочетания работают при вводе в терминале и формах, но отключены в модальных диалогах. Win означает Super/Meta (на macOS — Command вместе с Ctrl). Системные сочетания ОС могут перехватываться до браузера.
|
|
202
243
|
|
|
203
|
-
Если платформенный бинарник PTY недоступен, терминал работает в ограниченном режиме с баннером: команды вводятся в поле под выводом, Ctrl+C прерывает команду, Ctrl+D закрывает shell. `vim`, `less` и другие интерактивные программы требуют PTY.
|
|
244
|
+
Если платформенный бинарник PTY недоступен, терминал работает в ограниченном режиме с баннером: команды вводятся в поле под выводом, Ctrl+C прерывает команду, Ctrl+D закрывает shell. `vim`, `less` и другие интерактивные программы требуют PTY. Из сети терминал доступен только доверенному устройству. Переключатель исходных цветов отключает минимальный контраст для программ со своей палитрой.
|
|
204
245
|
|
|
205
246
|
Для разработки на другом порту backend можно передать `GITHUMAN_DEV_API=http://localhost:3848` при запуске Quasar dev-сервера.
|
|
206
247
|
|
package/dist/cli/config.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
1
2
|
import { parseArgs } from 'node:util';
|
|
3
|
+
import { isLoopbackHost } from "../shared/network/loopback.js";
|
|
4
|
+
import { WILDCARD_HOSTS } from "../server/utils/lan-addresses.js";
|
|
2
5
|
const DEFAULT_PORT = 3847;
|
|
3
6
|
const DEFAULT_HOST = 'localhost';
|
|
7
|
+
// One DNS name: dot-separated labels of letters, digits and inner hyphens.
|
|
8
|
+
const HOST_NAME_PATTERN = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
|
|
4
9
|
/**
|
|
5
10
|
* Default prefix for filenames written under `.githuman/` (e.g.
|
|
6
11
|
* `ght-reviews.db`). The original mcollina/githuman CLI reads and writes
|
|
@@ -42,8 +47,33 @@ function resolvePort(explicit) {
|
|
|
42
47
|
}
|
|
43
48
|
return parsed;
|
|
44
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Only loopback or every interface (ADR 0079). With one LAN address there
|
|
52
|
+
* is no loopback listener, so no local tab can approve a device.
|
|
53
|
+
*/
|
|
45
54
|
function resolveHost(explicit) {
|
|
46
|
-
|
|
55
|
+
if (explicit === undefined || explicit.length === 0) {
|
|
56
|
+
return DEFAULT_HOST;
|
|
57
|
+
}
|
|
58
|
+
if (WILDCARD_HOSTS.has(explicit) ||
|
|
59
|
+
(isLoopbackHost(explicit) && !explicit.startsWith('['))) {
|
|
60
|
+
return explicit;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Invalid --host value "${explicit}": use localhost (default), or 0.0.0.0 or :: for access from the local network`);
|
|
63
|
+
}
|
|
64
|
+
/** Exact host names only: no scheme, port, path, userinfo or wildcard. */
|
|
65
|
+
function resolveAllowedHosts(values) {
|
|
66
|
+
return values.map(value => {
|
|
67
|
+
const name = String(value).toLowerCase();
|
|
68
|
+
// A loopback name allows nothing: local requests are found by the socket.
|
|
69
|
+
const loopback = name === 'localhost' ||
|
|
70
|
+
name.endsWith('.localhost') ||
|
|
71
|
+
name.startsWith('127.');
|
|
72
|
+
if (loopback || (!HOST_NAME_PATTERN.test(name) && isIP(name) !== 4)) {
|
|
73
|
+
throw new Error(`Invalid --allowed-host value "${String(value)}": give an exact non-loopback host name or IPv4 address, for example machine.tailnet.ts.net. IPv6 literals are not accepted.`);
|
|
74
|
+
}
|
|
75
|
+
return name;
|
|
76
|
+
});
|
|
47
77
|
}
|
|
48
78
|
export function parseServeArgs(argv) {
|
|
49
79
|
const { values } = parseArgs({
|
|
@@ -55,7 +85,8 @@ export function parseServeArgs(argv) {
|
|
|
55
85
|
'no-open': { type: 'boolean', default: false },
|
|
56
86
|
'notes-dir': { type: 'string' },
|
|
57
87
|
'db-prefix': { type: 'string' },
|
|
58
|
-
'api-docs': { type: 'boolean', default: false }
|
|
88
|
+
'api-docs': { type: 'boolean', default: false },
|
|
89
|
+
'allowed-host': { type: 'string', multiple: true }
|
|
59
90
|
},
|
|
60
91
|
strict: false
|
|
61
92
|
});
|
|
@@ -64,12 +95,14 @@ export function parseServeArgs(argv) {
|
|
|
64
95
|
const open = values['no-open'] === true ? false : values.open !== false;
|
|
65
96
|
const dbPrefix = resolveDbPrefix(typeof values['db-prefix'] === 'string' ? values['db-prefix'] : undefined);
|
|
66
97
|
const apiDocs = values['api-docs'] === true;
|
|
98
|
+
const allowedHosts = resolveAllowedHosts(Array.isArray(values['allowed-host']) ? values['allowed-host'] : []);
|
|
67
99
|
return {
|
|
68
100
|
port,
|
|
69
101
|
host,
|
|
70
102
|
open,
|
|
71
103
|
dbPrefix,
|
|
72
104
|
apiDocs,
|
|
105
|
+
allowedHosts,
|
|
73
106
|
...(typeof values['notes-dir'] === 'string'
|
|
74
107
|
? { notesDir: values['notes-dir'] }
|
|
75
108
|
: process.env.GITHUMAN_NOTES_DIR
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { writeFileSync } from 'node:fs';
|
|
|
4
4
|
import { parseServeArgs } from "./config.js";
|
|
5
5
|
import { startServer, resolveRepositoryPath, resolveReviewsDbPath } from "./server-runtime.js";
|
|
6
6
|
import { formatStartupMessage } from "./startup-message.js";
|
|
7
|
+
import { lanAddresses } from "../server/utils/lan-addresses.js";
|
|
7
8
|
import { parseListArgs, runList } from "./commands/list.js";
|
|
8
9
|
import { parseExportArgs, runExport } from "./commands/export.js";
|
|
9
10
|
import { createFileDatabase } from "../server/db/index.js";
|
|
@@ -47,7 +48,7 @@ async function main(argv) {
|
|
|
47
48
|
};
|
|
48
49
|
process.once('SIGINT', shutdown);
|
|
49
50
|
process.once('SIGTERM', shutdown);
|
|
50
|
-
console.log(formatStartupMessage(url, options.host));
|
|
51
|
+
console.log(formatStartupMessage(url, lanAddresses(options.host)));
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
case 'list': {
|
|
@@ -6,8 +6,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { buildApp } from "../server/app.js";
|
|
7
7
|
import { createFileDatabase } from "../server/db/index.js";
|
|
8
8
|
import { resolveDbPrefix } from "./config.js";
|
|
9
|
-
import { isLoopbackHost } from "../shared/network/loopback.js";
|
|
10
9
|
import { loadAgentPresets } from "../server/config/agents.js";
|
|
10
|
+
import { WILDCARD_HOSTS } from "../server/utils/lan-addresses.js";
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
function resolveStaticRoot() {
|
|
13
13
|
// Compiled layout: dist/cli/server-runtime.js sits next to dist/web/.
|
|
@@ -58,6 +58,8 @@ export function resolveAgentsConfigPath(repositoryPath, dbPrefix) {
|
|
|
58
58
|
export async function startServer(options) {
|
|
59
59
|
const repositoryPath = await resolveRepositoryPath(process.cwd());
|
|
60
60
|
const db = createFileDatabase(resolveReviewsDbPath(repositoryPath, options.dbPrefix));
|
|
61
|
+
// Agents and the terminal exist on every bind. Their hooks decide per
|
|
62
|
+
// request: a request from this machine or a trusted device (ADR 0079).
|
|
61
63
|
const app = buildApp({
|
|
62
64
|
staticRoot: resolveStaticRoot(),
|
|
63
65
|
repositoryPath,
|
|
@@ -66,18 +68,19 @@ export async function startServer(options) {
|
|
|
66
68
|
db,
|
|
67
69
|
watchFiles: true,
|
|
68
70
|
apiDocs: options.apiDocs === true,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
? {
|
|
74
|
-
agentPresets: loadAgentPresets(resolveAgentsConfigPath(repositoryPath, options.dbPrefix))
|
|
75
|
-
}
|
|
76
|
-
: {})
|
|
71
|
+
allowedHosts: options.allowedHosts ?? [],
|
|
72
|
+
onPairingRequest: device => console.log(`Pairing request from ${device.name} (${device.address}). Enter the code from the device in a localhost tab.`),
|
|
73
|
+
terminalEnabled: true,
|
|
74
|
+
agentPresets: loadAgentPresets(resolveAgentsConfigPath(repositoryPath, options.dbPrefix))
|
|
77
75
|
});
|
|
78
76
|
const address = await app.listen({ port: options.port, host: options.host });
|
|
77
|
+
// A wildcard bind reports its own address (`http://[::]:3847`), which is
|
|
78
|
+
// not a local Host. The operator tab must use localhost to approve devices.
|
|
79
|
+
const url = WILDCARD_HOSTS.has(options.host)
|
|
80
|
+
? `http://localhost:${new URL(address).port}`
|
|
81
|
+
: address;
|
|
79
82
|
if (options.open) {
|
|
80
|
-
openBrowser(
|
|
83
|
+
openBrowser(url);
|
|
81
84
|
}
|
|
82
|
-
return { app, url
|
|
85
|
+
return { app, url };
|
|
83
86
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export function formatStartupMessage(url,
|
|
1
|
+
import { addressUrl } from "../shared/network/address-url.js";
|
|
2
|
+
export function formatStartupMessage(url, lanAddresses) {
|
|
3
3
|
const lines = [`githuman listening on ${url}`];
|
|
4
|
-
if (
|
|
5
|
-
|
|
4
|
+
if (lanAddresses.length > 0) {
|
|
5
|
+
const port = Number(new URL(url).port);
|
|
6
|
+
lines.push(...lanAddresses.map(address => `Local network: ${addressUrl(address, port)}`), 'Other devices request access at a local network address. Approve them in a localhost tab.', 'Warning: traffic is plain HTTP. Anyone on this network can read it, including device cookies.');
|
|
6
7
|
}
|
|
7
8
|
return lines.join('\n');
|
|
8
9
|
}
|
package/dist/server/app.js
CHANGED
|
@@ -33,8 +33,21 @@ import { createSessionRegistry } from "./services/agent/session-registry.js";
|
|
|
33
33
|
import { createTestDatabase } from "./db/index.js";
|
|
34
34
|
import { createEventBus } from "./event-bus.js";
|
|
35
35
|
import { watchRepository } from "./services/file-watcher.service.js";
|
|
36
|
+
import fastifyCookie from '@fastify/cookie';
|
|
37
|
+
import fastifyPlugin from 'fastify-plugin';
|
|
38
|
+
import { DeviceService } from "./services/device.service.js";
|
|
39
|
+
import { DeviceConnections } from "./services/device-connections.js";
|
|
40
|
+
import { createAccessHook } from "./hooks/access/require-access.js";
|
|
41
|
+
import { deviceRoutes } from "./routes/devices.js";
|
|
42
|
+
import { AllowedHosts } from "./utils/allowed-hosts.js";
|
|
36
43
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- documented workaround above
|
|
37
44
|
const fastifySse = fastifySseImport;
|
|
45
|
+
// Expired records leave the database within this time (ADR 0079, item 8).
|
|
46
|
+
const DEVICE_SWEEP_MS = 60 * 60 * 1000;
|
|
47
|
+
// Open connections of a device that lost access close within this time,
|
|
48
|
+
// also when another server process on the same database revoked it.
|
|
49
|
+
// One indexed SELECT per connected device.
|
|
50
|
+
const DEVICE_RECHECK_MS = 2000;
|
|
38
51
|
export function buildApp(options = {}) {
|
|
39
52
|
const app = Fastify({
|
|
40
53
|
logger: false,
|
|
@@ -51,12 +64,36 @@ export function buildApp(options = {}) {
|
|
|
51
64
|
const repositoryPath = options.repositoryPath ?? process.cwd();
|
|
52
65
|
const db = options.db ?? createTestDatabase();
|
|
53
66
|
const eventBus = options.eventBus ?? createEventBus();
|
|
67
|
+
const devices = new DeviceService(db, {
|
|
68
|
+
...(options.now ? { now: options.now } : {}),
|
|
69
|
+
onChange: () => eventBus.publish({ type: 'devices:changed' })
|
|
70
|
+
});
|
|
71
|
+
const deviceConnections = new DeviceConnections(devices);
|
|
72
|
+
devices.removeExpired();
|
|
73
|
+
const sweep = setInterval(() => devices.removeExpired(), DEVICE_SWEEP_MS);
|
|
74
|
+
sweep.unref();
|
|
75
|
+
const recheck = setInterval(() => deviceConnections.recheck(), DEVICE_RECHECK_MS);
|
|
76
|
+
recheck.unref();
|
|
77
|
+
app.addHook('onClose', async () => {
|
|
78
|
+
clearInterval(sweep);
|
|
79
|
+
clearInterval(recheck);
|
|
80
|
+
});
|
|
54
81
|
// Windows includes a PTY binary, but the shell and process list are Unix-only.
|
|
55
82
|
const terminalEnabled = options.terminalEnabled === true && terminalSupported();
|
|
56
83
|
if (terminalEnabled) {
|
|
57
84
|
app.register(fastifyWebsocket, TERMINAL_WEBSOCKET_OPTIONS);
|
|
58
85
|
}
|
|
59
86
|
app.register(fastifySse);
|
|
87
|
+
app.decorateRequest('access', null);
|
|
88
|
+
app.register(fastifyCookie);
|
|
89
|
+
// Registered after the cookie and WebSocket plugins: the hook needs parsed
|
|
90
|
+
// cookies, and a rejected upgrade before the WebSocket hook leaves the socket open.
|
|
91
|
+
app.register(fastifyPlugin(async (root) => {
|
|
92
|
+
root.addHook('onRequest', createAccessHook({
|
|
93
|
+
devices,
|
|
94
|
+
allowedHosts: new AllowedHosts(options.allowedHosts)
|
|
95
|
+
}));
|
|
96
|
+
}));
|
|
60
97
|
// The spec collects routes as they are added, so it comes before them.
|
|
61
98
|
app.register(fastifySwagger, openApiOptions());
|
|
62
99
|
if (options.apiDocs) {
|
|
@@ -72,10 +109,7 @@ export function buildApp(options = {}) {
|
|
|
72
109
|
});
|
|
73
110
|
const projectPath = options.projectPath ?? repositoryPath;
|
|
74
111
|
const notes = new NotesService(projectPath, resolve(projectPath, options.notesDir ?? '.githuman/notes'), options.notesDir === undefined, eventBus, db);
|
|
75
|
-
app.register(notesRoutes, {
|
|
76
|
-
service: notes,
|
|
77
|
-
loopbackBind: options.loopbackBind === true
|
|
78
|
-
});
|
|
112
|
+
app.register(notesRoutes, { service: notes });
|
|
79
113
|
app.addHook('onClose', async () => {
|
|
80
114
|
await notes.identity.close();
|
|
81
115
|
});
|
|
@@ -90,12 +124,20 @@ export function buildApp(options = {}) {
|
|
|
90
124
|
app.register(reviewRoutes, { repositoryPath, db, eventBus });
|
|
91
125
|
app.register(commentRoutes, { db, eventBus });
|
|
92
126
|
app.register(exportRoutes, { db, repositoryPath });
|
|
127
|
+
app.register(deviceRoutes, {
|
|
128
|
+
devices,
|
|
129
|
+
...(options.onPairingRequest
|
|
130
|
+
? { onPairingRequest: options.onPairingRequest }
|
|
131
|
+
: {})
|
|
132
|
+
});
|
|
93
133
|
app.register(eventRoutes, {
|
|
94
134
|
eventBus,
|
|
135
|
+
connections: deviceConnections,
|
|
95
136
|
instanceId: options.instanceId ?? randomUUID()
|
|
96
137
|
});
|
|
97
138
|
if (terminalEnabled) {
|
|
98
139
|
app.register(terminalRoutes, {
|
|
140
|
+
deviceConnections,
|
|
99
141
|
registry: options.terminalRegistry ??
|
|
100
142
|
new TerminalRegistry({
|
|
101
143
|
repositoryPath,
|
|
@@ -112,7 +154,8 @@ export function buildApp(options = {}) {
|
|
|
112
154
|
}),
|
|
113
155
|
db,
|
|
114
156
|
repositoryPath,
|
|
115
|
-
eventBus
|
|
157
|
+
eventBus,
|
|
158
|
+
connections: deviceConnections
|
|
116
159
|
});
|
|
117
160
|
}
|
|
118
161
|
if (options.watchFiles) {
|
|
@@ -87,6 +87,25 @@ export function applyMigrations(db) {
|
|
|
87
87
|
ON review_files(file_path);
|
|
88
88
|
CREATE INDEX IF NOT EXISTS review_files_review_id_idx
|
|
89
89
|
ON review_files(review_id);
|
|
90
|
+
|
|
91
|
+
-- One row: the random id in the device cookie name (ADR 0079).
|
|
92
|
+
CREATE TABLE IF NOT EXISTS server_identity (
|
|
93
|
+
id TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
name TEXT NOT NULL,
|
|
99
|
+
secret_hash TEXT NOT NULL UNIQUE,
|
|
100
|
+
state TEXT NOT NULL,
|
|
101
|
+
code TEXT NOT NULL,
|
|
102
|
+
host TEXT NOT NULL,
|
|
103
|
+
address TEXT NOT NULL,
|
|
104
|
+
created_at TEXT NOT NULL,
|
|
105
|
+
expires_at TEXT NOT NULL,
|
|
106
|
+
last_seen_at TEXT NOT NULL
|
|
107
|
+
);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS devices_address_idx ON devices(address);
|
|
90
109
|
`);
|
|
91
110
|
const noteColumns = db
|
|
92
111
|
.prepare('PRAGMA table_info(note_catalog)')
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import createError from '@fastify/error';
|
|
2
|
+
export const DeviceNotFoundError = createError('GHT_DEVICE_NOT_FOUND', '%s', 404);
|
|
3
|
+
/** The pairing request exists but its time is over. */
|
|
4
|
+
export const PairingExpiredError = createError('GHT_PAIRING_EXPIRED', '%s', 410);
|
|
5
|
+
/** The code does not match, or the device is already trusted. */
|
|
6
|
+
export const PairingConflictError = createError('GHT_PAIRING_CONFLICT', '%s', 409);
|
|
7
|
+
export const PairingRateLimitError = createError('GHT_PAIRING_RATE_LIMIT', '%s', 429);
|
|
@@ -2,3 +2,4 @@ import createError from '@fastify/error';
|
|
|
2
2
|
export const BadRequestError = createError('GHT_HTTP_BAD_REQUEST', '%s', 400);
|
|
3
3
|
export const NotFoundError = createError('GHT_HTTP_NOT_FOUND', '%s', 404);
|
|
4
4
|
export const ForbiddenError = createError('GHT_HTTP_FORBIDDEN', '%s', 403);
|
|
5
|
+
export const UnauthorizedError = createError('GHT_HTTP_UNAUTHORIZED', '%s', 401);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isLoopbackHost } from "../../../shared/network/loopback.js";
|
|
2
|
+
import { forwardedAddresses, isLoopbackAddress, parseHost, sameOrigin } from "../../utils/request-origin.js";
|
|
3
|
+
/**
|
|
4
|
+
* True when the request comes from a page or a client on this machine
|
|
5
|
+
* (ADR 0079). The `Host` header alone is not proof: with a bind on
|
|
6
|
+
* `0.0.0.0` any client sends `Host: localhost`. A proxy on this machine
|
|
7
|
+
* (the Quasar dev server, Tailscale Serve) connects from loopback, so a
|
|
8
|
+
* non-loopback `X-Forwarded-For` entry also makes the request remote.
|
|
9
|
+
* The loopback `Host` stays as the defence against DNS rebinding.
|
|
10
|
+
*/
|
|
11
|
+
export function isLocalRequest(request) {
|
|
12
|
+
const { host, origin } = request.headers;
|
|
13
|
+
const forwarded = forwardedAddresses(request.headers['x-forwarded-for']);
|
|
14
|
+
const target = parseHost(host);
|
|
15
|
+
return (isLoopbackAddress(request.socket.remoteAddress) &&
|
|
16
|
+
forwarded !== undefined &&
|
|
17
|
+
forwarded.every(isLoopbackAddress) &&
|
|
18
|
+
target !== undefined &&
|
|
19
|
+
isLoopbackHost(target.hostname) &&
|
|
20
|
+
(origin === undefined || sameOrigin(origin, host)));
|
|
21
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DEVICE_IDLE_TTL_MS } from "../../../shared/devices/constants.js";
|
|
2
|
+
import { ForbiddenError, UnauthorizedError } from "../../errors/http.js";
|
|
3
|
+
import { clientAddress, sameOrigin } from "../../utils/request-origin.js";
|
|
4
|
+
import { isLocalRequest } from "./local-request.js";
|
|
5
|
+
// Methods that do not change state; CSRF checks the others.
|
|
6
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
7
|
+
// API routes that a browser without access needs to request pairing.
|
|
8
|
+
const PUBLIC_ROUTES = new Set([
|
|
9
|
+
'GET /api/access',
|
|
10
|
+
'HEAD /api/access',
|
|
11
|
+
'POST /api/devices/requests'
|
|
12
|
+
]);
|
|
13
|
+
export function deviceCookieName(serverId) {
|
|
14
|
+
return `ght_device_${serverId}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The cookie lives 30 days from the request or the last renewal. The
|
|
18
|
+
* database decides if the secret still gives access (ADR 0079, item 8).
|
|
19
|
+
* There is no `Secure` flag: the server speaks plain HTTP.
|
|
20
|
+
*/
|
|
21
|
+
export function setDeviceCookie(reply, serverId, secret) {
|
|
22
|
+
reply.setCookie(deviceCookieName(serverId), secret, {
|
|
23
|
+
httpOnly: true,
|
|
24
|
+
sameSite: 'strict',
|
|
25
|
+
path: '/',
|
|
26
|
+
maxAge: DEVICE_IDLE_TTL_MS / 1000
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function clearDeviceCookie(reply, serverId) {
|
|
30
|
+
reply.clearCookie(deviceCookieName(serverId), { path: '/' });
|
|
31
|
+
}
|
|
32
|
+
export function deviceSecret(request, serverId) {
|
|
33
|
+
return request.cookies[deviceCookieName(serverId)];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Global `onRequest` hook (ADR 0079, item 9). It runs for every bind, and
|
|
37
|
+
* decides by the matched route, not by `request.url`: Fastify finds
|
|
38
|
+
* `/api/reviews` for `/%61pi/reviews`. Static files, `/health` and the SPA
|
|
39
|
+
* fallback carry no data and pass.
|
|
40
|
+
*/
|
|
41
|
+
export function createAccessHook(options) {
|
|
42
|
+
const { devices, allowedHosts } = options;
|
|
43
|
+
return async function requireAccess(request, reply) {
|
|
44
|
+
const route = request.routeOptions.url;
|
|
45
|
+
const api = request.url.startsWith('/api/') || route?.startsWith('/api/') === true;
|
|
46
|
+
if (!api)
|
|
47
|
+
return;
|
|
48
|
+
reply.header('cache-control', 'no-store');
|
|
49
|
+
const { host, origin } = request.headers;
|
|
50
|
+
const local = isLocalRequest(request);
|
|
51
|
+
if (!local && !allowedHosts.allows(host)) {
|
|
52
|
+
throw new ForbiddenError(`Unknown Host "${host ?? '-'}". Open the app by the IP address of this machine, or start the server with --allowed-host.`);
|
|
53
|
+
}
|
|
54
|
+
if (origin !== undefined &&
|
|
55
|
+
!SAFE_METHODS.has(request.method) &&
|
|
56
|
+
!sameOrigin(origin, host)) {
|
|
57
|
+
throw new ForbiddenError('The request Origin does not match the server.');
|
|
58
|
+
}
|
|
59
|
+
if (local) {
|
|
60
|
+
request.access = { local: true, device: { status: 'none' } };
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const device = devices.access(deviceSecret(request, devices.serverId), clientAddress(request.socket.remoteAddress, request.headers['x-forwarded-for']));
|
|
64
|
+
request.access = { local: false, device };
|
|
65
|
+
if (device.status === 'device' && device.renewed) {
|
|
66
|
+
setDeviceCookie(reply, devices.serverId, deviceSecret(request, devices.serverId) ?? '');
|
|
67
|
+
}
|
|
68
|
+
if (route !== undefined &&
|
|
69
|
+
PUBLIC_ROUTES.has(`${request.method} ${route}`)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (device.status !== 'device') {
|
|
73
|
+
throw new UnauthorizedError('This device has no access. Open the app to request access.');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* True for a request of a trusted device that the access hook let through.
|
|
79
|
+
* A browser sends no `Origin` on a same-origin GET or EventSource, so the
|
|
80
|
+
* check needs `Origin` only when `originRequired` is set.
|
|
81
|
+
*/
|
|
82
|
+
export function isTrustedDeviceRequest(request, originRequired) {
|
|
83
|
+
if (request.access?.device.status !== 'device')
|
|
84
|
+
return false;
|
|
85
|
+
const { host, origin } = request.headers;
|
|
86
|
+
return origin === undefined ? !originRequired : sameOrigin(origin, host);
|
|
87
|
+
}
|
|
88
|
+
/** Route hook for device management: only this machine may approve or revoke. */
|
|
89
|
+
export async function requireLocalRequest(request) {
|
|
90
|
+
if (!isLocalRequest(request)) {
|
|
91
|
+
throw new ForbiddenError('Only this machine manages devices. Open the app via localhost.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
import { ForbiddenError } from "../../errors/http.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
// "[::1]:3847" → "[::1]"; "localhost:3847" → "localhost"
|
|
5
|
-
return hostWithPort.startsWith('[')
|
|
6
|
-
? hostWithPort.slice(0, hostWithPort.indexOf(']') + 1)
|
|
7
|
-
: (hostWithPort.split(':')[0] ?? '');
|
|
8
|
-
}
|
|
2
|
+
import { isLocalRequest } from "../access/local-request.js";
|
|
3
|
+
import { isTrustedDeviceRequest } from "../access/require-access.js";
|
|
9
4
|
/**
|
|
10
|
-
* `onRequest` hook for the agent routes (ADR 0023). They are an
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* when the browser sends an `Origin`, one identical to that `Host`.
|
|
15
|
-
* Non-browser clients (curl, tests) send no `Origin` and pass.
|
|
5
|
+
* `onRequest` hook for the agent routes (ADR 0023, ADR 0079). They are an
|
|
6
|
+
* RCE surface, so each request must come from this machine (see
|
|
7
|
+
* `isLocalRequest`) or from a trusted device. Non-browser clients (curl,
|
|
8
|
+
* tests) send no `Origin` and pass.
|
|
16
9
|
*/
|
|
17
10
|
export async function requireLocalOrigin(request) {
|
|
18
|
-
|
|
19
|
-
const origin = request.headers.origin;
|
|
20
|
-
let ok = isLoopbackHost(hostnameOf(host));
|
|
21
|
-
if (ok && origin !== undefined) {
|
|
22
|
-
try {
|
|
23
|
-
ok = new URL(origin).host === host;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
ok = false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (ok) {
|
|
11
|
+
if (isLocalRequest(request) || isTrustedDeviceRequest(request, false)) {
|
|
30
12
|
return;
|
|
31
13
|
}
|
|
32
|
-
|
|
14
|
+
const { host, origin } = request.headers;
|
|
15
|
+
throw new ForbiddenError(`Agent endpoints only accept requests from this machine or a trusted device (Host: ${host || '-'}, Origin: ${origin ?? '-'}).`);
|
|
33
16
|
}
|
|
@@ -1,37 +1,10 @@
|
|
|
1
|
-
import { isLoopbackHost } from "../../../shared/network/loopback.js";
|
|
2
1
|
import { ForbiddenError } from "../../errors/http.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { sameOrigin } from "../../utils/request-origin.js";
|
|
3
|
+
// The global access hook decides who may read and write notes (ADR 0079).
|
|
4
|
+
// Notes also reject a foreign Origin on reads, unlike other routes.
|
|
5
|
+
export async function requireNotesOrigin(request) {
|
|
6
|
+
const { host, origin } = request.headers;
|
|
7
|
+
if (origin && !sameOrigin(origin, host)) {
|
|
8
|
+
throw new ForbiddenError('Notes require the same origin as this server');
|
|
9
9
|
}
|
|
10
|
-
catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function isLoopbackRequestHost(host) {
|
|
15
|
-
if (!host)
|
|
16
|
-
return false;
|
|
17
|
-
try {
|
|
18
|
-
return isLoopbackHost(new URL(`http://${host}`).hostname);
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
// A loopback bind also needs a loopback Host. A rebound hostile domain sends
|
|
25
|
-
// matching Origin and Host, so the match alone does not stop DNS rebinding.
|
|
26
|
-
// ADR 0008 allows LAN access without a password, so other binds skip this check.
|
|
27
|
-
export function createNotesOriginHook(options) {
|
|
28
|
-
return async function requireNotesOrigin(request) {
|
|
29
|
-
const { host, origin } = request.headers;
|
|
30
|
-
if (options.loopbackBind && !isLoopbackRequestHost(host)) {
|
|
31
|
-
throw new ForbiddenError('Notes only accept requests from this local server. Open the app via localhost.');
|
|
32
|
-
}
|
|
33
|
-
if (origin && !isSameOrigin(host, origin)) {
|
|
34
|
-
throw new ForbiddenError('Notes require the same origin as this server');
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
10
|
}
|
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
import { isLoopbackHost } from "../../../shared/network/loopback.js";
|
|
2
1
|
import { ForbiddenError } from "../../errors/http.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
const source = new URL(origin);
|
|
9
|
-
const target = new URL(`http://${host}`);
|
|
10
|
-
return (isLoopbackHost(target.hostname) &&
|
|
11
|
-
target.host === host &&
|
|
12
|
-
source.host === target.host &&
|
|
13
|
-
source.protocol === 'http:' &&
|
|
14
|
-
source.origin === origin &&
|
|
15
|
-
!target.username &&
|
|
16
|
-
!target.password);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
2
|
+
import { isLocalRequest } from "../access/local-request.js";
|
|
3
|
+
import { isTrustedDeviceRequest } from "../access/require-access.js";
|
|
4
|
+
// A browser always sends `Origin` on a WebSocket upgrade and a POST.
|
|
5
|
+
// Without it the token could not be bound to a page (ADR 0037).
|
|
22
6
|
export async function requireTerminalOrigin(request) {
|
|
23
|
-
|
|
24
|
-
|
|
7
|
+
const allowed = request.headers.origin !== undefined &&
|
|
8
|
+
(isLocalRequest(request) || isTrustedDeviceRequest(request, true));
|
|
9
|
+
if (!allowed) {
|
|
10
|
+
throw new ForbiddenError('Terminal endpoints only accept requests from this machine or a trusted device.');
|
|
25
11
|
}
|
|
26
12
|
}
|