@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.
Files changed (237) hide show
  1. package/README.md +50 -9
  2. package/dist/cli/config.js +35 -2
  3. package/dist/cli/index.js +2 -1
  4. package/dist/cli/server-runtime.js +14 -11
  5. package/dist/cli/startup-message.js +5 -4
  6. package/dist/server/app.js +48 -5
  7. package/dist/server/db/migrations.js +19 -0
  8. package/dist/server/errors/devices.js +7 -0
  9. package/dist/server/errors/http.js +1 -0
  10. package/dist/server/hooks/access/local-request.js +21 -0
  11. package/dist/server/hooks/access/require-access.js +93 -0
  12. package/dist/server/hooks/agents/local-origin.js +9 -26
  13. package/dist/server/hooks/notes/same-origin.js +7 -34
  14. package/dist/server/hooks/terminal/local-origin.js +8 -22
  15. package/dist/server/repositories/device.repo.js +74 -0
  16. package/dist/server/routes/agent.js +11 -2
  17. package/dist/server/routes/devices.js +130 -0
  18. package/dist/server/routes/events.js +7 -1
  19. package/dist/server/routes/notes.js +3 -3
  20. package/dist/server/routes/terminal.js +10 -2
  21. package/dist/server/services/device-connections.js +79 -0
  22. package/dist/server/services/device.service.js +222 -0
  23. package/dist/server/services/notes-external-assets.service.js +5 -5
  24. package/dist/server/services/notes-identity.service.js +4 -2
  25. package/dist/server/services/terminal/tokens.js +1 -1
  26. package/dist/server/utils/address-key.js +33 -0
  27. package/dist/server/utils/allowed-hosts.js +38 -0
  28. package/dist/server/utils/device-name.js +27 -0
  29. package/dist/server/utils/lan-addresses.js +35 -0
  30. package/dist/server/utils/remote-asset-request.js +9 -2
  31. package/dist/server/utils/request-origin.js +105 -0
  32. package/dist/shared/devices/constants.js +16 -0
  33. package/dist/shared/devices/name.js +12 -0
  34. package/dist/shared/devices/schemas.js +67 -0
  35. package/dist/shared/devices/types.js +1 -0
  36. package/dist/shared/events/types.js +2 -1
  37. package/dist/shared/network/address-url.js +4 -0
  38. package/dist/shared/utils/date.js +3 -0
  39. package/dist/web/assets/AccessPage-_OKn9U0P.js +1 -0
  40. package/dist/web/assets/AccessPage-l5Yiiw9T.css +1 -0
  41. package/dist/web/assets/AppPage-oQ9Rz0be.js +1 -0
  42. package/dist/web/assets/CreateButton-D1EtoicG.js +1 -0
  43. package/dist/web/assets/CreateReviewFabButton-B2Td8Wno.css +1 -0
  44. package/dist/web/assets/CreateReviewFabButton-BxvT3i9e.js +1 -0
  45. package/dist/web/assets/ErrorNotFound-DtnO85PU.js +1 -0
  46. package/dist/web/assets/IndexPage-D5PvyGrX.css +1 -0
  47. package/dist/web/assets/IndexPage-gdD7BHxA.js +1 -0
  48. package/dist/web/assets/MainLayout-47PTLKZf.js +6 -0
  49. package/dist/web/assets/MainLayout-ikSrFqBh.css +1 -0
  50. package/dist/web/assets/NoteVisualEditor-BQ6mVfj0.js +1054 -0
  51. package/dist/web/assets/{NoteVisualEditor-IH3s3d3W.css → NoteVisualEditor-BYikrY7Q.css} +1 -1
  52. package/dist/web/assets/NotesPage-DNR9iW_G.js +4 -0
  53. package/dist/web/assets/NotesPage-_8naQciW.css +1 -0
  54. package/dist/web/assets/PanelResize-CAQFVc8b.js +1 -0
  55. package/dist/web/assets/PanelResize-XjrbOj2S.css +1 -0
  56. package/dist/web/assets/{PdfPreview-DgwnGJ2E.js → PdfPreview-kL_nk9my.js} +2 -2
  57. package/dist/web/assets/PdfPreview-l_xzsLJ_.css +1 -0
  58. package/dist/web/assets/{QBtn-DoggJe1Q.js → QBtn-CB-yw_iP.js} +1 -1
  59. package/dist/web/assets/QExpansionItem-Dr5oXO0m.js +4 -0
  60. package/dist/web/assets/QFab-DPz8RiP0.js +1 -0
  61. package/dist/web/assets/QForm-CASiKhGm.js +1 -0
  62. package/dist/web/assets/{QInput-DJgTh2Qy.js → QInput-k34nCPWg.js} +1 -1
  63. package/dist/web/assets/QSelect-CaKn9Tkg.js +1 -0
  64. package/dist/web/assets/QSpace-Di_bZQY6.js +1 -0
  65. package/dist/web/assets/QVirtualScroll-oGN_Tl5J.js +1 -0
  66. package/dist/web/assets/ReviewDetailPage-BKgIprY0.js +1 -0
  67. package/dist/web/assets/ReviewsPage-6PJ6wofN.js +1 -0
  68. package/dist/web/assets/{StatusSwitcher-BCv_9MtI.css → StatusSwitcher-D6i8_OfR.css} +1 -1
  69. package/dist/web/assets/StatusSwitcher-s9w6iKpR.js +1 -0
  70. package/dist/web/assets/{TerminalView-C1jFlfLH.css → TerminalView-617DZuPt.css} +1 -1
  71. package/dist/web/assets/TerminalView-BsO_SZut.js +19 -0
  72. package/dist/web/assets/{abnfDiagram-O67JEVCF-BYlDf7LI.js → abnfDiagram-O67JEVCF-qUSrlp_c.js} +1 -1
  73. package/dist/web/assets/access-store-Q8nkv4Nb.js +1 -0
  74. package/dist/web/assets/agent-store-D2jgeBgi.js +113 -0
  75. package/dist/web/assets/architecture-7GRP2DOG-D6PShS0_.js +1 -0
  76. package/dist/web/assets/{architectureDiagram-NJMV4G6O-CC0SmfK_.js → architectureDiagram-NJMV4G6O-D7RC6OS2.js} +1 -1
  77. package/dist/web/assets/{blockDiagram-BEXU5L5S-C1Ny76Gc.js → blockDiagram-BEXU5L5S-CmNtTpfK.js} +1 -1
  78. package/dist/web/assets/{c4Diagram-YGBWAQC7-CP2if62Y.js → c4Diagram-YGBWAQC7-CKCcu1qx.js} +1 -1
  79. package/dist/web/assets/channel-D6lb5DbY.js +1 -0
  80. package/dist/web/assets/{chunk-3FUC2YCW-cjM6hj3T.js → chunk-3FUC2YCW-Drz6oeKx.js} +1 -1
  81. package/dist/web/assets/{chunk-5DYCD2WN-DAW_EwWQ.js → chunk-5DYCD2WN-RItzEgV_.js} +1 -1
  82. package/dist/web/assets/{chunk-742MDFTN-nkStFRF-.js → chunk-742MDFTN-D6hKdOv7.js} +1 -1
  83. package/dist/web/assets/{chunk-7INBJB4K-EQQsEy8I.js → chunk-7INBJB4K-Dk7-Eior.js} +1 -1
  84. package/dist/web/assets/{chunk-7M6MHVWA-DUJ2C21w.js → chunk-7M6MHVWA-Cw8E0Nkn.js} +1 -1
  85. package/dist/web/assets/{chunk-7PRAP22T-C1Bj5-NO.js → chunk-7PRAP22T-t-mTeNXF.js} +1 -1
  86. package/dist/web/assets/{chunk-GTNCS2PH-DW39t1bj.js → chunk-GTNCS2PH-C7j2atV8.js} +1 -1
  87. package/dist/web/assets/{chunk-GWA4HPMP-i49YwHnS.js → chunk-GWA4HPMP-CH9T9LZO.js} +1 -1
  88. package/dist/web/assets/{chunk-MBY4JIJT-C4mXlV61.js → chunk-MBY4JIJT-yXOO37xD.js} +1 -1
  89. package/dist/web/assets/{chunk-NETBCI7D-DoRSO-kq.js → chunk-NETBCI7D-C-s2UtAR.js} +1 -1
  90. package/dist/web/assets/{chunk-O7XYJQB3-D38ZePQT.js → chunk-O7XYJQB3-5Me_aV8o.js} +1 -1
  91. package/dist/web/assets/{chunk-UA2S7LBM-DmOI-u6c.js → chunk-UA2S7LBM-BQaibeff.js} +1 -1
  92. package/dist/web/assets/{chunk-WEXAMYUT-Dv710fF3.js → chunk-WEXAMYUT-UBcOE4Lf.js} +1 -1
  93. package/dist/web/assets/{chunk-Z7XXMR3K-CWHlAHIu.js → chunk-Z7XXMR3K-BsA8o3Pt.js} +1 -1
  94. package/dist/web/assets/{chunk-ZIGJFQKS-Bid5tGRk.js → chunk-ZIGJFQKS--KwSI1U0.js} +1 -1
  95. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-C1770G1t.js → classDiagram-v2-NBCMYWYE-pMF8oCmq.js} +1 -1
  96. package/dist/web/assets/create-B9Tn5Xvi.js +1 -0
  97. package/dist/web/assets/{cynefin-OW5HDTMX-CWxF4ZIN.js → cynefin-OW5HDTMX-DFgtXolF.js} +1 -1
  98. package/dist/web/assets/{cynefinDiagram-VND7K2PF-DcFBTI6m.js → cynefinDiagram-VND7K2PF-CPY0JRyq.js} +1 -1
  99. package/dist/web/assets/{dagre-6A5THRUB-CP5wTvgN.js → dagre-6A5THRUB-KdjJKAB7.js} +1 -1
  100. package/dist/web/assets/devices-store-CrZ4MxyY.js +1 -0
  101. package/dist/web/assets/{diagram-22UHCM2B-mLUm1v32.js → diagram-22UHCM2B-BDcOcvF_.js} +1 -1
  102. package/dist/web/assets/{diagram-3UASUU5V-CP0OFpuS.js → diagram-3UASUU5V-6j2nlZL3.js} +1 -1
  103. package/dist/web/assets/{diagram-ATOU4E4O-DolFulHH.js → diagram-ATOU4E4O-BN8cdxED.js} +1 -1
  104. package/dist/web/assets/{diagram-CDSNMT55-Bs17AhwI.js → diagram-CDSNMT55-cWPiXHkr.js} +1 -1
  105. package/dist/web/assets/{diagram-MLGK6HIB-BfhsJwV7.js → diagram-MLGK6HIB-DcgkH_Vl.js} +1 -1
  106. package/dist/web/assets/{diagram-MPIPVDR6-B1c-Ikp-.js → diagram-MPIPVDR6-D_e86L99.js} +1 -1
  107. package/dist/web/assets/diff-store-ByxMCKoa.js +93 -0
  108. package/dist/web/assets/{dist-CyyHluOV.js → dist-BMFtkY2y.js} +1 -1
  109. package/dist/web/assets/{dist-d8yBJi6g.js → dist-BP40mvwJ.js} +1 -1
  110. package/dist/web/assets/{dist-zqKz9Qrf.js → dist-BVVhgjWT.js} +1 -1
  111. package/dist/web/assets/{dist-BsjKEQwP.js → dist-Bdsq-Ze1.js} +1 -1
  112. package/dist/web/assets/{dist-Bm8OAr9J.js → dist-BwpyANeW.js} +1 -1
  113. package/dist/web/assets/{dist-DkKvAWYi.js → dist-Bz40Nd9I.js} +1 -1
  114. package/dist/web/assets/{dist-yQ-fH4og.js → dist-C9enVshI.js} +1 -1
  115. package/dist/web/assets/{dist-DuW-c5so.js → dist-CLCP7f3x.js} +1 -1
  116. package/dist/web/assets/{dist-C6YiC_nY.js → dist-CU43ytsh.js} +1 -1
  117. package/dist/web/assets/{dist-g0NXSFab.js → dist-CjkUzU4s.js} +1 -1
  118. package/dist/web/assets/{dist-D6WXR3TF.js → dist-MUQaJRtC.js} +1 -1
  119. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-BvQ60ZD9.js → ebnfDiagram-ZINNZB2B-D8xHOFOi.js} +1 -1
  120. package/dist/web/assets/{elk-276RUBZZ-C27WMGnY.js → elk-276RUBZZ-JiDwNyLy.js} +1 -1
  121. package/dist/web/assets/{erDiagram-OPXOYQCR-CdfkOXb9.js → erDiagram-OPXOYQCR-D3J6u81a.js} +1 -1
  122. package/dist/web/assets/eventmodeling-NTZA5JFV-QCwZLvmm.js +1 -0
  123. package/dist/web/assets/flowDiagram-KWPJA3E3-CgF90hDi.js +1 -0
  124. package/dist/web/assets/{ganttDiagram-FUAMR5RP-CuKNmNbv.js → ganttDiagram-FUAMR5RP-nvAVhbgI.js} +1 -1
  125. package/dist/web/assets/{gitGraph-4MIJSDKK-DBGr6EfP.js → gitGraph-4MIJSDKK-Bv_wkB2T.js} +1 -1
  126. package/dist/web/assets/{gitGraphDiagram-X574FWY7-BUZgiyYd.js → gitGraphDiagram-X574FWY7-DxxwadhH.js} +1 -1
  127. package/dist/web/assets/i18n-DDCqtTTW.js +1 -0
  128. package/dist/web/assets/index-BNHt6zmP.js +2 -0
  129. package/dist/web/assets/index-BwbM_RKi.css +1 -0
  130. package/dist/web/assets/{info-A6RAGUB7-CjnEima-.js → info-A6RAGUB7-Dw18yeIE.js} +1 -1
  131. package/dist/web/assets/{infoDiagram-VRGFBTTK-D91sa8do.js → infoDiagram-VRGFBTTK-C3BrgjYg.js} +1 -1
  132. package/dist/web/assets/init-app-BouXTk8p.js +1 -0
  133. package/dist/web/assets/{is-CE4jkrGQ.js → is-D0waCELe.js} +1 -1
  134. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-2K-kH65E.js → ishikawaDiagram-OU5B5YK6-DU2p9szX.js} +1 -1
  135. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CkENPVJA.js → journeyDiagram-ZHPQQLJL-Bs2zQnew.js} +1 -1
  136. package/dist/web/assets/{kanban-definition-PNTS6WVX-BHJODz7s.js → kanban-definition-PNTS6WVX-pqXd5uNi.js} +1 -1
  137. package/dist/web/assets/{line-C6VvIL_x.js → line-CN4Dxpei.js} +1 -1
  138. package/dist/web/assets/{markdown-192Ev3_O.js → markdown-jR0fhe4d.js} +1 -1
  139. package/dist/web/assets/{mermaid-parser.core-DMJg-lnr.js → mermaid-parser.core-DZqDOGRM.js} +3 -3
  140. package/dist/web/assets/{mermaid.core-Dd40xnyQ.js → mermaid.core-CUndCvkz.js} +4 -4
  141. package/dist/web/assets/{mindmap-definition-NLK3R4M7-DhfAMSga.js → mindmap-definition-NLK3R4M7-DyK7YqB7.js} +1 -1
  142. package/dist/web/assets/{packet-AYTQ26CC-DZUk7rEw.js → packet-AYTQ26CC-DvlW_0BP.js} +1 -1
  143. package/dist/web/assets/{pegDiagram-GJSIUBJH-W22cBCh2.js → pegDiagram-GJSIUBJH-k2e_-FWQ.js} +1 -1
  144. package/dist/web/assets/{pie-WAS4IAKB-BMvwPiHK.js → pie-WAS4IAKB-B99zaYjO.js} +1 -1
  145. package/dist/web/assets/{pieDiagram-5QR66LMP-CZ2EVCmx.js → pieDiagram-5QR66LMP-BPbS05lK.js} +1 -1
  146. package/dist/web/assets/pinia-BETEFLDG.js +1 -0
  147. package/dist/web/assets/pinia-DO0AdKGg.js +1 -0
  148. package/dist/web/assets/{quadrantDiagram-O4NWA36T-DBS45bbr.js → quadrantDiagram-O4NWA36T-DqNNdsd2.js} +1 -1
  149. package/dist/web/assets/{radar-RG4KPBEZ-BDkD2JF1.js → radar-RG4KPBEZ-BIAAr9LO.js} +1 -1
  150. package/dist/web/assets/{railroad-74A4TZTK-BH_sA5bU.js → railroad-74A4TZTK-DYxr-1-f.js} +1 -1
  151. package/dist/web/assets/railroad-abnf-HS5TGJTU-GESjhc3t.js +1 -0
  152. package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cybu0A7J.js +1 -0
  153. package/dist/web/assets/railroad-peg-WCYAUIDC-DCHxrtbI.js +1 -0
  154. package/dist/web/assets/{railroadDiagram-XR7U4H2S-CrI1TIG1.js → railroadDiagram-XR7U4H2S-TNg-LqRR.js} +1 -1
  155. package/dist/web/assets/{requirementDiagram-PLB6GJNP-BbtaFtGn.js → requirementDiagram-PLB6GJNP-hKAyJR6W.js} +1 -1
  156. package/dist/web/assets/runtime-core.esm-bundler-CAvPiMpd.js +1 -0
  157. package/dist/web/assets/{safe-storage-DekXHbgc.js → safe-storage-B1Aq5CGi.js} +1 -1
  158. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DRU9tLf3.js → sankeyDiagram-IPEJSGJF-_MSL-tES.js} +1 -1
  159. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-RquVuniP.js → sequenceDiagram-PO4LG4MO-Byq-joBG.js} +1 -1
  160. package/dist/web/assets/single-flight-DLCbzLI3.js +1 -0
  161. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-CKjC8JTS.js → stateDiagram-v2-GCMORJYK-BR2jAgaa.js} +1 -1
  162. package/dist/web/assets/{swimlanes-2SLR337P-B-5FN1NY.js → swimlanes-2SLR337P-Nt8ojGKK.js} +1 -1
  163. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-Ds1hVYqh.js +8 -0
  164. package/dist/web/assets/terminal-store-D0abibMa.js +1 -0
  165. package/dist/web/assets/theme-QEcOl0lf.js +1 -0
  166. package/dist/web/assets/{timeline-definition-EJHVYXUP-DbG_YVkW.js → timeline-definition-EJHVYXUP-BtihLvxk.js} +1 -1
  167. package/dist/web/assets/{treeView-Q6P3EWNA-BgkrW_FP.js → treeView-Q6P3EWNA-D5HzXeKd.js} +1 -1
  168. package/dist/web/assets/{treemap-WGGIJYW6-Dp8ISpo7.js → treemap-WGGIJYW6-Dovjuutg.js} +1 -1
  169. package/dist/web/assets/use-add-agent-context-BaPUIO93.js +1 -0
  170. package/dist/web/assets/use-app-theme-B1cdbnHn.js +1 -0
  171. package/dist/web/assets/use-countdown-CTag4T13.js +1 -0
  172. package/dist/web/assets/use-panel-width-CB-0S3zX.js +1 -0
  173. package/dist/web/assets/use-router-link-Dr9wnnyZ.js +1 -0
  174. package/dist/web/assets/use-row-height-DQHVMtUe.js +1 -0
  175. package/dist/web/assets/use-server-events-BrBCbeHu.js +1 -0
  176. package/dist/web/assets/{use-virtual-scroll-BSXYZjZ5.js → use-virtual-scroll-DG0UKjVk.js} +1 -1
  177. package/dist/web/assets/{usecaseDiagram-POWQR4AR-DwRXMhnD.js → usecaseDiagram-POWQR4AR-DSwRE8zj.js} +1 -1
  178. package/dist/web/assets/{vennDiagram-UO4OBE2U-CxdeHvjU.js → vennDiagram-UO4OBE2U-C-B8yP1K.js} +1 -1
  179. package/dist/web/assets/{vue-i18n.runtime-BpRrzZrT.js → vue-i18n.runtime-CEtihHHL.js} +2 -2
  180. package/dist/web/assets/{wardley-WFR3VGLG-C8gq8AfM.js → wardley-WFR3VGLG-wWgaZuh6.js} +1 -1
  181. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-C9Jo2KAj.js → wardleyDiagram-VNRHLVJA-DbhenrHd.js} +1 -1
  182. package/dist/web/assets/weighted-lru-BSxRnMHR.js +1 -0
  183. package/dist/web/assets/{xychartDiagram-PMCCYNJV-D3tp7hdU.js → xychartDiagram-PMCCYNJV-CxFDmAEt.js} +1 -1
  184. package/dist/web/index.html +8 -8
  185. package/package.json +3 -1
  186. package/dist/web/assets/AppPage-DdmqiV2R.js +0 -1
  187. package/dist/web/assets/ClosePopup-DhjIPp8s.js +0 -1
  188. package/dist/web/assets/CreateButton-BoxoJ7Jr.js +0 -93
  189. package/dist/web/assets/CreateReviewFabButton-CA5OsG7d.js +0 -1
  190. package/dist/web/assets/CreateReviewFabButton-Cufd8hVJ.css +0 -1
  191. package/dist/web/assets/ErrorNotFound-Cq0wNQMu.js +0 -1
  192. package/dist/web/assets/IndexPage-CROp8XNF.js +0 -1
  193. package/dist/web/assets/IndexPage-F8MMAAkn.css +0 -1
  194. package/dist/web/assets/MainLayout-DM30ynlR.css +0 -1
  195. package/dist/web/assets/MainLayout-DvfVDB2G.js +0 -6
  196. package/dist/web/assets/NoteVisualEditor-VfLNFzsD.js +0 -1054
  197. package/dist/web/assets/NotesPage-D_NiwDh8.js +0 -4
  198. package/dist/web/assets/NotesPage-KaCEnfdz.css +0 -1
  199. package/dist/web/assets/PanelResize-9uljPbE4.js +0 -1
  200. package/dist/web/assets/PanelResize-C51WwwLD.css +0 -1
  201. package/dist/web/assets/PdfPreview-Cb0xawi0.css +0 -1
  202. package/dist/web/assets/QFab-D1X33Wdk.js +0 -1
  203. package/dist/web/assets/QForm-BQMj3m1W.js +0 -4
  204. package/dist/web/assets/QResizeObserver-D-9MeQ9q.js +0 -1
  205. package/dist/web/assets/QScrollArea-DlLel0SO.js +0 -1
  206. package/dist/web/assets/QSelect-mw3iS4Cp.js +0 -1
  207. package/dist/web/assets/QSpace-BbB0w38T.js +0 -1
  208. package/dist/web/assets/QVirtualScroll-FtQn3PJj.js +0 -1
  209. package/dist/web/assets/ReviewDetailPage-D7x0R_2N.js +0 -1
  210. package/dist/web/assets/ReviewsPage-C317IA1b.js +0 -1
  211. package/dist/web/assets/StatusSwitcher-BQXo4sss.js +0 -1
  212. package/dist/web/assets/TerminalView-CTsFWvWm.js +0 -19
  213. package/dist/web/assets/agent-store-ByB1OrjP.js +0 -113
  214. package/dist/web/assets/app-info-store-Gn-SNU3Y.js +0 -1
  215. package/dist/web/assets/architecture-7GRP2DOG-DNWQGMIX.js +0 -1
  216. package/dist/web/assets/channel-DW-wL7ua.js +0 -1
  217. package/dist/web/assets/create-BSmXD9Zr.js +0 -1
  218. package/dist/web/assets/eventmodeling-NTZA5JFV-C1sONJTj.js +0 -1
  219. package/dist/web/assets/flowDiagram-KWPJA3E3-CU2TsxFx.js +0 -1
  220. package/dist/web/assets/i18n-Cxfq3B8n.js +0 -1
  221. package/dist/web/assets/index-DKirIJG5.js +0 -2
  222. package/dist/web/assets/index-Dma-RXBR.css +0 -1
  223. package/dist/web/assets/init-app-B6mIRN6f.js +0 -1
  224. package/dist/web/assets/pinia-Cp_DCu4m.js +0 -1
  225. package/dist/web/assets/pinia-DCsnCHsH.js +0 -1
  226. package/dist/web/assets/railroad-abnf-HS5TGJTU-D6lX11kj.js +0 -1
  227. package/dist/web/assets/railroad-ebnf-LZEXJU2U-D3N0Z6uV.js +0 -1
  228. package/dist/web/assets/railroad-peg-WCYAUIDC-DOaenw8H.js +0 -1
  229. package/dist/web/assets/runtime-core.esm-bundler-BKFI1w28.js +0 -1
  230. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-B5SL6iu0.js +0 -8
  231. package/dist/web/assets/theme-C1B2V2RV.js +0 -1
  232. package/dist/web/assets/use-add-agent-context-3-KB2D10.js +0 -1
  233. package/dist/web/assets/use-app-theme-D_HR0B3W.js +0 -1
  234. package/dist/web/assets/use-panel-width-DIzwb-yt.js +0 -1
  235. package/dist/web/assets/use-router-link-BGc0noIO.js +0 -1
  236. package/dist/web/assets/use-server-events-C13Sw-rU.js +0 -1
  237. package/dist/web/assets/window-store-CnPjWx6-.js +0 -1
@@ -0,0 +1,74 @@
1
+ function rowToRecord(row) {
2
+ return {
3
+ id: row.id,
4
+ name: row.name,
5
+ secretHash: row.secret_hash,
6
+ state: row.state,
7
+ code: row.code,
8
+ host: row.host,
9
+ address: row.address,
10
+ createdAt: row.created_at,
11
+ expiresAt: row.expires_at,
12
+ lastSeenAt: row.last_seen_at
13
+ };
14
+ }
15
+ /** Reads the server id, or stores the one from `create` on first use. */
16
+ export function ensureServerId(db, create) {
17
+ const row = db.prepare('SELECT id FROM server_identity LIMIT 1').get();
18
+ if (row)
19
+ return row.id;
20
+ const id = create();
21
+ db.prepare('INSERT INTO server_identity (id) VALUES (?)').run(id);
22
+ return id;
23
+ }
24
+ export function insertDevice(db, device) {
25
+ db.prepare(`INSERT INTO devices (id, name, secret_hash, state, code, host, address, created_at, expires_at, last_seen_at)
26
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(device.id, device.name, device.secretHash, device.state, device.code, device.host, device.address, device.createdAt, device.expiresAt, device.lastSeenAt);
27
+ }
28
+ export function findDeviceById(db, id) {
29
+ const row = db.prepare('SELECT * FROM devices WHERE id = ?').get(id);
30
+ return row ? rowToRecord(row) : undefined;
31
+ }
32
+ export function findDeviceBySecretHash(db, secretHash) {
33
+ const row = db
34
+ .prepare('SELECT * FROM devices WHERE secret_hash = ?')
35
+ .get(secretHash);
36
+ return row ? rowToRecord(row) : undefined;
37
+ }
38
+ export function listDevices(db) {
39
+ const rows = db
40
+ .prepare('SELECT * FROM devices ORDER BY created_at')
41
+ .all();
42
+ return rows.map(rowToRecord);
43
+ }
44
+ export function countPendingDevices(db) {
45
+ const row = db
46
+ .prepare("SELECT COUNT(*) AS count FROM devices WHERE state = 'pending'")
47
+ .get();
48
+ return row.count;
49
+ }
50
+ /** Ids of records whose time is over. ISO times in UTC compare as text. */
51
+ export function expiredDeviceIds(db, nowIso, state) {
52
+ const sql = state === undefined
53
+ ? 'SELECT id FROM devices WHERE expires_at <= ?'
54
+ : 'SELECT id FROM devices WHERE expires_at <= ? AND state = ?';
55
+ const params = state === undefined ? [nowIso] : [nowIso, state];
56
+ const rows = db.prepare(sql).all(...params);
57
+ return rows.map(row => row.id);
58
+ }
59
+ export function updateDevice(db, id, fields) {
60
+ const columns = {
61
+ name: fields.name,
62
+ state: fields.state,
63
+ address: fields.address,
64
+ expires_at: fields.expiresAt,
65
+ last_seen_at: fields.lastSeenAt
66
+ };
67
+ const entries = Object.entries(columns).filter((entry) => entry[1] !== undefined);
68
+ if (entries.length === 0)
69
+ return;
70
+ db.prepare(`UPDATE devices SET ${entries.map(([column]) => `${column} = ?`).join(', ')} WHERE id = ?`).run(...entries.map(([, value]) => value), id);
71
+ }
72
+ export function deleteDevice(db, id) {
73
+ return db.prepare('DELETE FROM devices WHERE id = ?').run(id).changes > 0;
74
+ }
@@ -21,7 +21,7 @@ const AGENT_ERROR_RESPONSES = {
21
21
  }
22
22
  };
23
23
  export async function agentRoutes(app, opts) {
24
- const { registry, db, repositoryPath, eventBus } = opts;
24
+ const { registry, db, repositoryPath, eventBus, connections } = opts;
25
25
  const typedApp = app.withTypeProvider();
26
26
  const fileIndex = createFileIndex(repositoryPath, eventBus);
27
27
  app.addHook('onRequest', requireLocalOrigin);
@@ -237,6 +237,11 @@ export async function agentRoutes(app, opts) {
237
237
  if (!reply.sse.isConnected) {
238
238
  return;
239
239
  }
240
+ // No `await` between this check of the device and the subscription.
241
+ const release = connections.track(request, () => reply.sse.close());
242
+ if (!release) {
243
+ return;
244
+ }
240
245
  const unsubscribe = registry.subscribe(message => {
241
246
  if (message.type === 'event') {
242
247
  safeSend(reply.sse, {
@@ -262,8 +267,12 @@ export async function agentRoutes(app, opts) {
262
267
  // have run then, so the listener is removed here.
263
268
  if (!reply.sse.isConnected) {
264
269
  unsubscribe();
270
+ release();
265
271
  return;
266
272
  }
267
- reply.sse.onClose(unsubscribe);
273
+ reply.sse.onClose(() => {
274
+ unsubscribe();
275
+ release();
276
+ });
268
277
  });
269
278
  }
@@ -0,0 +1,130 @@
1
+ import { BadRequestError } from "../errors/http.js";
2
+ import { clearDeviceCookie, deviceSecret, requireLocalRequest, setDeviceCookie } from "../hooks/access/require-access.js";
3
+ import { machineAddresses } from "../utils/lan-addresses.js";
4
+ import { clientAddress } from "../utils/request-origin.js";
5
+ import { AccessSchema, ApproveDeviceBody, CreatePairingRequestBody, DeviceListSchema, DeviceParams, DeviceSchema, LanAddressesSchema, RenameDeviceBody } from "../../shared/devices/schemas.js";
6
+ import { ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
7
+ // OpenAPI group of these routes.
8
+ const TAGS = ['devices'];
9
+ // The secret hash and the pairing code never reach the operator's browser:
10
+ // the code is shown on the device only (ADR 0080).
11
+ function toDevice({ secretHash: _secretHash, code: _code, ...device }) {
12
+ return device;
13
+ }
14
+ function pendingAccess(device, now) {
15
+ return {
16
+ status: 'pending',
17
+ pairing: {
18
+ code: device.code,
19
+ expiresAt: device.expiresAt,
20
+ remainingMs: Math.max(0, Date.parse(device.expiresAt) - now)
21
+ }
22
+ };
23
+ }
24
+ export async function deviceRoutes(app, opts) {
25
+ const { devices, onPairingRequest } = opts;
26
+ const typedApp = app.withTypeProvider();
27
+ typedApp.get('/api/access', {
28
+ schema: {
29
+ tags: TAGS,
30
+ summary: 'Access of this browser',
31
+ response: { 200: AccessSchema, ...ERROR_RESPONSES }
32
+ }
33
+ }, async (request, reply) => {
34
+ const access = request.access;
35
+ if (access?.local)
36
+ return { status: 'local' };
37
+ const device = access?.device ?? { status: 'none' };
38
+ if (device.status === 'device')
39
+ return { status: 'device' };
40
+ if (device.status === 'pending') {
41
+ return pendingAccess(device.device, devices.now());
42
+ }
43
+ // A stale secret blocks nothing, but a new request should start clean.
44
+ if (deviceSecret(request, devices.serverId) !== undefined) {
45
+ clearDeviceCookie(reply, devices.serverId);
46
+ }
47
+ return { status: 'none' };
48
+ });
49
+ typedApp.post('/api/devices/requests', {
50
+ schema: {
51
+ tags: TAGS,
52
+ summary: 'Request access for this browser',
53
+ body: CreatePairingRequestBody,
54
+ response: { 200: AccessSchema, ...ERROR_RESPONSES }
55
+ }
56
+ }, async (request, reply) => {
57
+ if (request.access?.local) {
58
+ throw new BadRequestError('This machine needs no pairing.');
59
+ }
60
+ const { device, secret } = devices.createRequest({
61
+ secret: deviceSecret(request, devices.serverId),
62
+ ...(request.body.name === undefined ? {} : { name: request.body.name }),
63
+ ...(request.headers['user-agent'] === undefined
64
+ ? {}
65
+ : { userAgent: request.headers['user-agent'] }),
66
+ address: clientAddress(request.socket.remoteAddress, request.headers['x-forwarded-for']),
67
+ host: request.headers.host ?? ''
68
+ });
69
+ if (secret === undefined)
70
+ return { status: 'device' };
71
+ setDeviceCookie(reply, devices.serverId, secret);
72
+ onPairingRequest?.(toDevice(device));
73
+ return pendingAccess(device, devices.now());
74
+ });
75
+ // Device management: only this machine may see, approve or revoke devices.
76
+ app.register(async (local) => {
77
+ local.addHook('onRequest', requireLocalRequest);
78
+ const typedLocal = local.withTypeProvider();
79
+ typedLocal.get('/api/devices', {
80
+ schema: {
81
+ tags: TAGS,
82
+ summary: 'List pending and trusted devices',
83
+ response: { 200: DeviceListSchema, ...ERROR_RESPONSES }
84
+ }
85
+ }, async () => devices.list().map(toDevice));
86
+ typedLocal.post('/api/devices/:id/approve', {
87
+ schema: {
88
+ tags: TAGS,
89
+ summary: 'Approve a pairing request',
90
+ params: DeviceParams,
91
+ body: ApproveDeviceBody,
92
+ response: {
93
+ 200: { ...DeviceSchema, description: 'The trusted device.' },
94
+ ...ERROR_RESPONSES
95
+ }
96
+ }
97
+ }, async (request) => toDevice(devices.approve(request.params.id, request.body.code)));
98
+ typedLocal.patch('/api/devices/:id', {
99
+ schema: {
100
+ tags: TAGS,
101
+ summary: 'Rename a device',
102
+ params: DeviceParams,
103
+ body: RenameDeviceBody,
104
+ response: {
105
+ 200: { ...DeviceSchema, description: 'The renamed device.' },
106
+ ...ERROR_RESPONSES
107
+ }
108
+ }
109
+ }, async (request) => toDevice(devices.rename(request.params.id, request.body.name)));
110
+ typedLocal.delete('/api/devices/:id', {
111
+ schema: {
112
+ tags: TAGS,
113
+ summary: 'Reject a pairing request or revoke a device',
114
+ params: DeviceParams,
115
+ response: { 204: NoContentSchema, ...ERROR_RESPONSES }
116
+ }
117
+ }, async (request, reply) => {
118
+ devices.revoke(request.params.id);
119
+ reply.code(204);
120
+ return null;
121
+ });
122
+ typedLocal.get('/api/devices/lan-addresses', {
123
+ schema: {
124
+ tags: TAGS,
125
+ summary: 'LAN addresses of this machine for a QR code',
126
+ response: { 200: LanAddressesSchema, ...ERROR_RESPONSES }
127
+ }
128
+ }, async () => machineAddresses());
129
+ });
130
+ }
@@ -1,7 +1,7 @@
1
1
  import { safeSend } from "../utils/sse.js";
2
2
  import { EventStreamSchema } from "../../shared/events/schemas.js";
3
3
  export async function eventRoutes(app, opts) {
4
- const { eventBus, instanceId } = opts;
4
+ const { eventBus, instanceId, connections } = opts;
5
5
  const hello = { instanceId };
6
6
  app.get('/api/events', {
7
7
  sse: true,
@@ -22,6 +22,11 @@ export async function eventRoutes(app, opts) {
22
22
  if (!reply.sse.isConnected) {
23
23
  return;
24
24
  }
25
+ // No `await` between this check of the device and the subscription.
26
+ const release = connections.track(request, () => reply.sse.close());
27
+ if (!release) {
28
+ return;
29
+ }
25
30
  const unsubscribe = eventBus.subscribe(event => {
26
31
  safeSend(reply.sse, {
27
32
  event: event.type,
@@ -30,6 +35,7 @@ export async function eventRoutes(app, opts) {
30
35
  });
31
36
  reply.sse.onClose(() => {
32
37
  unsubscribe();
38
+ release();
33
39
  });
34
40
  });
35
41
  }
@@ -7,14 +7,14 @@ import { NoteRecordSchema, NoteIdQuery, NoteDescriptorSchema, NoteResolveBody, N
7
7
  import { ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
8
8
  import { BadRequestError } from "../errors/http.js";
9
9
  import { withRequestSignal } from "../utils/request-abort.js";
10
- import { createNotesOriginHook } from "../hooks/notes/same-origin.js";
11
- export async function notesRoutes(app, { service, loopbackBind = false, externalAssets = new NotesExternalAssetsService() }) {
10
+ import { requireNotesOrigin } from "../hooks/notes/same-origin.js";
11
+ export async function notesRoutes(app, { service, externalAssets = new NotesExternalAssetsService() }) {
12
12
  await app.register(multipart, {
13
13
  attachFieldsToBody: true,
14
14
  limits: { fileSize: MAX_NOTE_ASSET_BYTES, files: 1, fields: 0, parts: 1 }
15
15
  });
16
16
  const typed = app.withTypeProvider();
17
- app.addHook('onRequest', createNotesOriginHook({ loopbackBind }));
17
+ app.addHook('onRequest', requireNotesOrigin);
18
18
  app.post('/api/notes/assets', {
19
19
  preValidation: async (request) => {
20
20
  if (!request.isMultipart()) {
@@ -49,9 +49,17 @@ export async function terminalRoutes(app, options) {
49
49
  throw new ForbiddenError('Terminal connection token is invalid or expired.');
50
50
  }
51
51
  }
52
- }, socket => {
52
+ }, (socket, request) => {
53
+ // No `await` between this check of the device and the attach.
54
+ const release = options.deviceConnections.track(request, () => socket.terminate());
55
+ if (!release) {
56
+ return;
57
+ }
53
58
  const connection = attachTerminalSocket(socket, options.registry, {
54
- onDisconnect: () => connections.delete(connection)
59
+ onDisconnect: () => {
60
+ connections.delete(connection);
61
+ release();
62
+ }
55
63
  });
56
64
  connections.add(connection);
57
65
  });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Open streams and sockets of trusted devices (ADR 0079, item 13). A revoke
3
+ * in this process closes the connections in the same synchronous step.
4
+ * A revoke by another process and an expiry close them at the next
5
+ * `recheck`.
6
+ *
7
+ * Between the access hook and the registration of a stream there are
8
+ * `await` steps, and the device can lose access in them. So `track` checks
9
+ * the device again and adds the connection without an `await` between the
10
+ * two: `node:sqlite` is synchronous, and JS runs one step at a time.
11
+ */
12
+ export class DeviceConnections {
13
+ byDevice = new Map();
14
+ devices;
15
+ constructor(devices) {
16
+ this.devices = devices;
17
+ devices.onRemoved(id => this.closeDevice(id));
18
+ }
19
+ /**
20
+ * Registers `close` for the device of this request. Returns the release
21
+ * function for a normal close. Returns undefined when the device has no
22
+ * access any more: then `close` was already called. A local request has
23
+ * no device, so its release function does nothing.
24
+ */
25
+ track(request, close) {
26
+ const access = request.access;
27
+ if (!access || access.local)
28
+ return () => { };
29
+ const id = access.device.status === 'device' ? access.device.device.id : undefined;
30
+ if (id === undefined || !this.devices.isTrusted(id)) {
31
+ close();
32
+ return undefined;
33
+ }
34
+ let closers = this.byDevice.get(id);
35
+ if (!closers) {
36
+ closers = new Set();
37
+ this.byDevice.set(id, closers);
38
+ }
39
+ closers.add(close);
40
+ return () => {
41
+ closers.delete(close);
42
+ if (closers.size === 0 && this.byDevice.get(id) === closers) {
43
+ this.byDevice.delete(id);
44
+ }
45
+ };
46
+ }
47
+ /**
48
+ * Closes the connections of devices that lost access without a call of
49
+ * the local service: another server process on the same database revoked
50
+ * them, or their time is over. The listener of the local service cannot
51
+ * see such a change.
52
+ */
53
+ recheck() {
54
+ // Deleting the current key during Map iteration is safe.
55
+ for (const id of this.byDevice.keys()) {
56
+ if (!this.devices.isTrusted(id))
57
+ this.closeDevice(id);
58
+ }
59
+ }
60
+ /** Closes every connection of the device. A repeated call does nothing. */
61
+ closeDevice(id) {
62
+ const closers = this.byDevice.get(id);
63
+ if (!closers)
64
+ return;
65
+ this.byDevice.delete(id);
66
+ for (const close of closers) {
67
+ try {
68
+ close();
69
+ }
70
+ catch {
71
+ // One broken connection must not keep the others open.
72
+ }
73
+ }
74
+ }
75
+ /** Number of open connections of the device; for tests. */
76
+ count(id) {
77
+ return this.byDevice.get(id)?.size ?? 0;
78
+ }
79
+ }
@@ -0,0 +1,222 @@
1
+ import { randomBytes, randomInt, randomUUID } from 'node:crypto';
2
+ import { DEVICE_IDLE_TTL_MS, DEVICE_TOUCH_INTERVAL_MS, MAX_PAIRING_REQUESTS_PER_MINUTE, MAX_PENDING_REQUESTS, PAIRING_CODE_LENGTH, PAIRING_REQUEST_INTERVAL_MS, PAIRING_REQUEST_TTL_MS } from "../../shared/devices/constants.js";
3
+ import { cleanDeviceName } from "../../shared/devices/name.js";
4
+ import { toIsoString } from "../../shared/utils/date.js";
5
+ import { DeviceNotFoundError, PairingConflictError, PairingExpiredError, PairingRateLimitError } from "../errors/devices.js";
6
+ import { BadRequestError } from "../errors/http.js";
7
+ import { countPendingDevices, deleteDevice, ensureServerId, expiredDeviceIds, findDeviceById, findDeviceBySecretHash, insertDevice, listDevices, updateDevice } from "../repositories/device.repo.js";
8
+ import { deviceNameFromUserAgent } from "../utils/device-name.js";
9
+ import { addressKey } from "../utils/address-key.js";
10
+ import { sha256Hex } from "../utils/hash.js";
11
+ // Random bytes in a device secret: 256 bits, 43 base64url characters.
12
+ const SECRET_BYTES = 32;
13
+ // A cookie value of another length is not a secret of this server.
14
+ const SECRET_LENGTH = 43;
15
+ // Window of the global request limit.
16
+ const RATE_WINDOW_MS = 60 * 1000;
17
+ /**
18
+ * Pairing requests and trusted devices (ADR 0079). The secret lives only in
19
+ * the browser cookie; the database keeps its SHA-256. A random 256-bit
20
+ * secret cannot be found from response times, so a lookup by the hash
21
+ * index is safe without a constant-time compare.
22
+ */
23
+ export class DeviceService {
24
+ /** Random id in the cookie name: instances for other repositories keep their own cookie. */
25
+ serverId;
26
+ /** Clock of the service; routes use it for the time left of a request. */
27
+ now;
28
+ onChange;
29
+ lastRequestAt = new Map();
30
+ recentRequests = [];
31
+ removeListeners = new Set();
32
+ db;
33
+ constructor(db, options = {}) {
34
+ this.db = db;
35
+ this.now = options.now ?? Date.now;
36
+ this.onChange = options.onChange ?? (() => { });
37
+ this.serverId = ensureServerId(db, () => randomBytes(6).toString('base64url'));
38
+ }
39
+ /** Calls `listener` with the id of each removed device. Returns the unsubscribe function. */
40
+ onRemoved(listener) {
41
+ this.removeListeners.add(listener);
42
+ return () => this.removeListeners.delete(listener);
43
+ }
44
+ /**
45
+ * Creates a pairing request. A browser that is already trusted gets its
46
+ * device back and no new request. A new request from the same address
47
+ * or with the same cookie replaces the old one.
48
+ */
49
+ createRequest(input) {
50
+ const current = this.access(input.secret, input.address);
51
+ if (current.status === 'device') {
52
+ return { device: current.device };
53
+ }
54
+ this.removeExpiredPending();
55
+ this.checkRate(addressKey(input.address));
56
+ if (current.status === 'pending') {
57
+ this.remove(current.device.id);
58
+ }
59
+ const key = addressKey(input.address);
60
+ for (const device of listDevices(this.db)) {
61
+ if (device.state === 'pending' && addressKey(device.address) === key) {
62
+ this.remove(device.id);
63
+ }
64
+ }
65
+ // A full list drops the oldest request: otherwise a few addresses could
66
+ // keep every slot and lock the real device out.
67
+ while (countPendingDevices(this.db) >= MAX_PENDING_REQUESTS) {
68
+ const oldest = listDevices(this.db).find(device => device.state === 'pending');
69
+ if (!oldest)
70
+ break;
71
+ this.remove(oldest.id);
72
+ }
73
+ const now = this.now();
74
+ this.lastRequestAt.set(key, now);
75
+ this.recentRequests.push(now);
76
+ const secret = randomBytes(SECRET_BYTES).toString('base64url');
77
+ const device = {
78
+ id: randomUUID(),
79
+ name: cleanDeviceName(input.name) || deviceNameFromUserAgent(input.userAgent),
80
+ secretHash: sha256Hex(secret),
81
+ state: 'pending',
82
+ code: randomInt(0, 10 ** PAIRING_CODE_LENGTH)
83
+ .toString()
84
+ .padStart(PAIRING_CODE_LENGTH, '0'),
85
+ host: input.host,
86
+ address: input.address,
87
+ createdAt: toIsoString(now),
88
+ expiresAt: toIsoString(now + PAIRING_REQUEST_TTL_MS),
89
+ lastSeenAt: toIsoString(now)
90
+ };
91
+ insertDevice(this.db, device);
92
+ this.onChange();
93
+ return { device, secret };
94
+ }
95
+ /**
96
+ * Access of the browser with this cookie secret. An expired record never
97
+ * gives access. A trusted device records activity at most once per
98
+ * `DEVICE_TOUCH_INTERVAL_MS`.
99
+ */
100
+ access(secret, address) {
101
+ if (secret?.length !== SECRET_LENGTH)
102
+ return { status: 'none' };
103
+ const device = findDeviceBySecretHash(this.db, sha256Hex(secret));
104
+ const now = this.now();
105
+ if (!device || Date.parse(device.expiresAt) <= now) {
106
+ return { status: 'none' };
107
+ }
108
+ if (device.state === 'pending') {
109
+ return { status: 'pending', device };
110
+ }
111
+ if (now - Date.parse(device.lastSeenAt) < DEVICE_TOUCH_INTERVAL_MS) {
112
+ return { status: 'device', device, renewed: false };
113
+ }
114
+ const fields = {
115
+ lastSeenAt: toIsoString(now),
116
+ expiresAt: toIsoString(now + DEVICE_IDLE_TTL_MS),
117
+ ...(address === undefined ? {} : { address })
118
+ };
119
+ updateDevice(this.db, device.id, fields);
120
+ return { status: 'device', device: { ...device, ...fields }, renewed: true };
121
+ }
122
+ /** True when the device exists, is trusted and its time is not over. */
123
+ isTrusted(id) {
124
+ const device = findDeviceById(this.db, id);
125
+ return (device?.state === 'trusted' && Date.parse(device.expiresAt) > this.now());
126
+ }
127
+ /**
128
+ * Makes a pending request trusted. The code proves that the operator saw
129
+ * this request: a replaced request has another id, and a matching id with
130
+ * another code is a conflict.
131
+ */
132
+ approve(id, code) {
133
+ const device = findDeviceById(this.db, id);
134
+ if (!device) {
135
+ throw new DeviceNotFoundError('The pairing request does not exist. It was replaced, rejected or removed.');
136
+ }
137
+ if (device.state === 'trusted') {
138
+ throw new PairingConflictError('The device is already trusted.');
139
+ }
140
+ const now = this.now();
141
+ if (Date.parse(device.expiresAt) <= now) {
142
+ throw new PairingExpiredError('The pairing request has expired.');
143
+ }
144
+ if (device.code !== code) {
145
+ throw new PairingConflictError('The code does not match the pairing request.');
146
+ }
147
+ const fields = {
148
+ state: 'trusted',
149
+ lastSeenAt: toIsoString(now),
150
+ expiresAt: toIsoString(now + DEVICE_IDLE_TTL_MS)
151
+ };
152
+ updateDevice(this.db, id, fields);
153
+ this.onChange();
154
+ return { ...device, ...fields };
155
+ }
156
+ rename(id, name) {
157
+ const device = findDeviceById(this.db, id);
158
+ if (!device) {
159
+ throw new DeviceNotFoundError('The device does not exist.');
160
+ }
161
+ const trimmed = cleanDeviceName(name);
162
+ if (!trimmed) {
163
+ throw new BadRequestError('The device name is empty.');
164
+ }
165
+ updateDevice(this.db, id, { name: trimmed });
166
+ this.onChange();
167
+ return { ...device, name: trimmed };
168
+ }
169
+ /** Rejects a request or revokes a device. */
170
+ revoke(id) {
171
+ if (!this.remove(id)) {
172
+ throw new DeviceNotFoundError('The device does not exist.');
173
+ }
174
+ }
175
+ /** Devices with time left. Expired pending requests are removed first. */
176
+ list() {
177
+ this.removeExpiredPending();
178
+ const now = this.now();
179
+ return listDevices(this.db).filter(device => Date.parse(device.expiresAt) > now);
180
+ }
181
+ /** Removes every expired record through the revoke path. Returns their ids. */
182
+ removeExpired() {
183
+ const ids = expiredDeviceIds(this.db, toIsoString(this.now()));
184
+ for (const id of ids)
185
+ this.remove(id);
186
+ return ids;
187
+ }
188
+ removeExpiredPending() {
189
+ for (const id of expiredDeviceIds(this.db, toIsoString(this.now()), 'pending')) {
190
+ this.remove(id);
191
+ }
192
+ }
193
+ /**
194
+ * Deletes the record and tells listeners synchronously, so that open
195
+ * connections close in the same step (ADR 0079, item 13). A repeated call
196
+ * returns false and changes nothing.
197
+ */
198
+ remove(id) {
199
+ if (!deleteDevice(this.db, id))
200
+ return false;
201
+ for (const listener of this.removeListeners)
202
+ listener(id);
203
+ this.onChange();
204
+ return true;
205
+ }
206
+ checkRate(key) {
207
+ const now = this.now();
208
+ const last = this.lastRequestAt.get(key);
209
+ if (last !== undefined && now - last < PAIRING_REQUEST_INTERVAL_MS) {
210
+ throw new PairingRateLimitError('Pairing requests from this address are too frequent. Wait a few seconds.');
211
+ }
212
+ this.recentRequests = this.recentRequests.filter(time => now - time < RATE_WINDOW_MS);
213
+ if (this.recentRequests.length >= MAX_PAIRING_REQUESTS_PER_MINUTE) {
214
+ throw new PairingRateLimitError('Too many pairing requests. Try again in a minute.');
215
+ }
216
+ for (const [storedKey, time] of this.lastRequestAt) {
217
+ if (now - time >= PAIRING_REQUEST_INTERVAL_MS) {
218
+ this.lastRequestAt.delete(storedKey);
219
+ }
220
+ }
221
+ }
222
+ }
@@ -1,7 +1,7 @@
1
1
  import PQueue from 'p-queue';
2
2
  import { BadRequestError } from "../errors/http.js";
3
3
  import { NoteAssetLimitError, NoteAssetQueueError } from "../errors/note-assets.js";
4
- import { remoteAssetRequest, translateRemoteAssetError } from "../utils/remote-asset-request.js";
4
+ import { destroyRemoteAssetResponse, remoteAssetRequest, translateRemoteAssetError } from "../utils/remote-asset-request.js";
5
5
  import { detectNoteAssetFormat } from "../utils/note-asset-format.js";
6
6
  import { MAX_NOTE_ASSET_BYTES, NOTE_PDF_WARNING_BYTES } from "../../shared/notes/constants.js";
7
7
  import { maxNoteAssetBytes } from "../../shared/notes/asset-formats.js";
@@ -26,7 +26,7 @@ async function* boundedChunks(source, limit, counter, signal, message) {
26
26
  }
27
27
  }
28
28
  function isSuccess(response) {
29
- return (!!response.statusCode &&
29
+ return (response.statusCode !== undefined &&
30
30
  response.statusCode >= 200 &&
31
31
  response.statusCode < 300);
32
32
  }
@@ -61,7 +61,7 @@ export class NotesExternalAssetsService {
61
61
  };
62
62
  }
63
63
  finally {
64
- response.destroy();
64
+ destroyRemoteAssetResponse(response);
65
65
  }
66
66
  });
67
67
  }
@@ -137,7 +137,7 @@ export class NotesExternalAssetsService {
137
137
  }
138
138
  }
139
139
  finally {
140
- response.destroy();
140
+ destroyRemoteAssetResponse(response);
141
141
  }
142
142
  });
143
143
  }
@@ -161,7 +161,7 @@ export class NotesExternalAssetsService {
161
161
  return { body, mime: format.mime };
162
162
  }
163
163
  finally {
164
- response.destroy();
164
+ destroyRemoteAssetResponse(response);
165
165
  }
166
166
  });
167
167
  }
@@ -444,7 +444,9 @@ export class NotesIdentityService {
444
444
  !source.deletedAt &&
445
445
  this.catalog
446
446
  .links(source.id)
447
- .some(link => !!link.targetId && this.record(link.targetId).id === note.id));
447
+ .some(link => link.targetId !== null &&
448
+ link.targetId !== '' &&
449
+ this.record(link.targetId).id === note.id));
448
450
  return { note, backlinks };
449
451
  });
450
452
  }
@@ -742,7 +744,7 @@ export class NotesIdentityService {
742
744
  (() => {
743
745
  const target = referenceTarget(ref, source.path, paths);
744
746
  const clicked = referenceTarget({ ...ref, format: 'markdown', href }, source.path, paths);
745
- return (!!target &&
747
+ return (target !== null &&
746
748
  target.path === clicked?.path &&
747
749
  target.anchor === clicked.anchor);
748
750
  })()));