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
@@ -0,0 +1,339 @@
1
+ /**
2
+ * FTS5 full-text search indexer tests.
3
+ *
4
+ * Drives the indexer directly (ensureIndexer + initial scan, plus indexFile /
5
+ * deleteFile to simulate chokidar events) so assertions are deterministic and
6
+ * do not depend on filesystem-watcher timing.
7
+ */
8
+ import { test, before, after, afterEach } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtemp, rm, writeFile, mkdir, symlink } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import path from "node:path";
13
+
14
+ let tmpHome: string;
15
+ let rootA: string;
16
+ let rootB: string;
17
+
18
+ before(async () => {
19
+ tmpHome = await mkdtemp(path.join(tmpdir(), "search-test-home-"));
20
+ rootA = await mkdtemp(path.join(tmpdir(), "search-test-rootA-"));
21
+ rootB = await mkdtemp(path.join(tmpdir(), "search-test-rootB-"));
22
+ process.env.HOME = tmpHome;
23
+ });
24
+
25
+ after(async () => {
26
+ await rm(tmpHome, { recursive: true, force: true });
27
+ await rm(rootA, { recursive: true, force: true });
28
+ await rm(rootB, { recursive: true, force: true });
29
+ });
30
+
31
+ import {
32
+ ensureIndexer,
33
+ ftsSearch,
34
+ indexFile,
35
+ deleteFile,
36
+ purgeWorkspace,
37
+ _resetIndexer,
38
+ _waitForIdle,
39
+ } from "../../lib/search/indexer.js";
40
+ import { _resetSearchDb, getSearchDb } from "../../lib/search/search-db.js";
41
+ import { _resetWatcherPool } from "../../lib/search/watcher-pool.js";
42
+ import { sanitizeFtsQuery } from "../../lib/search/sanitize.js";
43
+ import { readFileSync } from "node:fs";
44
+
45
+ afterEach(() => {
46
+ _resetIndexer();
47
+ _resetWatcherPool();
48
+ // Clear all rows so each test starts clean (cheaper than rebuilding the DB).
49
+ try {
50
+ const db = getSearchDb();
51
+ db.exec("DELETE FROM docs; DELETE FROM docs_meta;");
52
+ } catch {
53
+ /* db may not exist yet */
54
+ }
55
+ });
56
+
57
+ async function scan(wsId: string, root: string): Promise<void> {
58
+ await ensureIndexer(wsId, root);
59
+ await _waitForIdle(wsId);
60
+ }
61
+
62
+ test("indexes a markdown file and finds it", async () => {
63
+ await writeFile(path.join(rootA, "notes.md"), "the quick brown fox");
64
+ await scan("wsA", rootA);
65
+ const res = ftsSearch("wsA", "fox", 10);
66
+ assert.equal(res.matches.length, 1);
67
+ assert.equal(res.matches[0]!.path, "notes.md");
68
+ assert.match(res.matches[0]!.snippet, /<mark>fox<\/mark>/);
69
+ });
70
+
71
+ test("BM25 ordering: denser document ranks first", async () => {
72
+ await writeFile(path.join(rootA, "sparse.md"), "alpha one two three four");
73
+ await writeFile(
74
+ path.join(rootA, "dense.md"),
75
+ "alpha alpha alpha alpha alpha",
76
+ );
77
+ await scan("wsA", rootA);
78
+ const res = ftsSearch("wsA", "alpha", 10);
79
+ assert.equal(res.matches.length, 2);
80
+ assert.equal(res.matches[0]!.path, "dense.md");
81
+ });
82
+
83
+ test("snippet contains <mark> highlight", async () => {
84
+ const words = Array.from({ length: 200 }, (_, i) =>
85
+ i === 100 ? "needle" : `word${i}`,
86
+ ).join(" ");
87
+ await writeFile(path.join(rootA, "long.md"), words);
88
+ await scan("wsA", rootA);
89
+ const res = ftsSearch("wsA", "needle", 10);
90
+ assert.equal(res.matches.length, 1);
91
+ assert.match(res.matches[0]!.snippet, /<mark>needle<\/mark>/);
92
+ });
93
+
94
+ test("workspace isolation: a doc in A never appears in a B query", async () => {
95
+ await writeFile(path.join(rootA, "secret.md"), "topsecretalpha");
96
+ await writeFile(path.join(rootB, "secret.md"), "topsecretbeta");
97
+ await scan("wsA", rootA);
98
+ await scan("wsB", rootB);
99
+
100
+ assert.equal(ftsSearch("wsA", "topsecretbeta", 10).matches.length, 0);
101
+ assert.equal(ftsSearch("wsB", "topsecretalpha", 10).matches.length, 0);
102
+ assert.equal(ftsSearch("wsA", "topsecretalpha", 10).matches.length, 1);
103
+ assert.equal(ftsSearch("wsB", "topsecretbeta", 10).matches.length, 1);
104
+ });
105
+
106
+ test("incremental update on file change", async () => {
107
+ const p = path.join(rootA, "x.md");
108
+ await writeFile(p, "foofoo");
109
+ await scan("wsA", rootA);
110
+ assert.equal(ftsSearch("wsA", "foofoo", 10).matches.length, 1);
111
+
112
+ await writeFile(p, "barbar");
113
+ await indexFile("wsA", rootA, "x.md");
114
+ assert.equal(ftsSearch("wsA", "foofoo", 10).matches.length, 0);
115
+ assert.equal(ftsSearch("wsA", "barbar", 10).matches.length, 1);
116
+ });
117
+
118
+ test("deleted file no longer matches", async () => {
119
+ await writeFile(path.join(rootA, "gone.md"), "deletetoken");
120
+ await scan("wsA", rootA);
121
+ assert.equal(ftsSearch("wsA", "deletetoken", 10).matches.length, 1);
122
+
123
+ await deleteFile("wsA", "gone.md");
124
+ assert.equal(ftsSearch("wsA", "deletetoken", 10).matches.length, 0);
125
+
126
+ const db = getSearchDb();
127
+ const row = db
128
+ .prepare("SELECT COUNT(*) AS n FROM docs_meta WHERE ws = ? AND path = ?")
129
+ .get("wsA", "gone.md") as { n: number };
130
+ assert.equal(row.n, 0);
131
+ });
132
+
133
+ test("frontmatter is searchable", async () => {
134
+ await writeFile(
135
+ path.join(rootA, "fm.md"),
136
+ "---\ntitle: ImportantDoc\ntags: [alphatag, betatag]\n---\nbody text",
137
+ );
138
+ await scan("wsA", rootA);
139
+ assert.equal(ftsSearch("wsA", "ImportantDoc", 10).matches.length, 1);
140
+ assert.equal(ftsSearch("wsA", "alphatag", 10).matches.length, 1);
141
+ });
142
+
143
+ test("non-text file: filename indexed, binary body skipped", async () => {
144
+ // PNG magic header + random-ish bytes (contains a null byte -> binary).
145
+ const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01, 0x02, 0x03]);
146
+ await writeFile(path.join(rootA, "diagram.png"), buf);
147
+ await scan("wsA", rootA);
148
+ assert.equal(ftsSearch("wsA", "diagram", 10).matches.length, 1);
149
+ });
150
+
151
+ test("oversize file body skipped, filename still indexed", async () => {
152
+ const big = "needlebig ".repeat(150_000); // > 1 MiB
153
+ await writeFile(path.join(rootA, "huge.txt"), big);
154
+ await scan("wsA", rootA);
155
+ assert.equal(ftsSearch("wsA", "needlebig", 10).matches.length, 0);
156
+ assert.equal(ftsSearch("wsA", "huge", 10).matches.length, 1);
157
+ });
158
+
159
+ test("denied paths (.proof, .git) skipped", async () => {
160
+ await mkdir(path.join(rootA, ".proof"), { recursive: true });
161
+ await mkdir(path.join(rootA, ".git"), { recursive: true });
162
+ await writeFile(path.join(rootA, ".proof", "foo.md"), "prooftoken");
163
+ await writeFile(path.join(rootA, ".git", "HEAD"), "githeadtoken");
164
+ await writeFile(path.join(rootA, "real.md"), "realtoken");
165
+ await scan("wsA", rootA);
166
+ assert.equal(ftsSearch("wsA", "prooftoken", 10).matches.length, 0);
167
+ assert.equal(ftsSearch("wsA", "githeadtoken", 10).matches.length, 0);
168
+ assert.equal(ftsSearch("wsA", "realtoken", 10).matches.length, 1);
169
+ });
170
+
171
+ test("app dir contents are not indexed (node-app and static app)", async () => {
172
+ // node-app: directory with package.json
173
+ await mkdir(path.join(rootA, "my-app"), { recursive: true });
174
+ await writeFile(path.join(rootA, "my-app", "package.json"), "{}");
175
+ await writeFile(path.join(rootA, "my-app", "index.js"), "appinnertoken");
176
+ // static app: directory with index.html
177
+ await mkdir(path.join(rootA, "site"), { recursive: true });
178
+ await writeFile(path.join(rootA, "site", "index.html"), "sitehtmltoken");
179
+ await writeFile(path.join(rootA, "site", "app.js"), "siteinnertoken");
180
+ // a normal note that must still be searchable
181
+ await writeFile(path.join(rootA, "note.md"), "normalnotetoken");
182
+ await scan("wsA", rootA);
183
+
184
+ assert.equal(ftsSearch("wsA", "appinnertoken", 10).matches.length, 0);
185
+ assert.equal(ftsSearch("wsA", "sitehtmltoken", 10).matches.length, 0);
186
+ assert.equal(ftsSearch("wsA", "siteinnertoken", 10).matches.length, 0);
187
+ assert.equal(ftsSearch("wsA", "normalnotetoken", 10).matches.length, 1);
188
+ });
189
+
190
+ test("symlink escaping the workspace is not indexed", async () => {
191
+ // A secret file outside the workspace, reachable only via an in-root symlink.
192
+ const outside = await mkdtemp(path.join(tmpdir(), "search-test-outside-"));
193
+ await writeFile(path.join(outside, "leak.md"), "escapedsecrettoken");
194
+ try {
195
+ await symlink(
196
+ path.join(outside, "leak.md"),
197
+ path.join(rootA, "link.md"),
198
+ );
199
+ } catch {
200
+ // Platform without symlink permission: skip.
201
+ await rm(outside, { recursive: true, force: true });
202
+ return;
203
+ }
204
+ await scan("wsA", rootA);
205
+ // The symlinked-out content must never be searchable.
206
+ assert.equal(ftsSearch("wsA", "escapedsecrettoken", 10).matches.length, 0);
207
+ await rm(outside, { recursive: true, force: true });
208
+ });
209
+
210
+ test("purgeWorkspace removes all rows for that ws", async () => {
211
+ await writeFile(path.join(rootA, "a.md"), "purgetoken one");
212
+ await writeFile(path.join(rootA, "b.md"), "purgetoken two");
213
+ await scan("wsA", rootA);
214
+ assert.equal(ftsSearch("wsA", "purgetoken", 10).matches.length, 2);
215
+
216
+ await purgeWorkspace("wsA");
217
+ assert.equal(ftsSearch("wsA", "purgetoken", 10).matches.length, 0);
218
+
219
+ const db = getSearchDb();
220
+ const row = db
221
+ .prepare("SELECT COUNT(*) AS n FROM docs_meta WHERE ws = ?")
222
+ .get("wsA") as { n: number };
223
+ assert.equal(row.n, 0);
224
+ });
225
+
226
+ test("empty / whitespace / punctuation-only query returns empty without throwing", () => {
227
+ assert.deepEqual(ftsSearch("wsA", "", 10), { matches: [], truncated: false });
228
+ assert.deepEqual(ftsSearch("wsA", " ", 10), {
229
+ matches: [],
230
+ truncated: false,
231
+ });
232
+ assert.deepEqual(ftsSearch("wsA", "***", 10), {
233
+ matches: [],
234
+ truncated: false,
235
+ });
236
+ });
237
+
238
+ test("sanitizer neutralizes FTS5 operators", () => {
239
+ assert.equal(sanitizeFtsQuery("alpha AND beta"), '"alpha" "AND" "beta"');
240
+ assert.equal(sanitizeFtsQuery("foo NEAR bar"), '"foo" "NEAR" "bar"');
241
+ // Quotes and semicolons are stripped to safe tokens.
242
+ assert.equal(sanitizeFtsQuery('"; DROP docs'), '"DROP" "docs"');
243
+ // Prefix star preserved.
244
+ assert.equal(sanitizeFtsQuery("devel*"), '"devel"*');
245
+ });
246
+
247
+ test("operator-keyword query still searches literally and does not throw", async () => {
248
+ await writeFile(path.join(rootA, "lit.md"), "this and that");
249
+ await scan("wsA", rootA);
250
+ // "and" is quoted by the sanitizer, so this matches the literal word.
251
+ const res = ftsSearch("wsA", "and", 10);
252
+ assert.equal(res.matches.length, 1);
253
+ });
254
+
255
+ test("porter stemmer collapses inflections", async () => {
256
+ await writeFile(path.join(rootA, "stem.md"), "running fast");
257
+ await scan("wsA", rootA);
258
+ assert.equal(ftsSearch("wsA", "run", 10).matches.length, 1);
259
+ });
260
+
261
+ test("diacritic folding: café matches cafe", async () => {
262
+ await writeFile(path.join(rootA, "dia.md"), "the cafe is open");
263
+ await scan("wsA", rootA);
264
+ assert.equal(ftsSearch("wsA", "café", 10).matches.length, 1);
265
+ });
266
+
267
+ test("prefix search with * finds longer words", async () => {
268
+ await writeFile(path.join(rootA, "pre.md"), "developer notes");
269
+ await scan("wsA", rootA);
270
+ assert.equal(ftsSearch("wsA", "devel*", 10).matches.length, 1);
271
+ });
272
+
273
+ test("concurrent ensureIndexer is deduped (single scan)", async () => {
274
+ await writeFile(path.join(rootA, "dedupe.md"), "dedupetoken");
275
+ const [a, b] = [
276
+ ensureIndexer("wsA", rootA),
277
+ ensureIndexer("wsA", rootA),
278
+ ];
279
+ await Promise.all([a, b]);
280
+ await _waitForIdle("wsA");
281
+ const db = getSearchDb();
282
+ const row = db
283
+ .prepare("SELECT COUNT(*) AS n FROM docs WHERE ws = ? AND path = ?")
284
+ .get("wsA", "dedupe.md") as { n: number };
285
+ assert.equal(row.n, 1); // not double-inserted
286
+ });
287
+
288
+ test("unchanged file (same size+mtime) is not re-indexed", async () => {
289
+ const p = path.join(rootA, "stable.md");
290
+ await writeFile(p, "stabletoken");
291
+ await scan("wsA", rootA);
292
+ const db = getSearchDb();
293
+ const first = db
294
+ .prepare("SELECT indexed_at FROM docs_meta WHERE ws = ? AND path = ?")
295
+ .get("wsA", "stable.md") as { indexed_at: string };
296
+
297
+ await new Promise((r) => setTimeout(r, 5));
298
+ await indexFile("wsA", rootA, "stable.md"); // no file change
299
+ const second = db
300
+ .prepare("SELECT indexed_at FROM docs_meta WHERE ws = ? AND path = ?")
301
+ .get("wsA", "stable.md") as { indexed_at: string };
302
+ assert.equal(first.indexed_at, second.indexed_at);
303
+ });
304
+
305
+ test("rename via delete+add keeps only the new path", async () => {
306
+ await writeFile(path.join(rootA, "old.md"), "renametoken");
307
+ await scan("wsA", rootA);
308
+ await deleteFile("wsA", "old.md");
309
+ await writeFile(path.join(rootA, "new.md"), "renametoken");
310
+ await indexFile("wsA", rootA, "new.md");
311
+ const res = ftsSearch("wsA", "renametoken", 10);
312
+ assert.equal(res.matches.length, 1);
313
+ assert.equal(res.matches[0]!.path, "new.md");
314
+ });
315
+
316
+ test("DEFENSIVE: every 'FROM docs' in indexer.ts is ws-scoped", () => {
317
+ const raw = readFileSync(
318
+ new URL("../../lib/search/indexer.ts", import.meta.url),
319
+ "utf-8",
320
+ );
321
+ // Strip block and line comments so prose mentioning "FROM docs" is ignored;
322
+ // only real SQL counts.
323
+ const src = raw
324
+ .replace(/\/\*[\s\S]*?\*\//g, "")
325
+ .replace(/\/\/.*$/gm, "");
326
+ // Find each SELECT/DELETE ... FROM docs and assert a ws filter follows.
327
+ const re = /FROM docs\b([\s\S]{0,120})/g;
328
+ let m: RegExpExecArray | null;
329
+ let count = 0;
330
+ while ((m = re.exec(src)) !== null) {
331
+ count++;
332
+ assert.match(
333
+ m[1]!,
334
+ /ws\s*=\s*\?/,
335
+ `A 'FROM docs' query is missing a ws filter:\n${m[0]}`,
336
+ );
337
+ }
338
+ assert.ok(count >= 1, "expected at least one 'FROM docs' query");
339
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Phase E — cross-workspace isolation (the "no bleed" contract).
3
+ *
4
+ * Two workspaces (different rootDirs) that contain the SAME relative path must
5
+ * not share in-memory state:
6
+ * - edit leases (lease.ts is keyed by ns=rootDir)
7
+ * - collab-state revision / active flag
8
+ * - sidecars (live under <rootDir>/.proof/, naturally isolated)
9
+ */
10
+ import { test, before, after } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import path from "node:path";
15
+
16
+ import {
17
+ setLease,
18
+ hasActiveLease,
19
+ clearLease,
20
+ leaseGeneration,
21
+ _resetLeaseStore,
22
+ } from "../../lib/proof/lease.js";
23
+ import { computeCollabState } from "../../lib/proof/collab-state.js";
24
+ import { writeSidecar, emptySidecar } from "../../lib/proof/sidecar.js";
25
+
26
+ let rootA: string;
27
+ let rootB: string;
28
+
29
+ before(async () => {
30
+ rootA = await mkdtemp(path.join(tmpdir(), "wsiso-A-"));
31
+ rootB = await mkdtemp(path.join(tmpdir(), "wsiso-B-"));
32
+ _resetLeaseStore();
33
+ });
34
+
35
+ after(async () => {
36
+ await rm(rootA, { recursive: true, force: true });
37
+ await rm(rootB, { recursive: true, force: true });
38
+ });
39
+
40
+ const REL = "notes.md";
41
+
42
+ test("lease on (rootA, notes.md) does NOT make (rootB, notes.md) active", () => {
43
+ setLease(rootA, REL, "user:alice");
44
+ assert.equal(hasActiveLease(rootA, REL), true);
45
+ assert.equal(hasActiveLease(rootB, REL), false, "workspace B must be unaffected");
46
+ clearLease(rootA, REL, "user:alice");
47
+ assert.equal(hasActiveLease(rootA, REL), false);
48
+ });
49
+
50
+ test("lease generation counters are independent per workspace", () => {
51
+ const genA0 = leaseGeneration(rootA, REL);
52
+ const genB0 = leaseGeneration(rootB, REL);
53
+ // Bump A twice (open/close), leave B untouched.
54
+ setLease(rootA, REL, "user:alice");
55
+ clearLease(rootA, REL, "user:alice");
56
+ assert.equal(leaseGeneration(rootA, REL), genA0 + 2);
57
+ assert.equal(leaseGeneration(rootB, REL), genB0, "B generation must not move");
58
+ });
59
+
60
+ test("computeCollabState: human lease in A is 'active' only in A", async () => {
61
+ // Same relpath, a sidecar with one comment in BOTH workspaces would be
62
+ // 'active'; here neither has artifacts, so state is driven purely by lease.
63
+ await mkdir(path.join(rootA, ".proof"), { recursive: true });
64
+ await mkdir(path.join(rootB, ".proof"), { recursive: true });
65
+
66
+ setLease(rootA, REL, "user:alice");
67
+ const a = await computeCollabState(rootA, REL);
68
+ const b = await computeCollabState(rootB, REL);
69
+ assert.equal(a.state, "active", "A has a live human lease");
70
+ assert.equal(b.state, "untracked", "B has no lease, no sidecar");
71
+ clearLease(rootA, REL, "user:alice");
72
+ });
73
+
74
+ test("sidecars are isolated by rootDir (same relpath, different content)", async () => {
75
+ const scA = emptySidecar(REL);
76
+ scA.revision = 7;
77
+ const scB = emptySidecar(REL);
78
+ scB.revision = 99;
79
+ await writeSidecar(rootA, REL, scA);
80
+ await writeSidecar(rootB, REL, scB);
81
+
82
+ const a = await computeCollabState(rootA, REL);
83
+ const b = await computeCollabState(rootB, REL);
84
+ // revision = sidecar.revision + leaseGeneration(ns); leases cleared above.
85
+ assert.equal(a.revision - leaseGeneration(rootA, REL), 7);
86
+ assert.equal(b.revision - leaseGeneration(rootB, REL), 99);
87
+ });
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Phase D1 — Workspace & Admin API tests.
3
+ *
4
+ * Covers:
5
+ * GET /workspaces empty → after create
6
+ * POST /workspaces admin-only
7
+ * PATCH /workspaces/[id] allowedUserIds admin-gating
8
+ * DELETE /workspaces/[id]
9
+ * GET /admins GET/POST/DELETE + last-admin guard
10
+ */
11
+ import { test, before, after } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdtemp, rm } from "node:fs/promises";
14
+ import { tmpdir } from "node:os";
15
+ import path from "node:path";
16
+
17
+ let tmpHome: string;
18
+ let tmpDir: string;
19
+
20
+ before(async () => {
21
+ tmpHome = await mkdtemp(path.join(tmpdir(), "ws-api-home-"));
22
+ tmpDir = await mkdtemp(path.join(tmpdir(), "ws-api-dir-"));
23
+ process.env.HOME = tmpHome;
24
+ });
25
+
26
+ after(async () => {
27
+ await rm(tmpHome, { recursive: true, force: true });
28
+ await rm(tmpDir, { recursive: true, force: true });
29
+ });
30
+
31
+ import { GET as wsGET, POST as wsPOST } from "../../app/api/system/workspaces/route.js";
32
+ import { PATCH as wsPATCH, DELETE as wsDELETE } from "../../app/api/system/workspaces/[id]/route.js";
33
+ import { POST as openPOST } from "../../app/api/system/workspaces/[id]/open/route.js";
34
+ import { GET as adminsGET, POST as adminsPOST, DELETE as adminsDELETE } from "../../app/api/system/admins/route.js";
35
+
36
+ async function signUp(tag: string): Promise<{ cookie: string; id: string; email: string }> {
37
+ const { auth, authReady } = await import("../../lib/auth/server.js");
38
+ await authReady();
39
+ const email = `${tag}${Date.now()}@test.local`;
40
+ const res = await auth.api.signUpEmail({
41
+ body: { email, password: "test1234!", name: tag },
42
+ asResponse: true,
43
+ });
44
+ if (!res.ok) throw new Error("signUp failed: " + res.status);
45
+ const setCookie = res.headers.get("set-cookie") ?? "";
46
+ const cookie = setCookie
47
+ .split(/,(?=[^ ])/)
48
+ .map((c) => c.split(";")[0].trim())
49
+ .join("; ");
50
+ const body = await res.json() as { user: { id: string } };
51
+ return { cookie, id: body.user.id, email };
52
+ }
53
+
54
+ function req(method: string, url: string, cookie: string, body?: unknown): Request {
55
+ return new Request(url, {
56
+ method,
57
+ headers: {
58
+ "content-type": "application/json",
59
+ cookie,
60
+ },
61
+ body: body !== undefined ? JSON.stringify(body) : undefined,
62
+ });
63
+ }
64
+
65
+ function params(id: string) {
66
+ return { params: Promise.resolve({ id }) };
67
+ }
68
+
69
+ /**
70
+ * Deterministically make a freshly-signed-up user an admin.
71
+ * Bootstrap only auto-promotes the FIRST-EVER user, so subsequent tests must
72
+ * promote their admin explicitly (no shared cookie for the bootstrapped one).
73
+ */
74
+ async function makeAdmin(userId: string): Promise<void> {
75
+ const { addAdmin } = await import("../../lib/auth/admin.js");
76
+ await addAdmin(userId);
77
+ }
78
+
79
+ // ── Admin bootstrap + admins API ───────────────────────────────────────────────
80
+
81
+ test("GET /admins: first user auto-promoted to admin", async () => {
82
+ const user = await signUp("admin1");
83
+ const res = await adminsGET(req("GET", "http://localhost/api/system/admins", user.cookie));
84
+ assert.equal(res.status, 200);
85
+ const json = await res.json() as { isAdmin: boolean; admins: string[]; users: unknown[] };
86
+ assert.equal(json.isAdmin, true);
87
+ assert.ok(json.admins.includes(user.id));
88
+ assert.ok(Array.isArray(json.users)); // admin sees user list
89
+ });
90
+
91
+ test("GET /admins: non-admin gets isAdmin:false, empty users", async () => {
92
+ // Sign up a second user — first user already bootstrapped above
93
+ const user2 = await signUp("nonadmin");
94
+ const res = await adminsGET(req("GET", "http://localhost/api/system/admins", user2.cookie));
95
+ assert.equal(res.status, 200);
96
+ const json = await res.json() as { isAdmin: boolean; users: unknown[] };
97
+ assert.equal(json.isAdmin, false);
98
+ assert.deepEqual(json.users, []);
99
+ });
100
+
101
+ test("POST /admins: admin promotes second user", async () => {
102
+ const admin = await signUp("admin2promo");
103
+ await makeAdmin(admin.id);
104
+ const user2 = await signUp("promo-target");
105
+ const res = await adminsPOST(req("POST", "http://localhost/api/system/admins", admin.cookie, { userId: user2.id }));
106
+ assert.equal(res.status, 200);
107
+ const json = await res.json() as { ok: boolean; admins: string[] };
108
+ assert.ok(json.ok);
109
+ assert.ok(json.admins.includes(user2.id));
110
+ });
111
+
112
+ test("DELETE /admins: refuses to remove last admin (LAST_ADMIN guard)", async () => {
113
+ // Create a fresh admin user, then ensure they are the ONLY admin by using
114
+ // removeAdmin directly, then verify the API returns 409.
115
+ const { removeAdmin: rm2, addAdmin: add2 } = await import("../../lib/auth/admin.js");
116
+ const { updateConfig: uc2 } = await import("../../lib/config.js");
117
+
118
+ // Set adminUserIds to a single known userId so last-admin guard fires.
119
+ const soloId = "solo-only-" + Date.now();
120
+ await uc2((cfg) => ({ ...cfg, adminUserIds: [soloId] }));
121
+
122
+ // Build a fake signed-in session for soloId is impossible without a real DB user.
123
+ // Instead, call removeAdmin directly to verify the guard, and also verify the
124
+ // API route returns 409 by calling it as an admin user that matches soloId.
125
+ // Since we cannot create a session for soloId, we verify guard via library:
126
+ try {
127
+ await rm2(soloId);
128
+ assert.fail("Expected LAST_ADMIN error");
129
+ } catch (e) {
130
+ const msg = e instanceof Error ? e.message : "";
131
+ assert.ok(msg.startsWith("LAST_ADMIN"), `Expected LAST_ADMIN, got: ${msg}`);
132
+ }
133
+
134
+ // Restore admin list for subsequent tests
135
+ const admin = await signUp("restore-admin");
136
+ await adminsGET(req("GET", "http://localhost/api/system/admins", admin.cookie));
137
+ });
138
+
139
+ // ── Workspace API ──────────────────────────────────────────────────────────────
140
+
141
+ test("GET /workspaces: empty on fresh config", async () => {
142
+ const user = await signUp("ws-list-user");
143
+ const res = await wsGET(req("GET", "http://localhost/api/system/workspaces", user.cookie));
144
+ assert.equal(res.status, 200);
145
+ const json = await res.json() as { workspaces: unknown[] };
146
+ assert.ok(Array.isArray(json.workspaces));
147
+ });
148
+
149
+ test("POST /workspaces: admin creates workspace", async () => {
150
+ const admin = await signUp("ws-creator");
151
+ await makeAdmin(admin.id);
152
+ const newDir = await mkdtemp(path.join(tmpdir(), "ws-api-create-"));
153
+ try {
154
+ const res = await wsPOST(req("POST", "http://localhost/api/system/workspaces", admin.cookie, { rootDir: newDir }));
155
+ assert.equal(res.status, 200);
156
+ const json = await res.json() as { ok: boolean; workspace: { id: string; rootDir: string } };
157
+ assert.ok(json.ok);
158
+ assert.equal(json.workspace.rootDir, newDir);
159
+ assert.ok(json.workspace.id.startsWith("ws_"));
160
+
161
+ // Verify it shows in list
162
+ const listRes = await wsGET(req("GET", "http://localhost/api/system/workspaces", admin.cookie));
163
+ const listJson = await listRes.json() as { workspaces: Array<{ id: string }> };
164
+ assert.ok(listJson.workspaces.some((w) => w.id === json.workspace.id));
165
+
166
+ // PATCH name — any user with access
167
+ const patchRes = await wsPATCH(
168
+ req("PATCH", `http://localhost/api/system/workspaces/${json.workspace.id}`, admin.cookie, { name: "Renamed" }),
169
+ params(json.workspace.id),
170
+ );
171
+ assert.equal(patchRes.status, 200);
172
+ const patchJson = await patchRes.json() as { workspace: { name: string } };
173
+ assert.equal(patchJson.workspace.name, "Renamed");
174
+
175
+ // PATCH allowedUserIds: non-admin should fail
176
+ const nonAdmin = await signUp("non-admin-patch");
177
+ const patchForbidden = await wsPATCH(
178
+ req("PATCH", `http://localhost/api/system/workspaces/${json.workspace.id}`, nonAdmin.cookie, { allowedUserIds: [] }),
179
+ params(json.workspace.id),
180
+ );
181
+ assert.equal(patchForbidden.status, 403);
182
+
183
+ // PATCH allowedUserIds: admin succeeds
184
+ const patchAllowed = await wsPATCH(
185
+ req("PATCH", `http://localhost/api/system/workspaces/${json.workspace.id}`, admin.cookie, { allowedUserIds: [admin.id] }),
186
+ params(json.workspace.id),
187
+ );
188
+ assert.equal(patchAllowed.status, 200);
189
+
190
+ // DELETE workspace
191
+ const delRes = await wsDELETE(
192
+ req("DELETE", `http://localhost/api/system/workspaces/${json.workspace.id}`, admin.cookie),
193
+ params(json.workspace.id),
194
+ );
195
+ assert.equal(delRes.status, 200);
196
+ const delJson = await delRes.json() as { ok: boolean };
197
+ assert.ok(delJson.ok);
198
+ } finally {
199
+ await rm(newDir, { recursive: true, force: true });
200
+ }
201
+ });
202
+
203
+ test("POST /workspaces: non-existent dir returns 404", async () => {
204
+ const admin = await signUp("ws-baddir");
205
+ await makeAdmin(admin.id);
206
+ const res = await wsPOST(req("POST", "http://localhost/api/system/workspaces", admin.cookie, { rootDir: "/nonexistent/path/xyz" }));
207
+ assert.equal(res.status, 404);
208
+ });
209
+
210
+ test("POST /workspaces/[id]/open: updates lastOpenedAt", async () => {
211
+ const admin = await signUp("ws-open");
212
+ await makeAdmin(admin.id);
213
+ const newDir = await mkdtemp(path.join(tmpdir(), "ws-open-"));
214
+ try {
215
+ const createRes = await wsPOST(req("POST", "http://localhost/api/system/workspaces", admin.cookie, { rootDir: newDir }));
216
+ const { workspace } = await createRes.json() as { workspace: { id: string; lastOpenedAt: string } };
217
+
218
+ await new Promise((r) => setTimeout(r, 10)); // small delay for timestamp difference
219
+ const openRes = await openPOST(
220
+ req("POST", `http://localhost/api/system/workspaces/${workspace.id}/open`, admin.cookie),
221
+ params(workspace.id),
222
+ );
223
+ assert.equal(openRes.status, 200);
224
+ } finally {
225
+ await rm(newDir, { recursive: true, force: true });
226
+ }
227
+ });