termdock 1.0.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +385 -0
  3. package/dist/client/apple-splash-default-1170x2532.png +0 -0
  4. package/dist/client/apple-splash-landscape-1136x640.png +0 -0
  5. package/dist/client/apple-splash-landscape-1334x750.png +0 -0
  6. package/dist/client/apple-splash-landscape-1792x828.png +0 -0
  7. package/dist/client/apple-splash-landscape-2048x1536.png +0 -0
  8. package/dist/client/apple-splash-landscape-2160x1620.png +0 -0
  9. package/dist/client/apple-splash-landscape-2208x1242.png +0 -0
  10. package/dist/client/apple-splash-landscape-2224x1668.png +0 -0
  11. package/dist/client/apple-splash-landscape-2266x1488.png +0 -0
  12. package/dist/client/apple-splash-landscape-2360x1640.png +0 -0
  13. package/dist/client/apple-splash-landscape-2388x1668.png +0 -0
  14. package/dist/client/apple-splash-landscape-2436x1125.png +0 -0
  15. package/dist/client/apple-splash-landscape-2532x1170.png +0 -0
  16. package/dist/client/apple-splash-landscape-2556x1179.png +0 -0
  17. package/dist/client/apple-splash-landscape-2622x1206.png +0 -0
  18. package/dist/client/apple-splash-landscape-2688x1242.png +0 -0
  19. package/dist/client/apple-splash-landscape-2732x2048.png +0 -0
  20. package/dist/client/apple-splash-landscape-2778x1284.png +0 -0
  21. package/dist/client/apple-splash-landscape-2796x1290.png +0 -0
  22. package/dist/client/apple-splash-landscape-2868x1320.png +0 -0
  23. package/dist/client/apple-splash-portrait-1125x2436.png +0 -0
  24. package/dist/client/apple-splash-portrait-1170x2532.png +0 -0
  25. package/dist/client/apple-splash-portrait-1179x2556.png +0 -0
  26. package/dist/client/apple-splash-portrait-1206x2622.png +0 -0
  27. package/dist/client/apple-splash-portrait-1242x2208.png +0 -0
  28. package/dist/client/apple-splash-portrait-1242x2688.png +0 -0
  29. package/dist/client/apple-splash-portrait-1284x2778.png +0 -0
  30. package/dist/client/apple-splash-portrait-1290x2796.png +0 -0
  31. package/dist/client/apple-splash-portrait-1320x2868.png +0 -0
  32. package/dist/client/apple-splash-portrait-1488x2266.png +0 -0
  33. package/dist/client/apple-splash-portrait-1536x2048.png +0 -0
  34. package/dist/client/apple-splash-portrait-1620x2160.png +0 -0
  35. package/dist/client/apple-splash-portrait-1640x2360.png +0 -0
  36. package/dist/client/apple-splash-portrait-1668x2224.png +0 -0
  37. package/dist/client/apple-splash-portrait-1668x2388.png +0 -0
  38. package/dist/client/apple-splash-portrait-2048x2732.png +0 -0
  39. package/dist/client/apple-splash-portrait-640x1136.png +0 -0
  40. package/dist/client/apple-splash-portrait-750x1334.png +0 -0
  41. package/dist/client/apple-splash-portrait-828x1792.png +0 -0
  42. package/dist/client/apple-touch-icon-180x180.png +0 -0
  43. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  44. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  45. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  46. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  47. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  48. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  49. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  50. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  51. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  52. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  53. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  54. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  55. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  56. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  57. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  58. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  59. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  60. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  61. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  62. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  63. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  64. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  65. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  66. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  67. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  68. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  69. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  70. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  71. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  72. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  73. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  74. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  75. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  76. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  77. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  78. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  79. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  80. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  81. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  82. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  83. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  84. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  85. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  86. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  87. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  88. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  89. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  90. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  91. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  92. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  93. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  94. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  95. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  96. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  97. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  98. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  99. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  100. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  101. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  102. package/dist/client/assets/abnfDiagram-VRR7QNED-D0NxeSHA.js +1 -0
  103. package/dist/client/assets/arc-B7GoKc6u.js +1 -0
  104. package/dist/client/assets/architectureDiagram-ZJ3FMSHR-BQPW-wUx.js +36 -0
  105. package/dist/client/assets/blockDiagram-677ZJIJ3-CX_hbcgG.js +132 -0
  106. package/dist/client/assets/c4Diagram-LMCZKHZV-BwOMQA4x.js +10 -0
  107. package/dist/client/assets/channel-C2gUFcyU.js +1 -0
  108. package/dist/client/assets/chunk-2Q5K7J3B-BLJzEQXz.js +1 -0
  109. package/dist/client/assets/chunk-32BRIVSS-CjBDr_gz.js +1 -0
  110. package/dist/client/assets/chunk-5VM5RSS4-lddvv0x2.js +15 -0
  111. package/dist/client/assets/chunk-EX3LRPZG-Dtsq1Stg.js +231 -0
  112. package/dist/client/assets/chunk-JWPE2WC7-BG3PZx4j.js +1 -0
  113. package/dist/client/assets/chunk-MOJQB5TN-CMtlCuQg.js +88 -0
  114. package/dist/client/assets/chunk-RYQCIY6F-uplKqw64.js +1 -0
  115. package/dist/client/assets/chunk-V7JOEXUC-CpteLsY3.js +206 -0
  116. package/dist/client/assets/chunk-VR4S4FIN-1zYOJQXL.js +1 -0
  117. package/dist/client/assets/chunk-XXDRQBXY-C0wXH91I.js +1 -0
  118. package/dist/client/assets/classDiagram-OUVF2IWQ-BTOnA9HS.js +1 -0
  119. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BTOnA9HS.js +1 -0
  120. package/dist/client/assets/cose-bilkent-JH36ORCC-BceT5ki2.js +1 -0
  121. package/dist/client/assets/cynefin-VYW2F7L2-BqCIkNeC.js +166 -0
  122. package/dist/client/assets/cynefinDiagram-TSTJHNR4-Dm7y-tsN.js +62 -0
  123. package/dist/client/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
  124. package/dist/client/assets/dagre-VKFMJZFB-Bn8eh0lE.js +4 -0
  125. package/dist/client/assets/defaultLocale-CrowFXzY.js +1 -0
  126. package/dist/client/assets/diagram-FQU43EPY-DmZGMAsX.js +3 -0
  127. package/dist/client/assets/diagram-G47NLZAW-BLWSm_Rn.js +24 -0
  128. package/dist/client/assets/diagram-NH7WQ7WH-C2-HK8pv.js +24 -0
  129. package/dist/client/assets/diagram-OA4YK3LP-8Ex4XIU5.js +30 -0
  130. package/dist/client/assets/diagram-WEI45ONY-MRNIpBXG.js +41 -0
  131. package/dist/client/assets/diffWorker-B6msVyjo.js +18 -0
  132. package/dist/client/assets/dnd-BCFAPC0L.js +1 -0
  133. package/dist/client/assets/ebnfDiagram-CCIWWBDH-D7Z6wZi_.js +1 -0
  134. package/dist/client/assets/erDiagram-Q63AITRT-C96Q5x9y.js +85 -0
  135. package/dist/client/assets/flowDiagram-23GEKE2U-DzQp40QH.js +156 -0
  136. package/dist/client/assets/ganttDiagram-NO4QXBWP-ZzkchZJ8.js +292 -0
  137. package/dist/client/assets/gitGraphDiagram-IHSO6WYX-CFacEf54.js +106 -0
  138. package/dist/client/assets/graph-D2o_JWn5.js +1 -0
  139. package/dist/client/assets/index-Ca-rq7yX.js +6 -0
  140. package/dist/client/assets/index-Cv7q4nZR.css +1 -0
  141. package/dist/client/assets/index-XktPMm51.js +394 -0
  142. package/dist/client/assets/infoDiagram-FWYZ7A6U-CVRXs5rF.js +2 -0
  143. package/dist/client/assets/init-Gi6I4Gst.js +1 -0
  144. package/dist/client/assets/ishikawaDiagram-FXEZZL3T-B8FP_wzM.js +70 -0
  145. package/dist/client/assets/journeyDiagram-5HDEW3XC-BR__4Hbd.js +139 -0
  146. package/dist/client/assets/kanban-definition-HUTT4EX6-v39oZe6X.js +89 -0
  147. package/dist/client/assets/katex-55ZPZeOF.css +1 -0
  148. package/dist/client/assets/katex-DnlPpQZa.js +257 -0
  149. package/dist/client/assets/katex-HP8lGamR.js +257 -0
  150. package/dist/client/assets/layout-1eBIAoSl.js +1 -0
  151. package/dist/client/assets/linear-jlTiHqO6.js +1 -0
  152. package/dist/client/assets/map-BdqsVdb3.js +1 -0
  153. package/dist/client/assets/mermaid.core-DiiIN0EC.js +306 -0
  154. package/dist/client/assets/mindmap-definition-LN4V7U3C-AeUxAZeq.js +96 -0
  155. package/dist/client/assets/ordinal-Cboi1Yqb.js +1 -0
  156. package/dist/client/assets/pegDiagram-2B236MQR-Dh-bgAbv.js +1 -0
  157. package/dist/client/assets/pieDiagram-ENE6RG2P-De7Si7Tm.js +39 -0
  158. package/dist/client/assets/purify.es-CC4Brkr_.js +3 -0
  159. package/dist/client/assets/quadrantDiagram-ABIIQ3AL-CgWfHqy0.js +7 -0
  160. package/dist/client/assets/railroadDiagram-RFXS5EU6-BXnQW-pI.js +1 -0
  161. package/dist/client/assets/react-XDCjal2T.js +8 -0
  162. package/dist/client/assets/requirementDiagram-TGXJPOKE-CU1nnpUM.js +84 -0
  163. package/dist/client/assets/sankeyDiagram-HTMAVEWB-DFK-AXY6.js +40 -0
  164. package/dist/client/assets/sequenceDiagram-DBY2YBRQ-D_bVDWgq.js +162 -0
  165. package/dist/client/assets/sizeCapture-X5ZJPWSS-nh89ExHT.js +1 -0
  166. package/dist/client/assets/stateDiagram-2N3HPSRC-DUvT6Ruk.js +1 -0
  167. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dz-3r-Yq.js +1 -0
  168. package/dist/client/assets/swimlanes-5IMT3BWC-ZYa52gjG.js +2 -0
  169. package/dist/client/assets/swimlanesDiagram-G3AALYLV-DXR_9lf9.js +8 -0
  170. package/dist/client/assets/terminal-C7X98Amf.js +10 -0
  171. package/dist/client/assets/timeline-definition-FHXFAJF6-Bp6A49KQ.js +120 -0
  172. package/dist/client/assets/vennDiagram-L72KCM5P-CDDUmLOI.js +34 -0
  173. package/dist/client/assets/wardleyDiagram-EHGQE667-CRc2ptu7.js +78 -0
  174. package/dist/client/assets/xychartDiagram-FW5EYKEG-ht_S0LUA.js +7 -0
  175. package/dist/client/favicon.ico +0 -0
  176. package/dist/client/fonts/JetBrainsMonoNL-Bold.woff2 +0 -0
  177. package/dist/client/fonts/JetBrainsMonoNL-BoldItalic.woff2 +0 -0
  178. package/dist/client/fonts/JetBrainsMonoNL-Italic.woff2 +0 -0
  179. package/dist/client/fonts/JetBrainsMonoNL-Regular.woff2 +0 -0
  180. package/dist/client/fonts/JetBrainsMonoNLNerdFontMono-Bold.ttf +0 -0
  181. package/dist/client/fonts/JetBrainsMonoNLNerdFontMono-BoldItalic.ttf +0 -0
  182. package/dist/client/fonts/JetBrainsMonoNLNerdFontMono-Italic.ttf +0 -0
  183. package/dist/client/fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf +0 -0
  184. package/dist/client/fonts/NotoSansMonoCJKsc-Bold-Subset.woff2 +0 -0
  185. package/dist/client/fonts/NotoSansMonoCJKsc-Subset.woff2 +0 -0
  186. package/dist/client/fonts/NotoSansSymbols2-Subset.woff2 +0 -0
  187. package/dist/client/fonts/SymbolsNerdFontMono-Regular.woff2 +0 -0
  188. package/dist/client/fonts/TermdockStarSymbol-Subset.woff2 +0 -0
  189. package/dist/client/index.html +255 -0
  190. package/dist/client/manifest.webmanifest +1 -0
  191. package/dist/client/maskable-icon-512x512.png +0 -0
  192. package/dist/client/pwa-192x192.png +0 -0
  193. package/dist/client/pwa-192x192.svg +5 -0
  194. package/dist/client/pwa-512x512.png +0 -0
  195. package/dist/client/pwa-512x512.svg +5 -0
  196. package/dist/client/pwa-64x64.png +0 -0
  197. package/dist/client/registerSW.js +1 -0
  198. package/dist/client/robots.txt +4 -0
  199. package/dist/client/shell-integration/termdock.bash +107 -0
  200. package/dist/client/shell-integration/termdock.fish +60 -0
  201. package/dist/client/shell-integration/termdock.zsh +164 -0
  202. package/dist/client/sw-notifications.js +42 -0
  203. package/dist/client/sw.js +1 -0
  204. package/dist/client/workbox-e4022e15.js +1 -0
  205. package/dist/server/certificateWatcher.js +111 -0
  206. package/dist/server/cli.js +2534 -0
  207. package/dist/server/config.js +54 -0
  208. package/dist/server/entry.js +571 -0
  209. package/dist/server/onboardingServer.js +54 -0
  210. package/dist/server/routes/auth.js +48 -0
  211. package/dist/server/routes/filesystem.js +3749 -0
  212. package/dist/server/routes/local.js +32 -0
  213. package/dist/server/routes/onboarding.js +134 -0
  214. package/dist/server/routes/terminal.js +4879 -0
  215. package/dist/server/utils/authProtection.js +338 -0
  216. package/dist/server/utils/caffeinate.js +124 -0
  217. package/dist/server/utils/changeAuditStore.js +521 -0
  218. package/dist/server/utils/cookieSecurity.js +13 -0
  219. package/dist/server/utils/csrfProtection.js +112 -0
  220. package/dist/server/utils/localAccess.js +211 -0
  221. package/dist/server/utils/pathValidator.js +272 -0
  222. package/dist/server/utils/requestSecurity.js +103 -0
  223. package/dist/server/utils/serverLogger.js +57 -0
  224. package/dist/server/utils/settings.js +157 -0
  225. package/dist/server/utils/tmuxFocus.js +45 -0
  226. package/dist/server/utils/tmuxProgramDetection.js +148 -0
  227. package/package.json +125 -0
@@ -0,0 +1,521 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { createHash } from 'crypto';
5
+ const STATE_DIR = path.join(os.homedir(), '.termdock');
6
+ const STORE_PATH = path.join(STATE_DIR, 'change-audit.json');
7
+ const MAX_RECORDS = 5_000;
8
+ const CACHE_REFRESH_INTERVAL_MS = 2_000;
9
+ let cachedState = { version: 1, records: [] };
10
+ let cachedMtimeMs = 0;
11
+ let reloadInFlight = false;
12
+ let lastRefreshStartedAt = 0;
13
+ // Change audit explanations are auxiliary sidebar data. Keep the browser GET
14
+ // path non-blocking: return the in-memory snapshot immediately and refresh the
15
+ // on-disk JSON in the background. Do not move readFile/JSON.parse back into
16
+ // listChangeAuditRecords(); it shares the same Node process as Git bundle,
17
+ // file tree and diff requests, so synchronous work here can stall the entire
18
+ // Changes tab.
19
+ function parseState(raw) {
20
+ try {
21
+ const parsed = JSON.parse(raw);
22
+ return {
23
+ version: 1,
24
+ records: Array.isArray(parsed.records) ? parsed.records.filter(isStoredRecord) : [],
25
+ branchRecords: Array.isArray(parsed.branchRecords) ? parsed.branchRecords.filter(isStoredBranchRecord) : [],
26
+ walkthroughs: Array.isArray(parsed.walkthroughs) ? parsed.walkthroughs.filter(isStoredWalkthrough) : [],
27
+ };
28
+ }
29
+ catch {
30
+ return { version: 1, records: [] };
31
+ }
32
+ }
33
+ function readStateForMutation() {
34
+ try {
35
+ const stat = fs.statSync(STORE_PATH);
36
+ if (stat.mtimeMs === cachedMtimeMs)
37
+ return cachedState;
38
+ cachedState = parseState(fs.readFileSync(STORE_PATH, 'utf8'));
39
+ cachedMtimeMs = stat.mtimeMs;
40
+ }
41
+ catch {
42
+ cachedState = { version: 1, records: [] };
43
+ cachedMtimeMs = 0;
44
+ }
45
+ return cachedState;
46
+ }
47
+ export function refreshChangeAuditCache() {
48
+ const now = Date.now();
49
+ if (now - lastRefreshStartedAt < CACHE_REFRESH_INTERVAL_MS)
50
+ return;
51
+ if (reloadInFlight)
52
+ return;
53
+ lastRefreshStartedAt = now;
54
+ reloadInFlight = true;
55
+ void (async () => {
56
+ try {
57
+ const stat = await fs.promises.stat(STORE_PATH);
58
+ if (stat.mtimeMs !== cachedMtimeMs) {
59
+ const raw = await fs.promises.readFile(STORE_PATH, 'utf8');
60
+ cachedState = parseState(raw);
61
+ cachedMtimeMs = stat.mtimeMs;
62
+ }
63
+ }
64
+ catch {
65
+ cachedState = { version: 1, records: [] };
66
+ cachedMtimeMs = 0;
67
+ }
68
+ finally {
69
+ reloadInFlight = false;
70
+ }
71
+ })();
72
+ }
73
+ function writeState(state) {
74
+ fs.mkdirSync(STATE_DIR, { recursive: true });
75
+ fs.writeFileSync(STORE_PATH, JSON.stringify(state, null, 2), { mode: 0o600 });
76
+ cachedState = state;
77
+ try {
78
+ cachedMtimeMs = fs.statSync(STORE_PATH).mtimeMs;
79
+ }
80
+ catch {
81
+ cachedMtimeMs = 0;
82
+ }
83
+ }
84
+ function isStoredRecord(value) {
85
+ if (!value || typeof value !== 'object')
86
+ return false;
87
+ const record = value;
88
+ return typeof record.id === 'string'
89
+ && typeof record.repoRoot === 'string'
90
+ && typeof record.filePath === 'string'
91
+ && typeof record.hunkHeader === 'string'
92
+ && typeof record.fingerprint === 'string'
93
+ && typeof record.explanation === 'string'
94
+ && typeof record.injectedAt === 'number';
95
+ }
96
+ function isStoredBranchRecord(value) {
97
+ if (!value || typeof value !== 'object')
98
+ return false;
99
+ const record = value;
100
+ return typeof record.id === 'string'
101
+ && typeof record.repoRoot === 'string'
102
+ && typeof record.baseRef === 'string'
103
+ && typeof record.filePath === 'string'
104
+ && typeof record.hunkHeader === 'string'
105
+ && typeof record.fingerprint === 'string'
106
+ && (record.diff === undefined || record.diff === null || typeof record.diff === 'string')
107
+ && typeof record.explanation === 'string'
108
+ && typeof record.injectedAt === 'number';
109
+ }
110
+ function isStoredWalkthrough(value) {
111
+ if (!value || typeof value !== 'object')
112
+ return false;
113
+ const walkthrough = value;
114
+ return walkthrough.version === 1
115
+ && typeof walkthrough.id === 'string'
116
+ && typeof walkthrough.repoRoot === 'string'
117
+ && typeof walkthrough.title === 'string'
118
+ && typeof walkthrough.injectedAt === 'number'
119
+ && Array.isArray(walkthrough.highlights)
120
+ && Array.isArray(walkthrough.nodes)
121
+ && Array.isArray(walkthrough.edges)
122
+ && Array.isArray(walkthrough.sections)
123
+ && Array.isArray(walkthrough.risks)
124
+ && Array.isArray(walkthrough.checks);
125
+ }
126
+ function normalizeString(value) {
127
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
128
+ }
129
+ function normalizeWalkthroughAnchor(anchor, fallback) {
130
+ if (!anchor || typeof anchor !== 'object' || Array.isArray(anchor))
131
+ return null;
132
+ const maybe = anchor;
133
+ const filePath = normalizeString(maybe.filePath);
134
+ if (!filePath)
135
+ return null;
136
+ const hunkIndex = typeof maybe.hunkIndex === 'number' && Number.isFinite(maybe.hunkIndex)
137
+ ? Math.max(0, Math.floor(maybe.hunkIndex))
138
+ : null;
139
+ const sectionIndex = typeof maybe.sectionIndex === 'number' && Number.isFinite(maybe.sectionIndex)
140
+ ? Math.max(0, Math.floor(maybe.sectionIndex))
141
+ : null;
142
+ return {
143
+ repoRoot: normalizeString(maybe.repoRoot) ?? normalizeString(fallback.repoRoot),
144
+ filePath,
145
+ hunkHeader: normalizeString(maybe.hunkHeader),
146
+ hunkIndex,
147
+ hunkFingerprint: normalizeString(maybe.hunkFingerprint),
148
+ sectionIndex,
149
+ sectionFingerprint: normalizeString(maybe.sectionFingerprint),
150
+ };
151
+ }
152
+ function normalizeStringList(value, max) {
153
+ if (!Array.isArray(value))
154
+ return [];
155
+ return value.map(normalizeString).filter((item) => Boolean(item)).slice(0, max);
156
+ }
157
+ function compact(items) {
158
+ return items.filter((item) => item !== null);
159
+ }
160
+ function normalizeWalkthrough(input, fallback, injectedAt) {
161
+ if (!input || typeof input !== 'object' || Array.isArray(input))
162
+ return null;
163
+ const repoRoot = normalizeString(input.repoRoot) ?? normalizeString(fallback.repoRoot);
164
+ const title = normalizeString(input.title);
165
+ if (!repoRoot || !title)
166
+ return null;
167
+ const highlights = Array.isArray(input.highlights) ? compact(input.highlights.map((entry) => {
168
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
169
+ return null;
170
+ const maybe = entry;
171
+ const what = normalizeString(maybe.what);
172
+ const effect = normalizeString(maybe.effect);
173
+ if (!what || !effect)
174
+ return null;
175
+ return { what, effect, tag: normalizeString(maybe.tag) };
176
+ })).slice(0, 8) : [];
177
+ const nodes = Array.isArray(input.nodes) ? compact(input.nodes.map((entry) => {
178
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
179
+ return null;
180
+ const maybe = entry;
181
+ const id = normalizeString(maybe.id);
182
+ const nodeTitle = normalizeString(maybe.title);
183
+ const business = normalizeString(maybe.business);
184
+ if (!id || !nodeTitle || !business)
185
+ return null;
186
+ return {
187
+ id,
188
+ title: nodeTitle,
189
+ kind: normalizeString(maybe.kind),
190
+ summary: normalizeString(maybe.summary),
191
+ business,
192
+ anchor: normalizeWalkthroughAnchor(maybe.anchor, { repoRoot }),
193
+ };
194
+ })).slice(0, 24) : [];
195
+ const nodeIds = new Set(nodes.map((node) => node.id));
196
+ const edges = Array.isArray(input.edges) ? compact(input.edges.map((entry) => {
197
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
198
+ return null;
199
+ const maybe = entry;
200
+ const from = normalizeString(maybe.from);
201
+ const to = normalizeString(maybe.to);
202
+ if (!from || !to || !nodeIds.has(from) || !nodeIds.has(to))
203
+ return null;
204
+ return { from, to, label: normalizeString(maybe.label), desc: normalizeString(maybe.desc) };
205
+ })).slice(0, 32) : [];
206
+ const sections = Array.isArray(input.sections) ? compact(input.sections.map((entry) => {
207
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
208
+ return null;
209
+ const maybe = entry;
210
+ const id = normalizeString(maybe.id);
211
+ const summary = normalizeString(maybe.summary);
212
+ const anchor = normalizeWalkthroughAnchor(maybe.anchor, { repoRoot });
213
+ if (!id || !summary || !anchor)
214
+ return null;
215
+ const nodeId = normalizeString(maybe.nodeId);
216
+ return {
217
+ id,
218
+ nodeId: nodeId && nodeIds.has(nodeId) ? nodeId : null,
219
+ anchor,
220
+ summary,
221
+ explanation: normalizeString(maybe.explanation),
222
+ };
223
+ })).slice(0, 200) : [];
224
+ const risks = Array.isArray(input.risks) ? compact(input.risks.map((entry) => {
225
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
226
+ return null;
227
+ const maybe = entry;
228
+ const riskTitle = normalizeString(maybe.title);
229
+ if (!riskTitle)
230
+ return null;
231
+ return { title: riskTitle, anchor: normalizeWalkthroughAnchor(maybe.anchor, { repoRoot }) };
232
+ })).slice(0, 16) : [];
233
+ const id = buildChangeAuditFingerprint([
234
+ repoRoot,
235
+ normalizeString(input.scope) ?? '',
236
+ title,
237
+ nodes.map((node) => node.id).join(','),
238
+ sections.map((section) => `${section.anchor.filePath}:${section.anchor.hunkIndex ?? ''}:${section.anchor.sectionIndex ?? ''}:${section.anchor.sectionFingerprint ?? ''}`).join(','),
239
+ ]);
240
+ return {
241
+ version: 1,
242
+ id,
243
+ workspaceRoot: normalizeString(input.workspaceRoot) ?? normalizeString(fallback.workspaceRoot),
244
+ repoRoot,
245
+ baseRef: normalizeString(input.baseRef) ?? normalizeString(fallback.baseRef),
246
+ branchName: normalizeString(input.branchName) ?? normalizeString(fallback.branchName),
247
+ headRef: normalizeString(input.headRef) ?? normalizeString(fallback.headRef),
248
+ diffFingerprint: normalizeString(input.diffFingerprint) ?? normalizeString(fallback.diffFingerprint),
249
+ title,
250
+ scope: normalizeString(input.scope),
251
+ summary: normalizeString(input.summary),
252
+ generatedBy: normalizeString(input.generatedBy) ?? normalizeString(fallback.generatedBy),
253
+ injectedAt,
254
+ highlights,
255
+ nodes,
256
+ edges,
257
+ sections,
258
+ risks,
259
+ checks: normalizeStringList(input.checks, 24),
260
+ };
261
+ }
262
+ function normalizeRecord(record, fallback, injectedAt) {
263
+ const repoRoot = normalizeString(record.repoRoot) ?? normalizeString(fallback.repoRoot);
264
+ const filePath = normalizeString(record.filePath);
265
+ const hunkHeader = normalizeString(record.hunkHeader);
266
+ const fingerprint = normalizeString(record.fingerprint);
267
+ const explanation = normalizeString(record.explanation);
268
+ if (!repoRoot || !filePath || !hunkHeader || !fingerprint || !explanation)
269
+ return null;
270
+ const oldPath = normalizeString(record.oldPath);
271
+ const newPath = normalizeString(record.newPath);
272
+ const summary = normalizeString(record.summary);
273
+ const hunkIndex = typeof record.hunkIndex === 'number' && Number.isFinite(record.hunkIndex)
274
+ ? Math.max(0, Math.floor(record.hunkIndex))
275
+ : null;
276
+ const sectionIndex = typeof record.sectionIndex === 'number' && Number.isFinite(record.sectionIndex)
277
+ ? Math.max(0, Math.floor(record.sectionIndex))
278
+ : null;
279
+ const sectionFingerprint = normalizeString(record.sectionFingerprint);
280
+ const id = buildChangeAuditRecordId({ repoRoot, filePath, hunkHeader, fingerprint, sectionFingerprint, sectionIndex });
281
+ return {
282
+ id,
283
+ repoRoot,
284
+ filePath,
285
+ oldPath,
286
+ newPath,
287
+ hunkHeader,
288
+ hunkIndex,
289
+ sectionIndex,
290
+ sectionFingerprint,
291
+ fingerprint,
292
+ explanation,
293
+ summary,
294
+ workspaceRoot: normalizeString(fallback.workspaceRoot),
295
+ generatedBy: normalizeString(fallback.generatedBy),
296
+ injectedAt,
297
+ };
298
+ }
299
+ export function buildChangeAuditFingerprint(parts) {
300
+ return createHash('sha256').update(parts.join('\n'), 'utf8').digest('hex').slice(0, 16);
301
+ }
302
+ export function buildChangeAuditRecordId(input) {
303
+ return createHash('sha256')
304
+ .update(`${input.repoRoot}\0${input.filePath}\0${input.hunkHeader}\0${input.fingerprint}\0${input.sectionIndex ?? ''}\0${input.sectionFingerprint ?? ''}`, 'utf8')
305
+ .digest('hex')
306
+ .slice(0, 24);
307
+ }
308
+ export function buildBranchAuditRecordId(input) {
309
+ return createHash('sha256')
310
+ .update(`${input.repoRoot}\0${input.baseRef}\0${input.branchName ?? ''}\0${input.filePath}\0${input.hunkHeader}\0${input.fingerprint}\0${input.sectionIndex ?? ''}\0${input.sectionFingerprint ?? ''}`, 'utf8')
311
+ .digest('hex')
312
+ .slice(0, 24);
313
+ }
314
+ export function injectChangeAudit(payload) {
315
+ const injectedAt = Date.now();
316
+ const normalized = (Array.isArray(payload.records) ? payload.records : [])
317
+ .map((record) => normalizeRecord(record, payload, injectedAt))
318
+ .filter((record) => record !== null);
319
+ const state = readStateForMutation();
320
+ const nextById = new Map(state.records.map((record) => [record.id, record]));
321
+ for (const record of normalized)
322
+ nextById.set(record.id, record);
323
+ const records = Array.from(nextById.values())
324
+ .sort((a, b) => b.injectedAt - a.injectedAt)
325
+ .slice(0, MAX_RECORDS);
326
+ const walkthrough = normalizeWalkthrough(payload.walkthrough, payload, injectedAt);
327
+ const walkthroughs = walkthrough
328
+ ? [walkthrough, ...(state.walkthroughs ?? []).filter((entry) => entry.repoRoot !== walkthrough.repoRoot || entry.id !== walkthrough.id)].slice(0, 100)
329
+ : state.walkthroughs;
330
+ if (normalized.length > 0 || walkthrough)
331
+ writeState({ ...state, records, walkthroughs });
332
+ return { inserted: normalized.length, total: records.length, walkthroughs: walkthroughs?.length ?? 0, records: normalized };
333
+ }
334
+ export function clearChangeAuditRecords(filter = {}) {
335
+ const state = readStateForMutation();
336
+ const ids = new Set((Array.isArray(filter.ids) ? filter.ids : []).filter((id) => typeof id === 'string' && id.length > 0));
337
+ const workspaceRoot = normalizeString(filter.workspaceRoot);
338
+ const repoRoot = normalizeString(filter.repoRoot);
339
+ const shouldDelete = (record) => {
340
+ if (ids.size > 0)
341
+ return ids.has(record.id);
342
+ if (repoRoot)
343
+ return record.repoRoot === repoRoot;
344
+ if (workspaceRoot)
345
+ return record.workspaceRoot === workspaceRoot;
346
+ return false;
347
+ };
348
+ const records = state.records.filter((record) => !shouldDelete(record));
349
+ const deleted = state.records.length - records.length;
350
+ let walkthroughs = state.walkthroughs;
351
+ if (repoRoot || workspaceRoot || ids.size === 0) {
352
+ const currentWalkthroughs = state.walkthroughs ?? [];
353
+ walkthroughs = currentWalkthroughs.filter((walkthrough) => {
354
+ if (repoRoot && walkthrough.repoRoot === repoRoot)
355
+ return false;
356
+ if (workspaceRoot && walkthrough.workspaceRoot === workspaceRoot)
357
+ return false;
358
+ return true;
359
+ });
360
+ }
361
+ if (deleted > 0 || walkthroughs !== state.walkthroughs)
362
+ writeState({ ...state, records, walkthroughs });
363
+ return { deleted, total: records.length };
364
+ }
365
+ function normalizeBranchRecord(record, fallback, injectedAt) {
366
+ const repoRoot = normalizeString(record.repoRoot) ?? normalizeString(fallback.repoRoot);
367
+ const baseRef = normalizeString(record.baseRef) ?? normalizeString(fallback.baseRef);
368
+ const filePath = normalizeString(record.filePath);
369
+ const hunkHeader = normalizeString(record.hunkHeader);
370
+ const fingerprint = normalizeString(record.fingerprint);
371
+ const explanation = normalizeString(record.explanation);
372
+ if (!repoRoot || !baseRef || !filePath || !hunkHeader || !fingerprint || !explanation)
373
+ return null;
374
+ const branchName = normalizeString(record.branchName) ?? normalizeString(fallback.branchName);
375
+ const hunkIndex = typeof record.hunkIndex === 'number' && Number.isFinite(record.hunkIndex)
376
+ ? Math.max(0, Math.floor(record.hunkIndex))
377
+ : null;
378
+ const sectionIndex = typeof record.sectionIndex === 'number' && Number.isFinite(record.sectionIndex)
379
+ ? Math.max(0, Math.floor(record.sectionIndex))
380
+ : null;
381
+ const sectionFingerprint = normalizeString(record.sectionFingerprint);
382
+ return {
383
+ id: buildBranchAuditRecordId({ repoRoot, baseRef, branchName, filePath, hunkHeader, fingerprint, sectionIndex, sectionFingerprint }),
384
+ workspaceRoot: normalizeString(fallback.workspaceRoot),
385
+ repoRoot,
386
+ baseRef,
387
+ branchName,
388
+ headRef: normalizeString(record.headRef) ?? normalizeString(fallback.headRef),
389
+ diffFingerprint: normalizeString(fallback.diffFingerprint),
390
+ filePath,
391
+ oldPath: normalizeString(record.oldPath),
392
+ newPath: normalizeString(record.newPath),
393
+ hunkHeader,
394
+ hunkIndex,
395
+ sectionIndex,
396
+ sectionFingerprint,
397
+ fingerprint,
398
+ diff: normalizeString(record.diff),
399
+ explanation,
400
+ summary: normalizeString(record.summary),
401
+ generatedBy: normalizeString(fallback.generatedBy),
402
+ injectedAt,
403
+ };
404
+ }
405
+ export function injectBranchAudit(payload) {
406
+ const injectedAt = Date.now();
407
+ const sourceRecords = Array.isArray(payload.records) ? payload.records : [];
408
+ const normalized = sourceRecords
409
+ .map((record) => normalizeBranchRecord(record, payload, injectedAt))
410
+ .filter((record) => record !== null);
411
+ const state = readStateForMutation();
412
+ const nextById = new Map((state.branchRecords ?? []).map((entry) => [entry.id, entry]));
413
+ for (const record of normalized)
414
+ nextById.set(record.id, record);
415
+ const branchRecords = Array.from(nextById.values())
416
+ .sort((a, b) => b.injectedAt - a.injectedAt)
417
+ .slice(0, MAX_RECORDS);
418
+ const walkthrough = normalizeWalkthrough(payload.walkthrough, payload, injectedAt);
419
+ const walkthroughs = walkthrough
420
+ ? [walkthrough, ...(state.walkthroughs ?? []).filter((entry) => (entry.repoRoot !== walkthrough.repoRoot
421
+ || entry.baseRef !== walkthrough.baseRef
422
+ || entry.branchName !== walkthrough.branchName
423
+ || entry.diffFingerprint !== walkthrough.diffFingerprint
424
+ || entry.id !== walkthrough.id))].slice(0, 100)
425
+ : state.walkthroughs;
426
+ if (normalized.length > 0 || walkthrough)
427
+ writeState({ ...state, branchRecords, walkthroughs });
428
+ return { inserted: normalized.length, total: branchRecords.length, walkthroughs: walkthroughs?.length ?? 0, records: normalized };
429
+ }
430
+ export function clearBranchAuditRecords(filter = {}) {
431
+ const state = readStateForMutation();
432
+ const ids = new Set((Array.isArray(filter.ids) ? filter.ids : []).filter((id) => typeof id === 'string' && id.length > 0));
433
+ const workspaceRoot = normalizeString(filter.workspaceRoot);
434
+ const repoRoot = normalizeString(filter.repoRoot);
435
+ const baseRef = normalizeString(filter.baseRef);
436
+ const branchName = normalizeString(filter.branchName);
437
+ const current = state.branchRecords ?? [];
438
+ const shouldDelete = (record) => {
439
+ if (ids.size > 0)
440
+ return ids.has(record.id);
441
+ if (repoRoot && record.repoRoot !== repoRoot)
442
+ return false;
443
+ if (workspaceRoot && record.workspaceRoot && record.workspaceRoot !== workspaceRoot)
444
+ return false;
445
+ if (baseRef && record.baseRef !== baseRef)
446
+ return false;
447
+ if (branchName && record.branchName !== branchName)
448
+ return false;
449
+ return Boolean(repoRoot || workspaceRoot || baseRef || branchName);
450
+ };
451
+ const branchRecords = current.filter((record) => !shouldDelete(record));
452
+ const deleted = current.length - branchRecords.length;
453
+ let walkthroughs = state.walkthroughs;
454
+ if (repoRoot || workspaceRoot || baseRef || branchName || ids.size === 0) {
455
+ const currentWalkthroughs = state.walkthroughs ?? [];
456
+ walkthroughs = currentWalkthroughs.filter((walkthrough) => {
457
+ if (repoRoot && walkthrough.repoRoot !== repoRoot)
458
+ return true;
459
+ if (workspaceRoot && walkthrough.workspaceRoot && walkthrough.workspaceRoot !== workspaceRoot)
460
+ return true;
461
+ if (baseRef && walkthrough.baseRef !== baseRef)
462
+ return true;
463
+ if (branchName && walkthrough.branchName !== branchName)
464
+ return true;
465
+ return !(repoRoot || workspaceRoot || baseRef || branchName);
466
+ });
467
+ }
468
+ if (deleted > 0 || walkthroughs !== state.walkthroughs)
469
+ writeState({ ...state, branchRecords, walkthroughs });
470
+ return { deleted, total: branchRecords.length };
471
+ }
472
+ export function listChangeAuditRecords(filter = {}) {
473
+ refreshChangeAuditCache();
474
+ const workspaceRoot = normalizeString(filter.workspaceRoot);
475
+ const repoRoot = normalizeString(filter.repoRoot);
476
+ const records = cachedState.records.filter((record) => {
477
+ if (repoRoot && record.repoRoot !== repoRoot)
478
+ return false;
479
+ if (workspaceRoot && record.workspaceRoot && record.workspaceRoot !== workspaceRoot)
480
+ return false;
481
+ return true;
482
+ });
483
+ const walkthroughs = (cachedState.walkthroughs ?? []).filter((walkthrough) => {
484
+ if (repoRoot && walkthrough.repoRoot !== repoRoot)
485
+ return false;
486
+ if (workspaceRoot && walkthrough.workspaceRoot && walkthrough.workspaceRoot !== workspaceRoot)
487
+ return false;
488
+ return true;
489
+ });
490
+ return { records, walkthroughs, loading: reloadInFlight };
491
+ }
492
+ export function listBranchAuditRecords(filter = {}) {
493
+ refreshChangeAuditCache();
494
+ const workspaceRoot = normalizeString(filter.workspaceRoot);
495
+ const repoRoot = normalizeString(filter.repoRoot);
496
+ const baseRef = normalizeString(filter.baseRef);
497
+ const branchName = normalizeString(filter.branchName);
498
+ const records = (cachedState.branchRecords ?? []).filter((record) => {
499
+ if (repoRoot && record.repoRoot !== repoRoot)
500
+ return false;
501
+ if (workspaceRoot && record.workspaceRoot && record.workspaceRoot !== workspaceRoot)
502
+ return false;
503
+ if (baseRef && record.baseRef !== baseRef)
504
+ return false;
505
+ if (branchName && record.branchName !== branchName)
506
+ return false;
507
+ return true;
508
+ });
509
+ const walkthroughs = (cachedState.walkthroughs ?? []).filter((walkthrough) => {
510
+ if (repoRoot && walkthrough.repoRoot !== repoRoot)
511
+ return false;
512
+ if (workspaceRoot && walkthrough.workspaceRoot && walkthrough.workspaceRoot !== workspaceRoot)
513
+ return false;
514
+ if (baseRef && walkthrough.baseRef !== baseRef)
515
+ return false;
516
+ if (branchName && walkthrough.branchName !== branchName)
517
+ return false;
518
+ return true;
519
+ });
520
+ return { records, walkthroughs, loading: reloadInFlight };
521
+ }
@@ -0,0 +1,13 @@
1
+ let secureCookies = false;
2
+ export function setSecureCookieMode(enabled) {
3
+ secureCookies = enabled;
4
+ }
5
+ export function shouldUseSecureCookies() {
6
+ return secureCookies;
7
+ }
8
+ export function getCookieSecurityOptions() {
9
+ return {
10
+ secure: secureCookies,
11
+ sameSite: 'lax',
12
+ };
13
+ }
@@ -0,0 +1,112 @@
1
+ import crypto from 'crypto';
2
+ import { getCookieSecurityOptions } from './cookieSecurity.js';
3
+ /**
4
+ * CSRF令牌管理器
5
+ */
6
+ export class CsrfProtection {
7
+ config;
8
+ constructor(config = {}) {
9
+ this.config = {
10
+ cookieName: config.cookieName || 'XSRF-TOKEN',
11
+ headerName: config.headerName || 'X-XSRF-TOKEN',
12
+ tokenLength: config.tokenLength || 32,
13
+ cookieOptions: {
14
+ httpOnly: true,
15
+ secure: config.cookieOptions?.secure ?? false,
16
+ sameSite: config.cookieOptions?.sameSite ?? 'strict',
17
+ maxAge: 24 * 60 * 60 * 1000, // 24小时
18
+ ...config.cookieOptions,
19
+ },
20
+ };
21
+ }
22
+ /**
23
+ * 生成CSRF令牌
24
+ */
25
+ generateToken() {
26
+ return crypto.randomBytes(this.config.tokenLength).toString('hex');
27
+ }
28
+ /**
29
+ * 验证CSRF令牌
30
+ */
31
+ verifyToken(cookieToken, headerToken) {
32
+ if (typeof cookieToken !== 'string' || typeof headerToken !== 'string') {
33
+ return false;
34
+ }
35
+ const cookieBuffer = Buffer.from(cookieToken);
36
+ const headerBuffer = Buffer.from(headerToken);
37
+ if (cookieBuffer.length !== headerBuffer.length) {
38
+ return false;
39
+ }
40
+ // 使用定时安全比较防止时序攻击
41
+ return crypto.timingSafeEqual(cookieBuffer, headerBuffer);
42
+ }
43
+ /**
44
+ * Express中间件:生成并设置CSRF令牌
45
+ */
46
+ tokenMiddleware() {
47
+ return (req, res, next) => {
48
+ if (req.method === 'GET' && !req.cookies?.[this.config.cookieName]) {
49
+ const token = this.generateToken();
50
+ res.cookie(this.config.cookieName, token, {
51
+ ...this.config.cookieOptions,
52
+ ...getCookieSecurityOptions(),
53
+ });
54
+ // 对于API端点,也返回令牌在响应头中
55
+ if (req.path.startsWith('/api/')) {
56
+ res.setHeader(this.config.headerName, token);
57
+ }
58
+ }
59
+ next();
60
+ };
61
+ }
62
+ /**
63
+ * Express中间件:验证CSRF令牌
64
+ */
65
+ verifyMiddleware() {
66
+ return (req, res, next) => {
67
+ // 跳过安全检查和GET/HEAD/OPTIONS请求
68
+ const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
69
+ if (safeMethods.includes(req.method)) {
70
+ return next();
71
+ }
72
+ // 跳过某些端点(如果需要)
73
+ const skipPaths = ['/api/csrf-token'];
74
+ if (skipPaths.some(path => req.path.startsWith(path))) {
75
+ return next();
76
+ }
77
+ // 获取令牌
78
+ const cookieToken = req.cookies?.[this.config.cookieName];
79
+ const headerToken = req.headers[this.config.headerName.toLowerCase()];
80
+ // 验证令牌
81
+ if (!this.verifyToken(cookieToken, headerToken)) {
82
+ console.warn(`CSRF token validation failed for ${req.method} ${req.path}`);
83
+ return res.status(403).json({
84
+ error: 'CSRF token validation failed',
85
+ code: 'CSRF_ERROR',
86
+ });
87
+ }
88
+ next();
89
+ };
90
+ }
91
+ /**
92
+ * 获取CSRF令牌端点处理器
93
+ */
94
+ getTokenHandler() {
95
+ return (req, res) => {
96
+ const existing = req.cookies?.[this.config.cookieName];
97
+ const token = typeof existing === 'string' && existing.length > 0
98
+ ? existing
99
+ : this.generateToken();
100
+ res.cookie(this.config.cookieName, token, {
101
+ ...this.config.cookieOptions,
102
+ ...getCookieSecurityOptions(),
103
+ });
104
+ res.json({
105
+ csrfToken: token,
106
+ headerName: this.config.headerName,
107
+ });
108
+ };
109
+ }
110
+ }
111
+ // 默认实例
112
+ export const csrfProtection = new CsrfProtection();