wiki-viewer 1.7.1 → 1.8.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 (437) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +7 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +32 -3
  5. package/.next/standalone/.next/routes-manifest.json +46 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.rsc +4 -4
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  17. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/standalone/.next/server/app/api/agent/activity/route.js +6 -6
  23. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js +6 -6
  25. package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js +6 -6
  27. package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js +7 -7
  29. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js +5 -5
  31. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js +5 -5
  33. package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js +8 -7
  35. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +9 -8
  37. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +8 -7
  39. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +7 -7
  41. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +6 -5
  43. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +7 -6
  45. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js +6 -5
  47. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/agent/settings/route.js +7 -7
  49. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js +8 -7
  51. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +2 -2
  53. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js +8 -2
  55. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/auth/[...all]/route.js +5 -5
  57. package/.next/standalone/.next/server/app/api/auth/[...all]/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/system/admins/route/app-paths-manifest.json +3 -0
  59. package/.next/standalone/.next/server/app/api/system/admins/route/build-manifest.json +9 -0
  60. package/.next/standalone/.next/server/app/api/system/admins/route/server-reference-manifest.json +4 -0
  61. package/.next/standalone/.next/server/app/api/system/admins/route.js +14 -0
  62. package/.next/standalone/.next/server/app/api/system/admins/route.js.map +5 -0
  63. package/.next/standalone/.next/server/app/api/system/admins/route.js.nft.json +1 -0
  64. package/.next/standalone/.next/server/app/api/system/admins/route_client-reference-manifest.js +3 -0
  65. package/.next/standalone/.next/server/app/api/system/auth-config/route.js +6 -6
  66. package/.next/standalone/.next/server/app/api/system/auth-config/route.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/api/system/auth-settings/route.js +4 -4
  68. package/.next/standalone/.next/server/app/api/system/auth-settings/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/api/system/browse/route.js +4 -4
  70. package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/api/system/clear-root/route.js +5 -5
  72. package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/system/config/route.js +6 -6
  74. package/.next/standalone/.next/server/app/api/system/config/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/system/pins/route.js +4 -4
  76. package/.next/standalone/.next/server/app/api/system/pins/route.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/api/system/reveal/route.js +6 -6
  78. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/api/system/root-status/route.js +5 -5
  80. package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
  81. package/.next/standalone/.next/server/app/api/system/set-root/route.js +6 -6
  82. package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/api/system/users/route/app-paths-manifest.json +3 -0
  84. package/.next/standalone/.next/server/app/api/system/users/route/build-manifest.json +9 -0
  85. package/.next/standalone/.next/server/app/api/system/users/route/server-reference-manifest.json +4 -0
  86. package/.next/standalone/.next/server/app/api/system/users/route.js +14 -0
  87. package/.next/standalone/.next/server/app/api/system/users/route.js.map +5 -0
  88. package/.next/standalone/.next/server/app/api/system/users/route.js.nft.json +1 -0
  89. package/.next/standalone/.next/server/app/api/system/users/route_client-reference-manifest.js +3 -0
  90. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route/app-paths-manifest.json +3 -0
  91. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route/build-manifest.json +9 -0
  92. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route/server-reference-manifest.json +4 -0
  93. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js +14 -0
  94. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js.map +5 -0
  95. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js.nft.json +1 -0
  96. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route_client-reference-manifest.js +3 -0
  97. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route/app-paths-manifest.json +3 -0
  98. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route/build-manifest.json +9 -0
  99. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route/server-reference-manifest.json +4 -0
  100. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js +14 -0
  101. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js.map +5 -0
  102. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js.nft.json +1 -0
  103. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route_client-reference-manifest.js +3 -0
  104. package/.next/standalone/.next/server/app/api/system/workspaces/route/app-paths-manifest.json +3 -0
  105. package/.next/standalone/.next/server/app/api/system/workspaces/route/build-manifest.json +9 -0
  106. package/.next/standalone/.next/server/app/api/system/workspaces/route/server-reference-manifest.json +4 -0
  107. package/.next/standalone/.next/server/app/api/system/workspaces/route.js +14 -0
  108. package/.next/standalone/.next/server/app/api/system/workspaces/route.js.map +5 -0
  109. package/.next/standalone/.next/server/app/api/system/workspaces/route.js.nft.json +1 -0
  110. package/.next/standalone/.next/server/app/api/system/workspaces/route_client-reference-manifest.js +3 -0
  111. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js +8 -2
  112. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
  113. package/.next/standalone/.next/server/app/api/wiki/app/route.js +5 -5
  114. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/api/wiki/content/route.js +6 -6
  116. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  117. package/.next/standalone/.next/server/app/api/wiki/download/route.js +7 -7
  118. package/.next/standalone/.next/server/app/api/wiki/download/route.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/api/wiki/file/route.js +8 -2
  120. package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/api/wiki/folder/route.js +6 -6
  122. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/api/wiki/move/route.js +6 -6
  124. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  125. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js +7 -7
  126. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  127. package/.next/standalone/.next/server/app/api/wiki/page/route.js +7 -7
  128. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  129. package/.next/standalone/.next/server/app/api/wiki/presence/route.js +7 -7
  130. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -1
  131. package/.next/standalone/.next/server/app/api/wiki/route.js +6 -6
  132. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/api/wiki/search/route/app-paths-manifest.json +3 -0
  134. package/.next/standalone/.next/server/app/api/wiki/search/route/build-manifest.json +9 -0
  135. package/.next/standalone/.next/server/app/api/wiki/search/route/server-reference-manifest.json +4 -0
  136. package/.next/standalone/.next/server/app/api/wiki/search/route.js +15 -0
  137. package/.next/standalone/.next/server/app/api/wiki/search/route.js.map +5 -0
  138. package/.next/standalone/.next/server/app/api/wiki/search/route.js.nft.json +1 -0
  139. package/.next/standalone/.next/server/app/api/wiki/search/route_client-reference-manifest.js +3 -0
  140. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js +6 -6
  141. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/api/wiki/upload/route.js +7 -7
  143. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  144. package/.next/standalone/.next/server/app/api/wiki/watch/route.js +7 -9
  145. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  146. package/.next/standalone/.next/server/app/index.html +1 -1
  147. package/.next/standalone/.next/server/app/index.rsc +5 -5
  148. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  149. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +5 -5
  150. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  151. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  152. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  153. package/.next/standalone/.next/server/app/manifest.webmanifest/route/app-paths-manifest.json +3 -0
  154. package/.next/standalone/.next/server/app/manifest.webmanifest/route/build-manifest.json +9 -0
  155. package/.next/standalone/.next/server/app/manifest.webmanifest/route/server-reference-manifest.json +4 -0
  156. package/.next/standalone/.next/server/app/manifest.webmanifest/route.js +9 -0
  157. package/.next/standalone/.next/server/app/manifest.webmanifest/route.js.map +5 -0
  158. package/.next/standalone/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -0
  159. package/.next/standalone/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +3 -0
  160. package/.next/standalone/.next/server/app/manifest.webmanifest.body +1 -0
  161. package/.next/standalone/.next/server/app/manifest.webmanifest.meta +1 -0
  162. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  163. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  164. package/.next/standalone/.next/server/app/signin/page.js +1 -1
  165. package/.next/standalone/.next/server/app/signin/page.js.nft.json +1 -1
  166. package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
  167. package/.next/standalone/.next/server/app-paths-manifest.json +7 -0
  168. package/.next/standalone/.next/server/chunks/0p91_next_dist_1195ub1._.js +18 -0
  169. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +1 -1
  170. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +1 -1
  171. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-244bv._.js +16 -0
  172. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0xz-_0v._.js → [root-of-the-server]__0-inabx._.js} +2 -2
  173. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0-ao6a_._.js → [root-of-the-server]__0.-9jt0._.js} +1 -1
  174. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0~-r-8f._.js → [root-of-the-server]__00c1-dr._.js} +1 -1
  175. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00lep-u._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00mtevo._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00qu86p._.js +3 -0
  178. package/.next/standalone/.next/server/chunks/[root-of-the-server]__012753j._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01hp8ax._.js +3 -0
  180. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0jhr.ex._.js → [root-of-the-server]__02n6z97._.js} +1 -1
  181. package/.next/standalone/.next/server/chunks/[root-of-the-server]__036mhjp._.js +5 -0
  182. package/.next/standalone/.next/server/chunks/[root-of-the-server]__03p0ob1._.js +16 -0
  183. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05t~v6l._.js +3 -0
  184. package/.next/standalone/.next/server/chunks/[root-of-the-server]__068ni_k._.js +3 -0
  185. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06yp.08._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__082nhm6._.js → [root-of-the-server]__07vphbd._.js} +1 -1
  187. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0pg6dn-._.js → [root-of-the-server]__08lkuwa._.js} +1 -1
  188. package/.next/standalone/.next/server/chunks/[root-of-the-server]__09cqzem._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0p_ox-k._.js → [root-of-the-server]__09k4q9e._.js} +1 -1
  190. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0_601._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ae3dk6._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0s28.4-._.js → [root-of-the-server]__0af-x45._.js} +1 -1
  193. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cqu-wm._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
  195. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d7x3e1._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d7zxm_._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7vs98._.js +3 -0
  198. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0fxzg4v._.js +3 -0
  199. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gda_iu._.js +3 -0
  200. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hu~q20._.js +3 -0
  201. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jk1uf4._.js +3 -0
  202. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l-8clq._.js +3 -0
  203. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lhka0x._.js +3 -0
  204. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0kdn1le._.js → [root-of-the-server]__0na11h0._.js} +2 -2
  205. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nd.b_c._.js +3 -0
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0otb8ra._.js +3 -0
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p0-p__._.js +3 -0
  208. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0piofm3._.js +3 -0
  209. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rrf-jr._.js +3 -0
  210. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s6_lzw._.js +3 -0
  211. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sh13v5._.js +3 -0
  212. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ta~flu._.js +3 -0
  213. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0v4im4p._.js +3 -0
  214. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0qn8e~s._.js → [root-of-the-server]__0v_kyp-._.js} +1 -1
  215. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vatqe-._.js +3 -0
  216. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vdb2nr._.js +3 -0
  217. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wdh63w._.js +3 -0
  218. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z3wg9x._.js +12 -0
  219. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0mzo03s._.js → [root-of-the-server]__0~.-3yk._.js} +1 -1
  220. package/.next/standalone/.next/server/chunks/[root-of-the-server]__123._fe._.js +3 -0
  221. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__12800b1._.js → [root-of-the-server]__12ee_s6._.js} +1 -1
  222. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12onytt._.js +3 -0
  223. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__04udrya._.js → [root-of-the-server]__12q8ch8._.js} +2 -2
  224. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1377.79._.js +3 -0
  225. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1378-ox._.js +3 -0
  226. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13aid91._.js +3 -0
  227. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13hp4tw._.js +12 -0
  228. package/.next/standalone/.next/server/chunks/_079xuln._.js +1 -1
  229. package/.next/standalone/.next/server/chunks/_0dr1r1g._.js +452 -0
  230. package/.next/standalone/.next/server/chunks/_101exke._.js +452 -0
  231. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_admins_route_actions_0dsm7cx.js +3 -0
  232. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_users_route_actions_0whj5cy.js +3 -0
  233. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_[id]_open_route_actions_1087xu7.js +3 -0
  234. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_[id]_route_actions_0~h7-8s.js +3 -0
  235. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_route_actions_0mkalc3.js +3 -0
  236. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_search_route_actions_0inxu93.js +3 -0
  237. package/.next/standalone/.next/server/chunks/_next-internal_server_app_manifest_webmanifest_route_actions_04wfv0d.js +3 -0
  238. package/.next/standalone/.next/server/chunks/node_modules__pnpm_0aj5g-w._.js +1 -1
  239. package/.next/standalone/.next/server/chunks/src_lib_search_0d-_j3u._.js +46 -0
  240. package/.next/standalone/.next/server/chunks/src_lib_search_0ymmp0c._.js +46 -0
  241. package/.next/standalone/.next/server/chunks/ssr/0p91_next_dist_esm_build_templates_app-page_042apsk.js +1 -1
  242. package/.next/standalone/.next/server/chunks/ssr/0p91_next_dist_esm_build_templates_app-page_08q3u1e.js +1 -1
  243. package/.next/standalone/.next/server/chunks/ssr/0p91_next_dist_esm_build_templates_app-page_0nflz3~.js +1 -1
  244. package/.next/standalone/.next/server/chunks/ssr/0p91_next_dist_esm_build_templates_app-page_0p5k29e.js +1 -1
  245. package/.next/standalone/.next/server/chunks/ssr/{0~w0_katex_dist_katex_mjs_0b8ra51._.js → 0~w0_katex_dist_katex_mjs_04mhawr._.js} +1 -1
  246. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_erDiagram-TEJ5UH35_mjs_0v6rnqy._.js +1 -1
  247. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_mermaid_core_mjs_0hj_n-~._.js +1 -1
  248. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0btcd4o._.js → [root-of-the-server]__02twzph._.js} +2 -2
  249. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ytvlzs._.js → [root-of-the-server]__09~x2v~._.js} +2 -2
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hvvps3._.js +1 -1
  251. package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +18 -18
  252. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0c71i8a._.js → node_modules__pnpm_0.96tmo._.js} +1 -1
  253. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0lm8e2c._.js → node_modules__pnpm_011vy_q._.js} +2 -2
  254. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0ezm2us._.js → node_modules__pnpm_01o_5qt._.js} +1 -1
  255. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0j3mvgg._.js → node_modules__pnpm_04n0h6.._.js} +1 -1
  256. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0~ln0by._.js → node_modules__pnpm_093dp2q._.js} +1 -1
  257. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_11e~j49._.js → node_modules__pnpm_09ej62_._.js} +1 -1
  258. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0de8xt-._.js → node_modules__pnpm_09ljbdz._.js} +2 -2
  259. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-x6zex._.js → node_modules__pnpm_09~_ob2._.js} +1 -1
  260. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0yq5z.h._.js → node_modules__pnpm_0a01ng7._.js} +2 -2
  261. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0kwc-vs._.js → node_modules__pnpm_0b8qx0g._.js} +1 -1
  262. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hrbz84._.js → node_modules__pnpm_0bbc---._.js} +1 -1
  263. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hobcbs._.js → node_modules__pnpm_0c.309.._.js} +1 -1
  264. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0rw91-f._.js → node_modules__pnpm_0c8aunj._.js} +1 -1
  265. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0o.c1z8._.js → node_modules__pnpm_0hk6xys._.js} +2 -2
  266. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0dboa_e._.js → node_modules__pnpm_0hlzy8f._.js} +1 -1
  267. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0j53hti._.js → node_modules__pnpm_0nw5ou2._.js} +1 -1
  268. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xrmzh1._.js → node_modules__pnpm_0o.dn8o._.js} +1 -1
  269. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0cxwhfd._.js → node_modules__pnpm_0s0ur~d._.js} +1 -1
  270. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_07rxv2x._.js → node_modules__pnpm_0shlang._.js} +1 -1
  271. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0i.pw_m._.js → node_modules__pnpm_0sq3c2r._.js} +1 -1
  272. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0ajzar9._.js → node_modules__pnpm_0t9jb7d._.js} +1 -1
  273. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_012nt.s._.js → node_modules__pnpm_0wmx9kq._.js} +2 -2
  274. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0z~h2ow._.js → node_modules__pnpm_0xfx0sf._.js} +1 -1
  275. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0cbsnwf._.js → node_modules__pnpm_0yiz.-t._.js} +1 -1
  276. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0bum3-3._.js → node_modules__pnpm_11sut1u._.js} +1 -1
  277. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0mdo4i3._.js → node_modules__pnpm_124u4o2._.js} +2 -2
  278. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0il8j.j._.js → node_modules__pnpm_13tajgx._.js} +2 -2
  279. package/.next/standalone/.next/server/chunks/ssr/src_app_signin_00yk0vm._.js +2 -2
  280. package/.next/standalone/.next/server/edge/chunks/{08yl_next_dist_esm_build_templates_edge-wrapper_0o9h3w_.js → 08yl_next_dist_esm_build_templates_edge-wrapper_0xbpts6.js} +2 -2
  281. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0.pmgt2._.js +3 -0
  282. package/.next/standalone/.next/server/functions-config-manifest.json +6 -0
  283. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  284. package/.next/standalone/.next/server/middleware-manifest.json +8 -8
  285. package/.next/standalone/.next/server/pages/404.html +1 -1
  286. package/.next/standalone/.next/server/pages/500.html +1 -1
  287. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  288. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  289. package/.next/standalone/.next/static/chunks/0c_~zevf2n0r4.css +2 -0
  290. package/.next/standalone/.next/static/chunks/0l4842ez-0_66.js +129 -0
  291. package/.next/standalone/README.md +46 -2
  292. package/.next/standalone/docs/workspacing-plan.md +364 -0
  293. package/.next/standalone/package.json +2 -2
  294. package/.next/standalone/packages/wiki-viewer-mcp/package.json +1 -1
  295. package/.next/standalone/public/icon-192.png +0 -0
  296. package/.next/standalone/public/icon-512.png +0 -0
  297. package/.next/standalone/public/icon-maskable-512.png +0 -0
  298. package/.next/standalone/src/app/api/agent/activity/route.ts +9 -8
  299. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +20 -8
  300. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +16 -9
  301. package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +28 -18
  302. package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +12 -7
  303. package/.next/standalone/src/app/api/agent/fs/move/route.ts +16 -11
  304. package/.next/standalone/src/app/api/agent/fs/search/route.ts +49 -13
  305. package/.next/standalone/src/app/api/agent/internal/span/route.ts +9 -5
  306. package/.next/standalone/src/app/api/agent/register/route.ts +9 -1
  307. package/.next/standalone/src/app/api/agent/settings/route.ts +7 -3
  308. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +8 -4
  309. package/.next/standalone/src/app/api/assets/[...path]/route.ts +9 -4
  310. package/.next/standalone/src/app/api/system/admins/route.ts +103 -0
  311. package/.next/standalone/src/app/api/system/reveal/route.ts +7 -6
  312. package/.next/standalone/src/app/api/system/set-root/route.ts +24 -1
  313. package/.next/standalone/src/app/api/system/users/route.ts +120 -0
  314. package/.next/standalone/src/app/api/system/workspaces/[id]/open/route.ts +34 -0
  315. package/.next/standalone/src/app/api/system/workspaces/[id]/route.ts +74 -0
  316. package/.next/standalone/src/app/api/system/workspaces/route.ts +67 -0
  317. package/.next/standalone/src/app/api/upload/[...path]/route.ts +10 -5
  318. package/.next/standalone/src/app/api/wiki/app/route.ts +10 -9
  319. package/.next/standalone/src/app/api/wiki/content/route.ts +14 -14
  320. package/.next/standalone/src/app/api/wiki/download/route.ts +7 -6
  321. package/.next/standalone/src/app/api/wiki/file/route.ts +7 -2
  322. package/.next/standalone/src/app/api/wiki/folder/route.ts +6 -5
  323. package/.next/standalone/src/app/api/wiki/move/route.ts +7 -7
  324. package/.next/standalone/src/app/api/wiki/new-file/route.ts +7 -6
  325. package/.next/standalone/src/app/api/wiki/page/route.ts +6 -5
  326. package/.next/standalone/src/app/api/wiki/presence/route.ts +8 -7
  327. package/.next/standalone/src/app/api/wiki/route.ts +13 -11
  328. package/.next/standalone/src/app/api/wiki/search/route.ts +44 -0
  329. package/.next/standalone/src/app/api/wiki/slugs/route.ts +8 -8
  330. package/.next/standalone/src/app/api/wiki/upload/route.ts +6 -5
  331. package/.next/standalone/src/app/api/wiki/watch/route.ts +16 -40
  332. package/.next/standalone/src/app/globals.css +484 -484
  333. package/.next/standalone/src/app/layout.tsx +15 -1
  334. package/.next/standalone/src/app/manifest.ts +30 -0
  335. package/.next/standalone/src/app/page.tsx +163 -49
  336. package/.next/standalone/src/components/auth-settings-sheet.tsx +171 -2
  337. package/.next/standalone/src/components/dir-picker.tsx +8 -6
  338. package/.next/standalone/src/components/editor/comment-thread.tsx +2 -1
  339. package/.next/standalone/src/components/editor/csv-viewer.tsx +3 -2
  340. package/.next/standalone/src/components/editor/editor-toolbar.tsx +2 -2
  341. package/.next/standalone/src/components/editor/editor.tsx +6 -5
  342. package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +3 -2
  343. package/.next/standalone/src/components/editor/folder-index.tsx +2 -1
  344. package/.next/standalone/src/components/editor/image-viewer.tsx +2 -1
  345. package/.next/standalone/src/components/editor/media-popover.tsx +2 -1
  346. package/.next/standalone/src/components/editor/media-viewer.tsx +2 -1
  347. package/.next/standalone/src/components/editor/mermaid-viewer.tsx +3 -2
  348. package/.next/standalone/src/components/editor/node-app-viewer.tsx +4 -3
  349. package/.next/standalone/src/components/editor/notebook-viewer.tsx +3 -2
  350. package/.next/standalone/src/components/editor/office/docx-viewer.tsx +2 -1
  351. package/.next/standalone/src/components/editor/office/office-chrome.tsx +3 -2
  352. package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +2 -1
  353. package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +2 -1
  354. package/.next/standalone/src/components/editor/pdf-viewer.tsx +2 -1
  355. package/.next/standalone/src/components/editor/proof-span-popover.tsx +2 -1
  356. package/.next/standalone/src/components/editor/source-viewer.tsx +3 -2
  357. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +2 -1
  358. package/.next/standalone/src/components/editor/suggestion-card.tsx +2 -1
  359. package/.next/standalone/src/components/editor/website-viewer.tsx +2 -1
  360. package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +2 -1
  361. package/.next/standalone/src/components/search/search-command-dialog.tsx +109 -0
  362. package/.next/standalone/src/components/search/sidebar-search-box.tsx +121 -0
  363. package/.next/standalone/src/components/search/snippet-text.tsx +48 -0
  364. package/.next/standalone/src/components/ui/command.tsx +3 -2
  365. package/.next/standalone/src/lib/auth/admin.ts +112 -0
  366. package/.next/standalone/src/lib/auth/server.ts +13 -2
  367. package/.next/standalone/src/lib/config.ts +56 -4
  368. package/.next/standalone/src/lib/proof/audit.ts +19 -10
  369. package/.next/standalone/src/lib/proof/auth.ts +11 -0
  370. package/.next/standalone/src/lib/proof/collab-state.ts +4 -2
  371. package/.next/standalone/src/lib/proof/file-lock.ts +5 -0
  372. package/.next/standalone/src/lib/proof/idempotency.ts +5 -0
  373. package/.next/standalone/src/lib/proof/lease.ts +40 -27
  374. package/.next/standalone/src/lib/proof/mutex.ts +4 -0
  375. package/.next/standalone/src/lib/proof/raw-fs.ts +2 -3
  376. package/.next/standalone/src/lib/proof/registry.ts +5 -0
  377. package/.next/standalone/src/lib/search/indexable-exts.ts +25 -0
  378. package/.next/standalone/src/lib/search/indexer.ts +567 -0
  379. package/.next/standalone/src/lib/search/sanitize.ts +35 -0
  380. package/.next/standalone/src/lib/search/search-db.ts +57 -0
  381. package/.next/standalone/src/lib/search/watcher-pool.ts +79 -0
  382. package/.next/standalone/src/lib/workspace-client.ts +82 -0
  383. package/.next/standalone/src/lib/workspace-context.ts +142 -0
  384. package/.next/standalone/src/lib/workspaces.ts +196 -0
  385. package/.next/standalone/src/stores/ai-panel-store.ts +3 -2
  386. package/.next/standalone/src/stores/editor-store.ts +4 -3
  387. package/.next/standalone/src/stores/proof-store.ts +4 -3
  388. package/.next/standalone/src/stores/search-store.ts +86 -0
  389. package/.next/standalone/src/stores/wiki-slugs-store.ts +2 -1
  390. package/.next/standalone/src/tests/proof/admin-create-user.test.ts +43 -0
  391. package/.next/standalone/src/tests/proof/agent-workspace-scope.test.ts +134 -0
  392. package/.next/standalone/src/tests/proof/collab-state.test.ts +43 -41
  393. package/.next/standalone/src/tests/proof/preload.ts +21 -0
  394. package/.next/standalone/src/tests/proof/search-index.test.ts +339 -0
  395. package/.next/standalone/src/tests/proof/workspace-isolation.test.ts +87 -0
  396. package/.next/standalone/src/tests/proof/workspaces-api.test.ts +227 -0
  397. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  398. package/README.md +46 -2
  399. package/package.json +2 -2
  400. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0o~h4zq.js +0 -5
  401. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.hb9q2._.js +0 -3
  402. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.uux34._.js +0 -3
  403. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0092hkp._.js +0 -3
  404. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +0 -3
  405. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05idwm9._.js +0 -3
  406. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +0 -3
  407. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +0 -3
  408. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07fpqks._.js +0 -3
  409. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094q5bj._.js +0 -3
  410. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0awzyon._.js +0 -3
  411. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +0 -3
  412. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +0 -3
  413. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +0 -3
  414. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0km8suj._.js +0 -3
  415. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +0 -15
  416. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0mkwoul._.js +0 -12
  417. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +0 -15
  418. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qyd-o8._.js +0 -3
  419. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rkve~v._.js +0 -3
  420. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rzo40z._.js +0 -3
  421. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sjocwy._.js +0 -3
  422. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ssj9~d._.js +0 -3
  423. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +0 -3
  424. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t220d-._.js +0 -3
  425. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t5mg.n._.js +0 -3
  426. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0trsqf4._.js +0 -3
  427. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +0 -3
  428. package/.next/standalone/.next/server/chunks/[root-of-the-server]__106uw_c._.js +0 -3
  429. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +0 -3
  430. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13w-0iy._.js +0 -3
  431. package/.next/standalone/.next/server/chunks/src_lib_auth_server_ts_00f4e0h._.js +0 -452
  432. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0ug2rvl._.js +0 -3
  433. package/.next/standalone/.next/static/chunks/0vwkqw_rofm-c.js +0 -129
  434. package/.next/standalone/.next/static/chunks/0zay6349~v_wc.css +0 -2
  435. /package/.next/standalone/.next/static/{H0E5wVJm9tAuN5CrU6zG2 → t7B6nXaoD1jo9f61t3aeK}/_buildManifest.js +0 -0
  436. /package/.next/standalone/.next/static/{H0E5wVJm9tAuN5CrU6zG2 → t7B6nXaoD1jo9f61t3aeK}/_clientMiddlewareManifest.js +0 -0
  437. /package/.next/standalone/.next/static/{H0E5wVJm9tAuN5CrU6zG2 → t7B6nXaoD1jo9f61t3aeK}/_ssgManifest.js +0 -0
@@ -9,7 +9,7 @@
9
9
  import { createHash } from "node:crypto";
10
10
  import { open, stat, rename, realpath, mkdir } from "node:fs/promises";
11
11
  import path from "node:path";
12
- import { getRootDir } from "@/lib/root-dir";
12
+
13
13
 
14
14
  // ── Denied path checks ──────────────────────────────────────────────────────
15
15
 
@@ -44,8 +44,7 @@ export function isMarkdown(filePath: string): boolean {
44
44
  *
45
45
  * Returns absolute path on success, null on rejection.
46
46
  */
47
- export async function safeAbsPath(rel: string): Promise<string | null> {
48
- const root = getRootDir();
47
+ export async function safeAbsPath(root: string, rel: string): Promise<string | null> {
49
48
  if (!root) return null;
50
49
 
51
50
  // Normalise and basic traversal guard
@@ -20,6 +20,11 @@ const LAST_SEEN_THROTTLE_MS = 30_000;
20
20
  export interface AgentScope {
21
21
  paths: string[]; // glob patterns
22
22
  ops: Array<"read" | "mutate" | "delete">;
23
+ /**
24
+ * If set, this agent may only operate in this specific workspace (by id).
25
+ * Undefined = wildcard: any workspace is allowed (back-compat for existing agents).
26
+ */
27
+ workspaceId?: string;
23
28
  }
24
29
 
25
30
  export interface Agent {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared extension sets for text-indexable files.
3
+ * Copied from src/app/api/wiki/content/route.ts so the indexer and
4
+ * the route share a single definition without importing a route module from lib.
5
+ */
6
+
7
+ export const TEXT_EXTS = new Set([
8
+ "txt", "md", "markdown", "json", "yaml", "yml", "toml", "csv", "tsv",
9
+ "xml", "html", "css", "js", "ts", "tsx", "jsx", "sh", "bash",
10
+ "zsh", "rb", "py", "go", "rs", "java", "c", "cpp", "h", "php",
11
+ "swift", "kt", "lua", "sql", "scss", "mmd", "mermaid", "ini",
12
+ "env", "log", "conf",
13
+ ]);
14
+
15
+ export const MARKDOWN_EXTS = new Set(["md", "markdown"]);
16
+
17
+ export function isIndexableExt(filename: string): boolean {
18
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
19
+ return TEXT_EXTS.has(ext);
20
+ }
21
+
22
+ export function isMarkdownExt(filename: string): boolean {
23
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
24
+ return MARKDOWN_EXTS.has(ext);
25
+ }
@@ -0,0 +1,567 @@
1
+ /**
2
+ * FTS5 full-text search indexer.
3
+ *
4
+ * Lifecycle:
5
+ * - ensureIndexer(wsId, rootDir) -- idempotent; starts background initial scan once
6
+ * - indexFile / deleteFile -- incremental updates (called by chokidar listener)
7
+ * - ftsSearch -- BM25 search query
8
+ * - purgeWorkspace -- called on workspace delete
9
+ *
10
+ * Thread safety: better-sqlite3 is synchronous and single-threaded. All DB
11
+ * writes are serialised through JS's single thread. Event-loop blocking from the
12
+ * initial scan is avoided by yielding via setImmediate every INITIAL_YIELD_EVERY files.
13
+ */
14
+ import { createHash } from "node:crypto";
15
+ import { readFile, readdir, stat } from "node:fs/promises";
16
+ import type { Dirent } from "node:fs";
17
+ import path from "node:path";
18
+ import { getSearchDb } from "./search-db";
19
+ import { sanitizeFtsQuery } from "./sanitize";
20
+ import { isIndexableExt, isMarkdownExt } from "./indexable-exts";
21
+ import { isDeniedRelPath, looksLikeBinary, safeAbsPath } from "../proof/raw-fs";
22
+ import { isNodeApp, isAppFolder } from "../wiki-helpers";
23
+ import { parseFrontmatter } from "../markdown/parse-frontmatter";
24
+ import { subscribe } from "./watcher-pool";
25
+
26
+ // ── Constants ──────────────────────────────────────────────────────────────────
27
+
28
+ const MAX_INDEX_BYTES = 1024 * 1024; // 1 MiB body cap
29
+ const BATCH_SIZE = 50; // max files per DB transaction
30
+ const BATCH_TIMEOUT_MS = 2000; // flush pending queue after this many ms
31
+ const INITIAL_YIELD_EVERY = 64; // setImmediate yield every N files
32
+
33
+ const SKIP_DIRS = new Set([".proof", ".git", "node_modules", ".next"]);
34
+
35
+ // ── Per-workspace state ────────────────────────────────────────────────────────
36
+
37
+ interface WsState {
38
+ rootDir: string;
39
+ initialScanDone: boolean;
40
+ initialScanPromise: Promise<void> | null;
41
+ pendingPaths: Set<string>;
42
+ pendingTimer: ReturnType<typeof setTimeout> | null;
43
+ unsubscribeWatcher: (() => void) | null;
44
+ }
45
+
46
+ const states = new Map<string, WsState>();
47
+
48
+ function getState(wsId: string, rootDir: string): WsState {
49
+ let s = states.get(wsId);
50
+ if (!s) {
51
+ s = {
52
+ rootDir,
53
+ initialScanDone: false,
54
+ initialScanPromise: null,
55
+ pendingPaths: new Set(),
56
+ pendingTimer: null,
57
+ unsubscribeWatcher: null,
58
+ };
59
+ states.set(wsId, s);
60
+ }
61
+ return s;
62
+ }
63
+
64
+ // ── DB helpers ─────────────────────────────────────────────────────────────────
65
+
66
+ function upsertDoc(
67
+ wsId: string,
68
+ relPath: string,
69
+ name: string,
70
+ frontmatter: string,
71
+ body: string,
72
+ size: number,
73
+ mtimeNs: bigint,
74
+ sha: string,
75
+ ): void {
76
+ const db = getSearchDb();
77
+ // DELETE + INSERT because FTS5 has no native UPSERT.
78
+ db.prepare(`DELETE FROM docs WHERE ws = ? AND path = ?`).run(wsId, relPath);
79
+ db.prepare(`INSERT INTO docs (ws, path, name, frontmatter, body) VALUES (?, ?, ?, ?, ?)`)
80
+ .run(wsId, relPath, name, frontmatter, body);
81
+ db.prepare(`
82
+ INSERT INTO docs_meta (ws, path, size, mtime_ns, sha, indexed_at)
83
+ VALUES (?, ?, ?, ?, ?, ?)
84
+ ON CONFLICT(ws, path) DO UPDATE SET
85
+ size = excluded.size,
86
+ mtime_ns = excluded.mtime_ns,
87
+ sha = excluded.sha,
88
+ indexed_at = excluded.indexed_at
89
+ `).run(wsId, relPath, size, String(mtimeNs), sha, new Date().toISOString());
90
+ }
91
+
92
+ function removeDoc(wsId: string, relPath: string): void {
93
+ const db = getSearchDb();
94
+ db.prepare(`DELETE FROM docs WHERE ws = ? AND path = ?`).run(wsId, relPath);
95
+ db.prepare(`DELETE FROM docs_meta WHERE ws = ? AND path = ?`).run(wsId, relPath);
96
+ }
97
+
98
+ // ── Text extraction ────────────────────────────────────────────────────────────
99
+
100
+ async function extractText(
101
+ absPath: string,
102
+ relPath: string,
103
+ ): Promise<{ name: string; frontmatter: string; body: string; sha: string; size: number; mtimeNs: bigint } | null> {
104
+ let st: Awaited<ReturnType<typeof stat>>;
105
+ try {
106
+ st = await stat(absPath);
107
+ } catch {
108
+ return null;
109
+ }
110
+
111
+ const size = st.size;
112
+ const mtimeNs = BigInt(Math.round(st.mtimeMs * 1_000_000));
113
+ const name = path.basename(relPath);
114
+
115
+ if (size > MAX_INDEX_BYTES) {
116
+ // Too large: index filename only, no body.
117
+ return { name, frontmatter: "", body: "", sha: "", size, mtimeNs };
118
+ }
119
+
120
+ let buf: Buffer;
121
+ try {
122
+ buf = await readFile(absPath);
123
+ } catch {
124
+ return null;
125
+ }
126
+
127
+ const sha = "sha256:" + createHash("sha256").update(buf).digest("hex");
128
+
129
+ if (looksLikeBinary(buf)) {
130
+ return { name, frontmatter: "", body: "", sha, size, mtimeNs };
131
+ }
132
+
133
+ const text = buf.toString("utf-8");
134
+
135
+ if (isMarkdownExt(name)) {
136
+ const parsed = parseFrontmatter(text);
137
+ const fmStr = Object.entries(parsed.data)
138
+ .map(([k, v]) => `${k} ${Array.isArray(v) ? (v as unknown[]).join(" ") : String(v)}`)
139
+ .join(" ");
140
+ return { name, frontmatter: fmStr, body: parsed.body, sha, size, mtimeNs };
141
+ }
142
+
143
+ return { name, frontmatter: "", body: text, sha, size, mtimeNs };
144
+ }
145
+
146
+ // ── Index one file ─────────────────────────────────────────────────────────────
147
+
148
+ async function indexOnePath(wsId: string, rootDir: string, relPath: string): Promise<void> {
149
+ if (isDeniedRelPath(relPath)) return;
150
+
151
+ // App-dir contents are opaque leaves; never index files inside an app folder.
152
+ if (await isUnderApp(rootDir, relPath)) {
153
+ removeDoc(wsId, relPath);
154
+ return;
155
+ }
156
+
157
+ // Symlink-escape guard: resolve the realpath and confirm it stays under root.
158
+ // A symlink whose target escapes the workspace must never be indexed.
159
+ const absPath = await safeAbsPath(rootDir, relPath);
160
+ if (!absPath) {
161
+ removeDoc(wsId, relPath);
162
+ return;
163
+ }
164
+ const db = getSearchDb();
165
+
166
+ // Fast-path: check size+mtime against existing meta
167
+ let st: Awaited<ReturnType<typeof stat>>;
168
+ try {
169
+ st = await stat(absPath);
170
+ } catch {
171
+ // File gone - remove it
172
+ removeDoc(wsId, relPath);
173
+ return;
174
+ }
175
+
176
+ if (!st.isFile()) return;
177
+
178
+ const size = st.size;
179
+ const mtimeNs = BigInt(Math.round(st.mtimeMs * 1_000_000));
180
+
181
+ const meta = db.prepare(`SELECT size, mtime_ns, sha FROM docs_meta WHERE ws = ? AND path = ?`)
182
+ .get(wsId, relPath) as { size: number; mtime_ns: string; sha: string } | undefined;
183
+
184
+ if (meta && meta.size === size && BigInt(meta.mtime_ns) === mtimeNs) {
185
+ // Unchanged - skip
186
+ return;
187
+ }
188
+
189
+ const extracted = await extractText(absPath, relPath);
190
+ if (!extracted) return;
191
+
192
+ // If we got a sha and it matches the existing meta, just touch indexed_at
193
+ if (meta && extracted.sha && extracted.sha === meta.sha) {
194
+ db.prepare(`UPDATE docs_meta SET indexed_at = ? WHERE ws = ? AND path = ?`)
195
+ .run(new Date().toISOString(), wsId, relPath);
196
+ return;
197
+ }
198
+
199
+ const name = path.basename(relPath);
200
+ if (!isIndexableExt(name) && !isMarkdownExt(name)) {
201
+ // Non-text: index name only so filename searches work
202
+ upsertDoc(wsId, relPath, name, "", "", size, mtimeNs, extracted.sha);
203
+ return;
204
+ }
205
+
206
+ upsertDoc(wsId, relPath, extracted.name, extracted.frontmatter, extracted.body, extracted.size, extracted.mtimeNs, extracted.sha);
207
+ }
208
+
209
+ // ── File tree walker ───────────────────────────────────────────────────────────
210
+
211
+ async function* walkFiles(rootDir: string, relDir: string): AsyncGenerator<string> {
212
+ let items: Dirent[];
213
+ try {
214
+ items = await readdir(path.join(rootDir, relDir), { withFileTypes: true });
215
+ } catch {
216
+ return;
217
+ }
218
+ for (const item of items) {
219
+ if (SKIP_DIRS.has(item.name)) continue;
220
+ const childRel = relDir ? `${relDir}/${item.name}` : item.name;
221
+ if (item.isDirectory()) {
222
+ // App dirs (package.json or index.html) are opaque leaves in the UI:
223
+ // clicking runs the app, it is not a browsable folder. Do not index
224
+ // their contents so search does not surface bundled app internals.
225
+ if (await isNodeApp(rootDir, childRel) || await isAppFolder(rootDir, childRel)) {
226
+ continue;
227
+ }
228
+ yield* walkFiles(rootDir, childRel);
229
+ } else if (item.isFile() || item.isSymbolicLink()) {
230
+ yield childRel;
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * True if any ancestor directory of relPath is an app folder (node-app or
237
+ * static app). Used by the incremental path so file events inside an app dir
238
+ * are ignored, matching the initial-scan walker which does not recurse into
239
+ * app dirs.
240
+ */
241
+ async function isUnderApp(rootDir: string, relPath: string): Promise<boolean> {
242
+ const parts = relPath.split("/");
243
+ for (let i = 1; i < parts.length; i++) {
244
+ const ancestor = parts.slice(0, i).join("/");
245
+ if (await isNodeApp(rootDir, ancestor) || await isAppFolder(rootDir, ancestor)) {
246
+ return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+
252
+ // ── Initial scan ───────────────────────────────────────────────────────────────
253
+
254
+ async function initialScan(wsId: string, rootDir: string): Promise<void> {
255
+ const db = getSearchDb();
256
+ let batch: Array<() => void> = [];
257
+ let fileCount = 0;
258
+
259
+ async function flushBatch() {
260
+ if (batch.length === 0) return;
261
+ const ops = batch;
262
+ batch = [];
263
+ db.transaction(() => {
264
+ for (const op of ops) op();
265
+ })();
266
+ }
267
+
268
+ for await (const relPath of walkFiles(rootDir, "")) {
269
+ if (isDeniedRelPath(relPath)) continue;
270
+
271
+ // Schedule the actual index work
272
+ const relPathCopy = relPath;
273
+
274
+ // Symlink-escape guard: skip anything whose realpath leaves the workspace.
275
+ const absPath = await safeAbsPath(rootDir, relPathCopy);
276
+ if (!absPath) continue;
277
+
278
+ // Stat + read inline (can't easily capture async in sync txn)
279
+ // We collect the data async, then write in a batch txn
280
+ let st: Awaited<ReturnType<typeof stat>>;
281
+ try {
282
+ st = await stat(absPath);
283
+ } catch {
284
+ continue;
285
+ }
286
+ if (!st.isFile()) continue;
287
+
288
+ const size = st.size;
289
+ const mtimeNs = BigInt(Math.round(st.mtimeMs * 1_000_000));
290
+
291
+ const meta = db.prepare(`SELECT size, mtime_ns FROM docs_meta WHERE ws = ? AND path = ?`)
292
+ .get(wsId, relPathCopy) as { size: number; mtime_ns: string } | undefined;
293
+
294
+ if (meta && meta.size === size && BigInt(meta.mtime_ns) === mtimeNs) {
295
+ fileCount++;
296
+ if (fileCount % INITIAL_YIELD_EVERY === 0) {
297
+ await new Promise<void>((r) => setImmediate(r));
298
+ }
299
+ continue;
300
+ }
301
+
302
+ const extracted = await extractText(absPath, relPathCopy);
303
+ if (!extracted) continue;
304
+
305
+ const name = path.basename(relPathCopy);
306
+ const shouldIndexBody = isIndexableExt(name) || isMarkdownExt(name);
307
+ const fBody = shouldIndexBody ? extracted.body : "";
308
+ const fFm = shouldIndexBody ? extracted.frontmatter : "";
309
+
310
+ const capSize = extracted.size;
311
+ const capMtime = extracted.mtimeNs;
312
+ const capSha = extracted.sha;
313
+
314
+ batch.push(() => {
315
+ db.prepare(`DELETE FROM docs WHERE ws = ? AND path = ?`).run(wsId, relPathCopy);
316
+ db.prepare(`INSERT INTO docs (ws, path, name, frontmatter, body) VALUES (?, ?, ?, ?, ?)`)
317
+ .run(wsId, relPathCopy, extracted.name, fFm, fBody);
318
+ db.prepare(`
319
+ INSERT INTO docs_meta (ws, path, size, mtime_ns, sha, indexed_at)
320
+ VALUES (?, ?, ?, ?, ?, ?)
321
+ ON CONFLICT(ws, path) DO UPDATE SET
322
+ size = excluded.size,
323
+ mtime_ns = excluded.mtime_ns,
324
+ sha = excluded.sha,
325
+ indexed_at = excluded.indexed_at
326
+ `).run(wsId, relPathCopy, capSize, String(capMtime), capSha, new Date().toISOString());
327
+ });
328
+
329
+ fileCount++;
330
+
331
+ if (batch.length >= BATCH_SIZE) {
332
+ await flushBatch();
333
+ await new Promise<void>((r) => setImmediate(r));
334
+ } else if (fileCount % INITIAL_YIELD_EVERY === 0) {
335
+ await new Promise<void>((r) => setImmediate(r));
336
+ }
337
+ }
338
+
339
+ await flushBatch();
340
+ }
341
+
342
+ // ── Batch queue (incremental updates from chokidar) ────────────────────────────
343
+
344
+ function enqueueIndex(wsId: string, relPath: string): void {
345
+ const s = states.get(wsId);
346
+ if (!s) return;
347
+ s.pendingPaths.add(relPath);
348
+ if (s.pendingTimer) return; // already scheduled
349
+ s.pendingTimer = setTimeout(() => flushQueue(wsId), BATCH_TIMEOUT_MS);
350
+ }
351
+
352
+ function flushQueue(wsId: string): void {
353
+ const s = states.get(wsId);
354
+ if (!s || s.pendingPaths.size === 0) return;
355
+ s.pendingTimer = null;
356
+ const paths = Array.from(s.pendingPaths);
357
+ s.pendingPaths.clear();
358
+
359
+ // Index each in a background promise; errors are logged not thrown.
360
+ // Work is chunked at BATCH_SIZE with a setImmediate yield between chunks so a
361
+ // large burst of file changes can never block the event loop in one big
362
+ // synchronous transaction.
363
+ const db = getSearchDb();
364
+ const rootDir = s.rootDir;
365
+ void (async () => {
366
+ let ops: Array<() => void> = [];
367
+
368
+ const flushOps = async () => {
369
+ if (ops.length === 0) return;
370
+ const chunk = ops;
371
+ ops = [];
372
+ db.transaction(() => { for (const op of chunk) op(); })();
373
+ await new Promise<void>((r) => setImmediate(r));
374
+ };
375
+
376
+ for (const relPath of paths) {
377
+ // App-dir contents are opaque leaves; never index files inside one.
378
+ if (await isUnderApp(rootDir, relPath)) {
379
+ ops.push(() => removeDoc(wsId, relPath));
380
+ if (ops.length >= BATCH_SIZE) await flushOps();
381
+ continue;
382
+ }
383
+ // Symlink-escape guard before any read.
384
+ const absPath = await safeAbsPath(rootDir, relPath);
385
+ if (!absPath) {
386
+ ops.push(() => removeDoc(wsId, relPath));
387
+ if (ops.length >= BATCH_SIZE) await flushOps();
388
+ continue;
389
+ }
390
+ const extracted = await extractText(absPath, relPath).catch(() => null);
391
+ if (!extracted) {
392
+ ops.push(() => removeDoc(wsId, relPath));
393
+ if (ops.length >= BATCH_SIZE) await flushOps();
394
+ continue;
395
+ }
396
+ const name = path.basename(relPath);
397
+ const shouldIndexBody = isIndexableExt(name) || isMarkdownExt(name);
398
+ const capSize = extracted.size;
399
+ const capMtime = extracted.mtimeNs;
400
+ const capSha = extracted.sha;
401
+ const fBody = shouldIndexBody ? extracted.body : "";
402
+ const fFm = shouldIndexBody ? extracted.frontmatter : "";
403
+ ops.push(() => {
404
+ db.prepare(`DELETE FROM docs WHERE ws = ? AND path = ?`).run(wsId, relPath);
405
+ db.prepare(`INSERT INTO docs (ws, path, name, frontmatter, body) VALUES (?, ?, ?, ?, ?)`)
406
+ .run(wsId, relPath, extracted.name, fFm, fBody);
407
+ db.prepare(`
408
+ INSERT INTO docs_meta (ws, path, size, mtime_ns, sha, indexed_at)
409
+ VALUES (?, ?, ?, ?, ?, ?)
410
+ ON CONFLICT(ws, path) DO UPDATE SET
411
+ size = excluded.size,
412
+ mtime_ns = excluded.mtime_ns,
413
+ sha = excluded.sha,
414
+ indexed_at = excluded.indexed_at
415
+ `).run(wsId, relPath, capSize, String(capMtime), capSha, new Date().toISOString());
416
+ });
417
+ if (ops.length >= BATCH_SIZE) await flushOps();
418
+ }
419
+ await flushOps();
420
+ })().catch((e) => console.error("[search] flushQueue error", e));
421
+ }
422
+
423
+ // ── Public API ─────────────────────────────────────────────────────────────────
424
+
425
+ /**
426
+ * Ensure the indexer is running for this workspace.
427
+ * Idempotent: concurrent calls share one initial-scan promise.
428
+ * Fire-and-forget from request handlers (catch the promise externally).
429
+ */
430
+ export async function ensureIndexer(wsId: string, rootDir: string): Promise<void> {
431
+ const s = getState(wsId, rootDir);
432
+
433
+ if (s.initialScanDone) return;
434
+
435
+ if (!s.initialScanPromise) {
436
+ // Subscribe to chokidar (permanent listener -- watcher stays alive while indexer exists).
437
+ if (!s.unsubscribeWatcher) {
438
+ s.unsubscribeWatcher = subscribe(wsId, rootDir, (ev, relPath) => {
439
+ if (!relPath || isDeniedRelPath(relPath)) return;
440
+ if (ev === "add" || ev === "change") {
441
+ enqueueIndex(wsId, relPath);
442
+ } else if (ev === "unlink") {
443
+ void deleteFile(wsId, relPath);
444
+ }
445
+ });
446
+ }
447
+
448
+ // Kick off the scan in the background via setImmediate to not block the
449
+ // calling request. The promise is stored so subsequent ensureIndexer calls
450
+ // await the same scan instead of spawning a second one.
451
+ s.initialScanPromise = new Promise<void>((resolve) => {
452
+ setImmediate(() => {
453
+ void initialScan(wsId, rootDir).then(() => {
454
+ s.initialScanDone = true;
455
+ resolve();
456
+ }).catch((e) => {
457
+ console.error("[search] initial scan error", e);
458
+ s.initialScanPromise = null; // allow retry
459
+ resolve();
460
+ });
461
+ });
462
+ });
463
+ }
464
+
465
+ // Do NOT await: callers call fire-and-forget. Search works on whatever is already indexed.
466
+ // (Tests use _waitForIdle to synchronise.)
467
+ }
468
+
469
+ /** Index a single file (called on chokidar change/add events). */
470
+ export async function indexFile(wsId: string, rootDir: string, relPath: string): Promise<void> {
471
+ await indexOnePath(wsId, rootDir, relPath);
472
+ }
473
+
474
+ /** Remove a single file from the index. */
475
+ export async function deleteFile(wsId: string, relPath: string): Promise<void> {
476
+ removeDoc(wsId, relPath);
477
+ }
478
+
479
+ /** Remove all indexed data for a workspace (called on workspace delete). */
480
+ export async function purgeWorkspace(wsId: string): Promise<void> {
481
+ const db = getSearchDb();
482
+ db.transaction(() => {
483
+ db.prepare(`DELETE FROM docs WHERE ws = ?`).run(wsId);
484
+ db.prepare(`DELETE FROM docs_meta WHERE ws = ?`).run(wsId);
485
+ })();
486
+ // Clean up module state
487
+ const s = states.get(wsId);
488
+ if (s) {
489
+ if (s.pendingTimer) clearTimeout(s.pendingTimer);
490
+ s.unsubscribeWatcher?.();
491
+ states.delete(wsId);
492
+ }
493
+ }
494
+
495
+ export interface IndexedMatch {
496
+ path: string;
497
+ score: number; // negated BM25 rank (higher = better)
498
+ snippet: string;
499
+ }
500
+
501
+ export interface SearchResult {
502
+ matches: IndexedMatch[];
503
+ truncated: boolean;
504
+ }
505
+
506
+ /**
507
+ * Full-text search within a single workspace.
508
+ *
509
+ * CRITICAL: ws isolation is enforced by `WHERE ws = ?` on every query.
510
+ * This is the ONLY place in the codebase that executes SELECT ... FROM docs
511
+ * returning rows to callers. The ws column is always bound as the first param.
512
+ */
513
+ export function ftsSearch(wsId: string, query: string, limit: number): SearchResult {
514
+ const sanitized = sanitizeFtsQuery(query);
515
+ if (!sanitized) return { matches: [], truncated: false };
516
+
517
+ const hardLimit = Math.min(Math.max(1, limit), 200);
518
+ const db = getSearchDb();
519
+
520
+ let rows: Array<{ path: string; score: number; snippet: string }>;
521
+ try {
522
+ rows = db.prepare(`
523
+ SELECT path,
524
+ rank AS score,
525
+ snippet(docs, 4, '<mark>', '</mark>', '\u2026', 12) AS snippet
526
+ FROM docs
527
+ WHERE ws = ? AND docs MATCH ?
528
+ ORDER BY rank
529
+ LIMIT ?
530
+ `).all(wsId, sanitized, hardLimit + 1) as typeof rows;
531
+ } catch (e) {
532
+ console.error("[search] ftsSearch error", e);
533
+ return { matches: [], truncated: false };
534
+ }
535
+
536
+ const truncated = rows.length > hardLimit;
537
+ return {
538
+ matches: rows.slice(0, hardLimit).map((r) => ({
539
+ path: r.path,
540
+ score: -r.score,
541
+ snippet: r.snippet ?? "",
542
+ })),
543
+ truncated,
544
+ };
545
+ }
546
+
547
+ // ── Test hooks ─────────────────────────────────────────────────────────────────
548
+
549
+ /** Reset all module-level state (for tests). */
550
+ export function _resetIndexer(): void {
551
+ for (const s of states.values()) {
552
+ if (s.pendingTimer) clearTimeout(s.pendingTimer);
553
+ s.unsubscribeWatcher?.();
554
+ }
555
+ states.clear();
556
+ }
557
+
558
+ /**
559
+ * Wait until the initial scan for wsId has completed (for tests).
560
+ * Resolves immediately if the scan is already done.
561
+ */
562
+ export async function _waitForIdle(wsId: string): Promise<void> {
563
+ const s = states.get(wsId);
564
+ if (!s) return;
565
+ if (s.initialScanDone) return;
566
+ if (s.initialScanPromise) await s.initialScanPromise;
567
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * FTS5 user-query sanitizer.
3
+ *
4
+ * Converts raw user input into a safe FTS5 MATCH expression.
5
+ * Strategy: whitelist alphanumerics + hyphen + underscore, double-quote
6
+ * every token, optionally preserve trailing * for prefix search.
7
+ * This neutralizes ALL FTS5 operators (AND, OR, NOT, NEAR, (, ), ^)
8
+ * because quoted tokens are treated as literal phrases, not operators.
9
+ */
10
+
11
+ /** Match Unicode letters, numbers, hyphen, underscore, optionally followed by *. */
12
+ const TOKEN_RE = /[\p{L}\p{N}_-]+\*?/gu;
13
+
14
+ /** Max tokens accepted per query (prevents degenerate query bombs). */
15
+ const MAX_TOKENS = 16;
16
+
17
+ /**
18
+ * Returns a sanitized FTS5 query string, or "" if the input is empty
19
+ * or contains no valid tokens. An empty return means the caller should
20
+ * short-circuit and return no results without hitting FTS5.
21
+ */
22
+ export function sanitizeFtsQuery(raw: string): string {
23
+ const tokens = (raw.match(TOKEN_RE) ?? []).slice(0, MAX_TOKENS);
24
+ if (tokens.length === 0) return "";
25
+ return tokens
26
+ .map((t) => {
27
+ const star = t.endsWith("*");
28
+ const core = star ? t.slice(0, -1) : t;
29
+ if (!core) return null; // trailing * with empty body
30
+ const escaped = core.replace(/"/g, '""');
31
+ return `"${escaped}"` + (star ? "*" : "");
32
+ })
33
+ .filter(Boolean)
34
+ .join(" ");
35
+ }