wiki-viewer 1.7.2 → 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 (414) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +6 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/routes-manifest.json +40 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  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 +1 -1
  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 +2 -2
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  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 +3 -3
  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 +3 -3
  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/page.js.nft.json +1 -1
  154. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  155. package/.next/standalone/.next/server/app/signin/page.js +1 -1
  156. package/.next/standalone/.next/server/app/signin/page.js.nft.json +1 -1
  157. package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
  158. package/.next/standalone/.next/server/app-paths-manifest.json +6 -0
  159. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +1 -1
  160. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +1 -1
  161. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-244bv._.js +16 -0
  162. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0xz-_0v._.js → [root-of-the-server]__0-inabx._.js} +2 -2
  163. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0-ao6a_._.js → [root-of-the-server]__0.-9jt0._.js} +1 -1
  164. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0~-r-8f._.js → [root-of-the-server]__00c1-dr._.js} +1 -1
  165. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00lep-u._.js +3 -0
  166. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00mtevo._.js +3 -0
  167. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00qu86p._.js +3 -0
  168. package/.next/standalone/.next/server/chunks/[root-of-the-server]__012753j._.js +3 -0
  169. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01hp8ax._.js +3 -0
  170. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0jhr.ex._.js → [root-of-the-server]__02n6z97._.js} +1 -1
  171. package/.next/standalone/.next/server/chunks/[root-of-the-server]__036mhjp._.js +5 -0
  172. package/.next/standalone/.next/server/chunks/[root-of-the-server]__03p0ob1._.js +16 -0
  173. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05t~v6l._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/[root-of-the-server]__068ni_k._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__082nhm6._.js → [root-of-the-server]__07vphbd._.js} +1 -1
  176. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0pg6dn-._.js → [root-of-the-server]__08lkuwa._.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/[root-of-the-server]__09cqzem._.js +3 -0
  178. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0p_ox-k._.js → [root-of-the-server]__09k4q9e._.js} +1 -1
  179. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0_601._.js +3 -0
  180. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ae3dk6._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0s28.4-._.js → [root-of-the-server]__0af-x45._.js} +1 -1
  182. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cqu-wm._.js +3 -0
  183. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
  184. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d7x3e1._.js +3 -0
  185. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d7zxm_._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7vs98._.js +3 -0
  187. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0fxzg4v._.js +3 -0
  188. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gda_iu._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hu~q20._.js +3 -0
  190. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jk1uf4._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l-8clq._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lhka0x._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0kdn1le._.js → [root-of-the-server]__0na11h0._.js} +2 -2
  194. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nd.b_c._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0otb8ra._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p0-p__._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0piofm3._.js +3 -0
  198. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rrf-jr._.js +3 -0
  199. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s6_lzw._.js +3 -0
  200. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sh13v5._.js +3 -0
  201. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ta~flu._.js +3 -0
  202. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0v4im4p._.js +3 -0
  203. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0qn8e~s._.js → [root-of-the-server]__0v_kyp-._.js} +1 -1
  204. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vatqe-._.js +3 -0
  205. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vdb2nr._.js +3 -0
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wdh63w._.js +3 -0
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z3wg9x._.js +12 -0
  208. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0mzo03s._.js → [root-of-the-server]__0~.-3yk._.js} +1 -1
  209. package/.next/standalone/.next/server/chunks/[root-of-the-server]__123._fe._.js +3 -0
  210. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__12800b1._.js → [root-of-the-server]__12ee_s6._.js} +1 -1
  211. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12onytt._.js +3 -0
  212. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__04udrya._.js → [root-of-the-server]__12q8ch8._.js} +2 -2
  213. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1377.79._.js +3 -0
  214. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1378-ox._.js +3 -0
  215. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13aid91._.js +3 -0
  216. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13hp4tw._.js +12 -0
  217. package/.next/standalone/.next/server/chunks/_079xuln._.js +1 -1
  218. package/.next/standalone/.next/server/chunks/_0dr1r1g._.js +452 -0
  219. package/.next/standalone/.next/server/chunks/_101exke._.js +452 -0
  220. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_admins_route_actions_0dsm7cx.js +3 -0
  221. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_users_route_actions_0whj5cy.js +3 -0
  222. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_[id]_open_route_actions_1087xu7.js +3 -0
  223. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_[id]_route_actions_0~h7-8s.js +3 -0
  224. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_route_actions_0mkalc3.js +3 -0
  225. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_search_route_actions_0inxu93.js +3 -0
  226. package/.next/standalone/.next/server/chunks/node_modules__pnpm_0aj5g-w._.js +1 -1
  227. package/.next/standalone/.next/server/chunks/src_lib_search_0d-_j3u._.js +46 -0
  228. package/.next/standalone/.next/server/chunks/src_lib_search_0ymmp0c._.js +46 -0
  229. 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
  230. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_erDiagram-TEJ5UH35_mjs_0v6rnqy._.js +1 -1
  231. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_mermaid_core_mjs_0hj_n-~._.js +1 -1
  232. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0btcd4o._.js → [root-of-the-server]__02twzph._.js} +2 -2
  233. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ytvlzs._.js → [root-of-the-server]__09~x2v~._.js} +2 -2
  234. package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +18 -18
  235. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0c71i8a._.js → node_modules__pnpm_0.96tmo._.js} +1 -1
  236. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0lm8e2c._.js → node_modules__pnpm_011vy_q._.js} +2 -2
  237. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0ezm2us._.js → node_modules__pnpm_01o_5qt._.js} +1 -1
  238. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0j3mvgg._.js → node_modules__pnpm_04n0h6.._.js} +1 -1
  239. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0~ln0by._.js → node_modules__pnpm_093dp2q._.js} +1 -1
  240. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_11e~j49._.js → node_modules__pnpm_09ej62_._.js} +1 -1
  241. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0de8xt-._.js → node_modules__pnpm_09ljbdz._.js} +2 -2
  242. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-x6zex._.js → node_modules__pnpm_09~_ob2._.js} +1 -1
  243. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0yq5z.h._.js → node_modules__pnpm_0a01ng7._.js} +2 -2
  244. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0kwc-vs._.js → node_modules__pnpm_0b8qx0g._.js} +1 -1
  245. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hrbz84._.js → node_modules__pnpm_0bbc---._.js} +1 -1
  246. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hobcbs._.js → node_modules__pnpm_0c.309.._.js} +1 -1
  247. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0rw91-f._.js → node_modules__pnpm_0c8aunj._.js} +1 -1
  248. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0o.c1z8._.js → node_modules__pnpm_0hk6xys._.js} +2 -2
  249. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0dboa_e._.js → node_modules__pnpm_0hlzy8f._.js} +1 -1
  250. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0j53hti._.js → node_modules__pnpm_0nw5ou2._.js} +1 -1
  251. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xrmzh1._.js → node_modules__pnpm_0o.dn8o._.js} +1 -1
  252. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0cxwhfd._.js → node_modules__pnpm_0s0ur~d._.js} +1 -1
  253. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_07rxv2x._.js → node_modules__pnpm_0shlang._.js} +1 -1
  254. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0i.pw_m._.js → node_modules__pnpm_0sq3c2r._.js} +1 -1
  255. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0ajzar9._.js → node_modules__pnpm_0t9jb7d._.js} +1 -1
  256. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_012nt.s._.js → node_modules__pnpm_0wmx9kq._.js} +2 -2
  257. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0z~h2ow._.js → node_modules__pnpm_0xfx0sf._.js} +1 -1
  258. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0cbsnwf._.js → node_modules__pnpm_0yiz.-t._.js} +1 -1
  259. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0bum3-3._.js → node_modules__pnpm_11sut1u._.js} +1 -1
  260. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0mdo4i3._.js → node_modules__pnpm_124u4o2._.js} +2 -2
  261. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0il8j.j._.js → node_modules__pnpm_13tajgx._.js} +2 -2
  262. package/.next/standalone/.next/server/chunks/ssr/src_app_signin_00yk0vm._.js +2 -2
  263. 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
  264. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0.pmgt2._.js +3 -0
  265. package/.next/standalone/.next/server/functions-config-manifest.json +6 -0
  266. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  267. package/.next/standalone/.next/server/middleware-manifest.json +8 -8
  268. package/.next/standalone/.next/server/pages/404.html +1 -1
  269. package/.next/standalone/.next/server/pages/500.html +1 -1
  270. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  271. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  272. package/.next/standalone/.next/static/chunks/0c_~zevf2n0r4.css +2 -0
  273. package/.next/standalone/.next/static/chunks/0l4842ez-0_66.js +129 -0
  274. package/.next/standalone/README.md +46 -2
  275. package/.next/standalone/docs/workspacing-plan.md +364 -0
  276. package/.next/standalone/package.json +2 -2
  277. package/.next/standalone/src/app/api/agent/activity/route.ts +9 -8
  278. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +20 -8
  279. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +16 -9
  280. package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +28 -18
  281. package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +12 -7
  282. package/.next/standalone/src/app/api/agent/fs/move/route.ts +16 -11
  283. package/.next/standalone/src/app/api/agent/fs/search/route.ts +49 -13
  284. package/.next/standalone/src/app/api/agent/internal/span/route.ts +9 -5
  285. package/.next/standalone/src/app/api/agent/register/route.ts +9 -1
  286. package/.next/standalone/src/app/api/agent/settings/route.ts +7 -3
  287. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +8 -4
  288. package/.next/standalone/src/app/api/assets/[...path]/route.ts +9 -4
  289. package/.next/standalone/src/app/api/system/admins/route.ts +103 -0
  290. package/.next/standalone/src/app/api/system/reveal/route.ts +7 -6
  291. package/.next/standalone/src/app/api/system/set-root/route.ts +24 -1
  292. package/.next/standalone/src/app/api/system/users/route.ts +120 -0
  293. package/.next/standalone/src/app/api/system/workspaces/[id]/open/route.ts +34 -0
  294. package/.next/standalone/src/app/api/system/workspaces/[id]/route.ts +74 -0
  295. package/.next/standalone/src/app/api/system/workspaces/route.ts +67 -0
  296. package/.next/standalone/src/app/api/upload/[...path]/route.ts +10 -5
  297. package/.next/standalone/src/app/api/wiki/app/route.ts +10 -9
  298. package/.next/standalone/src/app/api/wiki/content/route.ts +14 -14
  299. package/.next/standalone/src/app/api/wiki/download/route.ts +7 -6
  300. package/.next/standalone/src/app/api/wiki/file/route.ts +7 -2
  301. package/.next/standalone/src/app/api/wiki/folder/route.ts +6 -5
  302. package/.next/standalone/src/app/api/wiki/move/route.ts +7 -7
  303. package/.next/standalone/src/app/api/wiki/new-file/route.ts +7 -6
  304. package/.next/standalone/src/app/api/wiki/page/route.ts +6 -5
  305. package/.next/standalone/src/app/api/wiki/presence/route.ts +8 -7
  306. package/.next/standalone/src/app/api/wiki/route.ts +13 -11
  307. package/.next/standalone/src/app/api/wiki/search/route.ts +44 -0
  308. package/.next/standalone/src/app/api/wiki/slugs/route.ts +8 -8
  309. package/.next/standalone/src/app/api/wiki/upload/route.ts +6 -5
  310. package/.next/standalone/src/app/api/wiki/watch/route.ts +16 -40
  311. package/.next/standalone/src/app/globals.css +484 -484
  312. package/.next/standalone/src/app/page.tsx +163 -49
  313. package/.next/standalone/src/components/auth-settings-sheet.tsx +171 -2
  314. package/.next/standalone/src/components/dir-picker.tsx +8 -6
  315. package/.next/standalone/src/components/editor/comment-thread.tsx +2 -1
  316. package/.next/standalone/src/components/editor/csv-viewer.tsx +3 -2
  317. package/.next/standalone/src/components/editor/editor-toolbar.tsx +2 -2
  318. package/.next/standalone/src/components/editor/editor.tsx +6 -5
  319. package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +3 -2
  320. package/.next/standalone/src/components/editor/folder-index.tsx +2 -1
  321. package/.next/standalone/src/components/editor/image-viewer.tsx +2 -1
  322. package/.next/standalone/src/components/editor/media-popover.tsx +2 -1
  323. package/.next/standalone/src/components/editor/media-viewer.tsx +2 -1
  324. package/.next/standalone/src/components/editor/mermaid-viewer.tsx +3 -2
  325. package/.next/standalone/src/components/editor/node-app-viewer.tsx +4 -3
  326. package/.next/standalone/src/components/editor/notebook-viewer.tsx +3 -2
  327. package/.next/standalone/src/components/editor/office/docx-viewer.tsx +2 -1
  328. package/.next/standalone/src/components/editor/office/office-chrome.tsx +3 -2
  329. package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +2 -1
  330. package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +2 -1
  331. package/.next/standalone/src/components/editor/pdf-viewer.tsx +2 -1
  332. package/.next/standalone/src/components/editor/proof-span-popover.tsx +2 -1
  333. package/.next/standalone/src/components/editor/source-viewer.tsx +3 -2
  334. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +2 -1
  335. package/.next/standalone/src/components/editor/suggestion-card.tsx +2 -1
  336. package/.next/standalone/src/components/editor/website-viewer.tsx +2 -1
  337. package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +2 -1
  338. package/.next/standalone/src/components/search/search-command-dialog.tsx +109 -0
  339. package/.next/standalone/src/components/search/sidebar-search-box.tsx +121 -0
  340. package/.next/standalone/src/components/search/snippet-text.tsx +48 -0
  341. package/.next/standalone/src/components/ui/command.tsx +3 -2
  342. package/.next/standalone/src/lib/auth/admin.ts +112 -0
  343. package/.next/standalone/src/lib/auth/server.ts +13 -2
  344. package/.next/standalone/src/lib/config.ts +56 -4
  345. package/.next/standalone/src/lib/proof/audit.ts +19 -10
  346. package/.next/standalone/src/lib/proof/auth.ts +11 -0
  347. package/.next/standalone/src/lib/proof/collab-state.ts +4 -2
  348. package/.next/standalone/src/lib/proof/file-lock.ts +5 -0
  349. package/.next/standalone/src/lib/proof/idempotency.ts +5 -0
  350. package/.next/standalone/src/lib/proof/lease.ts +40 -27
  351. package/.next/standalone/src/lib/proof/mutex.ts +4 -0
  352. package/.next/standalone/src/lib/proof/raw-fs.ts +2 -3
  353. package/.next/standalone/src/lib/proof/registry.ts +5 -0
  354. package/.next/standalone/src/lib/search/indexable-exts.ts +25 -0
  355. package/.next/standalone/src/lib/search/indexer.ts +567 -0
  356. package/.next/standalone/src/lib/search/sanitize.ts +35 -0
  357. package/.next/standalone/src/lib/search/search-db.ts +57 -0
  358. package/.next/standalone/src/lib/search/watcher-pool.ts +79 -0
  359. package/.next/standalone/src/lib/workspace-client.ts +82 -0
  360. package/.next/standalone/src/lib/workspace-context.ts +142 -0
  361. package/.next/standalone/src/lib/workspaces.ts +196 -0
  362. package/.next/standalone/src/stores/ai-panel-store.ts +3 -2
  363. package/.next/standalone/src/stores/editor-store.ts +4 -3
  364. package/.next/standalone/src/stores/proof-store.ts +4 -3
  365. package/.next/standalone/src/stores/search-store.ts +86 -0
  366. package/.next/standalone/src/stores/wiki-slugs-store.ts +2 -1
  367. package/.next/standalone/src/tests/proof/admin-create-user.test.ts +43 -0
  368. package/.next/standalone/src/tests/proof/agent-workspace-scope.test.ts +134 -0
  369. package/.next/standalone/src/tests/proof/collab-state.test.ts +43 -41
  370. package/.next/standalone/src/tests/proof/preload.ts +21 -0
  371. package/.next/standalone/src/tests/proof/search-index.test.ts +339 -0
  372. package/.next/standalone/src/tests/proof/workspace-isolation.test.ts +87 -0
  373. package/.next/standalone/src/tests/proof/workspaces-api.test.ts +227 -0
  374. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  375. package/README.md +46 -2
  376. package/package.json +2 -2
  377. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0o~h4zq.js +0 -5
  378. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.hb9q2._.js +0 -3
  379. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.uux34._.js +0 -3
  380. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0092hkp._.js +0 -3
  381. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +0 -3
  382. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05idwm9._.js +0 -3
  383. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +0 -3
  384. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +0 -3
  385. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07fpqks._.js +0 -3
  386. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094q5bj._.js +0 -3
  387. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0awzyon._.js +0 -3
  388. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +0 -3
  389. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +0 -3
  390. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +0 -3
  391. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0km8suj._.js +0 -3
  392. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +0 -15
  393. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0mkwoul._.js +0 -12
  394. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +0 -15
  395. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qyd-o8._.js +0 -3
  396. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rkve~v._.js +0 -3
  397. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rzo40z._.js +0 -3
  398. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sjocwy._.js +0 -3
  399. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ssj9~d._.js +0 -3
  400. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +0 -3
  401. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t220d-._.js +0 -3
  402. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t5mg.n._.js +0 -3
  403. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0trsqf4._.js +0 -3
  404. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +0 -3
  405. package/.next/standalone/.next/server/chunks/[root-of-the-server]__106uw_c._.js +0 -3
  406. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +0 -3
  407. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13w-0iy._.js +0 -3
  408. package/.next/standalone/.next/server/chunks/src_lib_auth_server_ts_00f4e0h._.js +0 -452
  409. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0ug2rvl._.js +0 -3
  410. package/.next/standalone/.next/static/chunks/0vwkqw_rofm-c.js +0 -129
  411. package/.next/standalone/.next/static/chunks/0zay6349~v_wc.css +0 -2
  412. /package/.next/standalone/.next/static/{0XvNCOSEq7cbARTLtAolL → t7B6nXaoD1jo9f61t3aeK}/_buildManifest.js +0 -0
  413. /package/.next/standalone/.next/static/{0XvNCOSEq7cbARTLtAolL → t7B6nXaoD1jo9f61t3aeK}/_clientMiddlewareManifest.js +0 -0
  414. /package/.next/standalone/.next/static/{0XvNCOSEq7cbARTLtAolL → t7B6nXaoD1jo9f61t3aeK}/_ssgManifest.js +0 -0
@@ -67,7 +67,7 @@ The wizard walks you through every option and writes `~/.wiki-viewer/config.json
67
67
 
68
68
  Open **http://localhost:3000** (or **https://localhost:3000** with `--https`).
69
69
 
70
- To switch the served directory later, click **Change** at the bottom of the sidebar and pick a new folder. No restart needed.
70
+ To switch between directories later, use the **workspace switcher** at the bottom of the sidebar. Each _workspace_ is a registered root directory; you can run several at once with no state bleed (open different workspaces in different browser tabs). See [Workspaces](#workspaces) below. No restart needed.
71
71
 
72
72
  On first run with no users in the database, the app works in single-user mode and any visitor on `localhost` can sign up. Set `AUTH_ALLOWED_DOMAIN` or `AUTH_ALLOWED_EMAILS` before exposing the server to anyone else.
73
73
 
@@ -123,7 +123,7 @@ wiki-viewer service restart
123
123
  wiki-viewer service uninstall
124
124
  ```
125
125
 
126
- The run config is saved to `~/.wiki-viewer/config.json`. Edit that file and run `wiki-viewer service restart` to change settings without reinstalling. To change just the served directory at runtime, you can also click **Change** in the sidebar.
126
+ The run config is saved to `~/.wiki-viewer/config.json`. Edit that file and run `wiki-viewer service restart` to change settings without reinstalling. To change the served directory at runtime, use the workspace switcher in the sidebar (see [Workspaces](#workspaces)).
127
127
 
128
128
  On Linux, install enables lingering (`loginctl enable-linger`) so the service runs without an active login session and survives reboot. If that step needs privileges, the installer prints the command to run manually.
129
129
 
@@ -174,6 +174,49 @@ Updates the global install to the latest version (npm/pnpm/yarn auto-detected) a
174
174
 
175
175
  ---
176
176
 
177
+ ## Workspaces
178
+
179
+ A **workspace** is a registered root directory. One running wiki-viewer can
180
+ serve many workspaces at once, fully isolated from each other:
181
+
182
+ - Two browser tabs can each open a different workspace (via a `?ws=<id>` URL
183
+ param) and edit files concurrently. Edit leases, write locks, idempotency
184
+ keys, collab sidecars, and the audit log are all namespaced per workspace —
185
+ a `notes.md` in workspace A never collides with a `notes.md` in workspace B.
186
+ - AI agents target a workspace with an `X-Workspace: <id>` header (or `?ws=`).
187
+ An agent's grant may be pinned to one workspace; requests that resolve to a
188
+ different workspace are rejected `403 FORBIDDEN`. Agents registered without a
189
+ workspace id keep working across all workspaces (wildcard).
190
+
191
+ ### Switching and creating
192
+
193
+ The sidebar footer shows the active workspace. Click it to switch between the
194
+ workspaces you can access, or (admins only) **Add workspace…** to register a
195
+ new root directory. Switching navigates to `?ws=<id>` and resets the view.
196
+
197
+ The legacy single-directory flow is unchanged: launching with `ROOT_DIR` or
198
+ `wiki-viewer <dir>` auto-registers that directory as the first workspace, and a
199
+ single-workspace install behaves exactly as before (no `?ws=` needed).
200
+
201
+ ### Admins and access control
202
+
203
+ Workspace management is **admin-gated** (organizational access control, not
204
+ OS-level permissions):
205
+
206
+ - The **first user to sign up** becomes an admin automatically. Admins can
207
+ promote/demote other users from the settings sheet (gear icon → **Admins**).
208
+ Seed or override admins headlessly with `WIKI_ADMIN_EMAILS` (csv).
209
+ - Only admins can create/delete workspaces and edit a workspace's allowed-user
210
+ list (`allowedUserIds`). An empty allow-list means any signed-in user may
211
+ open the workspace (the default). Non-admins with access get full file
212
+ read/write on the workspaces they can see.
213
+ - Admin state lives in `~/.wiki-viewer/config.json` (`adminUserIds`), workspace
214
+ records under `workspaces[]`.
215
+
216
+ > Note: removing a workspace only unregisters it — the directory on disk is
217
+ > never touched. The last admin cannot be demoted unless `WIKI_ADMIN_EMAILS`
218
+ > provides a fallback.
219
+
177
220
  ## Auth and multi-user mode
178
221
 
179
222
  wiki-viewer uses [Better Auth](https://better-auth.com) with SQLite. State lives at `~/.wiki-viewer/auth.db` (WAL mode) and `~/.wiki-viewer/auth.secret` (chmod 0600). Both are created on first start. Set `BETTER_AUTH_SECRET` in the environment to override the file-stored secret.
@@ -528,6 +571,7 @@ The dev server supports hot reload.
528
571
  | `AUTH_ALLOWED_DOMAIN` | csv: only emails on these domains can sign up (overridden by UI allowlist if set) | unset (open) |
529
572
  | `WIKI_OWNER_HOSTS` | csv: extra hostnames trusted for CSRF Origin check | `localhost,127.0.0.1` |
530
573
  | `WIKI_ALLOW_INSECURE` | Set to `1` to bypass the prod-https guard (dev / CI only) | unset |
574
+ | `WIKI_ADMIN_EMAILS` | csv: emails treated as admins (seed/override; otherwise first signup is admin) | unset |
531
575
  | `AGENT_RATE_LIMIT` | Max mutation ops per minute per agent identity | `60` |
532
576
 
533
577
  ---
@@ -0,0 +1,364 @@
1
+ # Workspacing plan
2
+
3
+ > **Status: IMPLEMENTED.** Phases A–E complete. 294 tests pass, tsc clean.
4
+ > Key files: `src/lib/workspaces.ts`, `src/lib/workspace-context.ts`,
5
+ > `src/lib/auth/admin.ts`, `src/lib/workspace-client.ts`, workspace + admin
6
+ > APIs under `src/app/api/system/`, agent scope via `scope.workspaceId`.
7
+ > The legacy `src/lib/root-dir.ts` global is retained as a fallback source
8
+ > (synthetic default workspace) for `ROOT_DIR`/CLI/tests — not yet deleted.
9
+
10
+ ## Goal
11
+
12
+ Let one running wiki-viewer serve multiple root directories ("workspaces")
13
+ concurrently, with zero state bleed between them. A user can have two tabs open
14
+ on two different workspaces at once. AI agents target a specific workspace.
15
+
16
+ Decisions (locked):
17
+
18
+ - **Selection**: URL/query carries `ws=<wsId>` for browser routes; agents send
19
+ an `X-Workspace: <wsId>` header. No process-global "current root".
20
+ - **Agents are workspace-scoped**: every agent request resolves a workspace and
21
+ is denied if it has no grant for it.
22
+ - **Isolation type**: organizational, not OS-level ACL. We add an explicit
23
+ per-workspace access list (which users / which agents may use a workspace).
24
+ We do NOT add per-user filesystem path restrictions on the directory browser.
25
+
26
+ ## Why the current code bleeds
27
+
28
+ `src/lib/root-dir.ts` keeps a single `globalThis.__wikiRootDir`. Every route
29
+ calls `getRootDir()` / `safeRootPath()` against that one value, so:
30
+
31
+ - "Change" (`/api/system/clear-root`) wipes the root for every user/tab.
32
+ - Only one root can be active per process. No concurrency at all.
33
+
34
+ In-memory stores keyed by **relPath only** (collide across roots once more than
35
+ one root is live):
36
+
37
+ - `src/lib/proof/lease.ts` — `leases` + `generations` maps keyed by relPath.
38
+ - `src/lib/proof/idempotency.ts` — single LRU keyed by idempotency key.
39
+ - `src/lib/proof/mutex.ts` — in-process lock map keyed by relPath / mdPath.
40
+ - `src/lib/proof/file-lock.ts` — cross-process sentinel hashed from lockKey
41
+ (relPath). Two workspaces with `notes.md` share one lock.
42
+
43
+ Config is single-root shaped:
44
+
45
+ - `src/lib/config.ts` — one `lastOpenedPath`, one flat `pinnedPaths`.
46
+
47
+ Already correctly scoped (these take `rootDir` as a param, keep as-is):
48
+
49
+ - `src/lib/proof/sidecar.ts`, `activity.ts` — read/write under
50
+ `<rootDir>/.proof/`. Naturally isolated once `rootDir` is per-workspace.
51
+
52
+ Global, needs a `workspaceId` column / field:
53
+
54
+ - `src/lib/proof/audit.ts` — `agent_fs_audit` table mixes paths from all roots.
55
+ - `src/lib/proof/registry.ts` — `agents.json`; agent scope is path globs with
56
+ no workspace dimension.
57
+
58
+ ## Target architecture
59
+
60
+ ### 0. Admin role (new prerequisite)
61
+
62
+ No admin concept exists today: every signed-in user is equal, and the settings
63
+ routes just `requireUser`. Workspace access management is admin-gated, so we
64
+ introduce a minimal global admin role.
65
+
66
+ New `src/lib/auth/admin.ts`:
67
+
68
+ ```ts
69
+ isAdmin(userId, email): Promise<boolean>
70
+ listAdmins(): Promise<string[]> // user ids
71
+ addAdmin(userId): Promise<void>
72
+ removeAdmin(userId): Promise<void> // refuse to remove the last admin
73
+ requireAdmin(req): Promise<{ ok: true; user } | { ok: false; status; code }>
74
+ ```
75
+
76
+ Resolution:
77
+
78
+ - `adminUserIds: string[]` stored in `config.json`.
79
+ - `WIKI_ADMIN_EMAILS` (csv) env: any matching signed-in user is treated as
80
+ admin regardless of config (seed/override, headless-friendly).
81
+ - **Bootstrap**: if `adminUserIds` is empty AND no `WIKI_ADMIN_EMAILS`, the
82
+ first user to sign up is written into `adminUserIds`. Hook the better-auth
83
+ signup path, or do it lazily on the first authenticated request when the
84
+ admin set is empty (behind the config write mutex). Log once.
85
+ - `requireAdmin` returns 403 `ADMIN_REQUIRED` for non-admins.
86
+
87
+ Admin management API: `GET/POST/DELETE /api/system/admins` (admin-gated; list
88
+ users, promote, demote). `DELETE` refuses to remove the last admin (lockout
89
+ guard). A small Admins section in the settings sheet lists users (from the
90
+ better-auth `user` table) with promote/demote toggles.
91
+
92
+ Non-admin users keep full file access to workspaces they are granted; admin is
93
+ only about creating workspaces and editing access lists (and promoting admins).
94
+
95
+ ### 1. Workspace registry (replaces single rootDir)
96
+
97
+ New `src/lib/workspaces.ts`. Persisted in `~/.wiki-viewer/config.json` under a
98
+ new `workspaces` array. Each entry:
99
+
100
+ ```ts
101
+ interface Workspace {
102
+ id: string; // "ws_" + short random; stable, URL-safe
103
+ name: string; // display label, defaults to basename(rootDir)
104
+ rootDir: string; // absolute, resolved
105
+ createdAt: string;
106
+ lastOpenedAt?: string;
107
+ pinnedPaths?: string[]; // moved here from the flat top-level list
108
+ createdBy?: string; // user id of the admin who created it
109
+ // access management (organizational, admin-edited only):
110
+ allowedUserIds?: string[]; // empty/undefined = any signed-in user
111
+ // agent grants live in the agent registry, keyed by workspaceId
112
+ }
113
+ ```
114
+
115
+ API surface (pure functions, no globalThis root):
116
+
117
+ ```ts
118
+ listWorkspaces(): Promise<Workspace[]>
119
+ getWorkspace(id): Promise<Workspace | null>
120
+ createWorkspace({ rootDir, name? }): Promise<Workspace>
121
+ renameWorkspace(id, name): Promise<void>
122
+ removeWorkspace(id): Promise<void> // does NOT touch the directory
123
+ setWorkspaceAccess(id, userIds): Promise<void> // admin-only, edits allowedUserIds
124
+ userCanAccess(ws, userId): boolean // admins always pass; else allowedUserIds check
125
+ resolveWorkspaceRoot(id): Promise<string|null>
126
+ safeWorkspacePath(rootDir, rel): string|null // the old safeRootPath logic
127
+ ```
128
+
129
+ `safeRootPath` logic moves here as `safeWorkspacePath(rootDir, rel)` (takes an
130
+ explicit root instead of reading the global). Keep `root-dir.ts` as a thin
131
+ deprecated shim during migration if helpful, then delete.
132
+
133
+ ### 2. Request-scoped workspace resolution
134
+
135
+ New `src/lib/workspace-context.ts`:
136
+
137
+ ```ts
138
+ // Browser/session routes
139
+ async function resolveWorkspaceForUser(
140
+ req,
141
+ ): Promise<
142
+ | { ok: true; ws: Workspace; rootDir: string }
143
+ | { ok: false; status: number; code: string }
144
+ >;
145
+ ```
146
+
147
+ Resolution order for browser routes:
148
+
149
+ 1. `ws` query param (preferred) or `x-workspace` header.
150
+ 2. If absent, fall back to the user's `lastOpenedAt` workspace (back-compat,
151
+ single-workspace users never notice).
152
+ 3. 400 `WORKSPACE_REQUIRED` if none and more than one workspace exists.
153
+
154
+ Then enforce `userCanAccess(ws, user.id)` → 403 `WORKSPACE_FORBIDDEN`.
155
+
156
+ For agent routes, `checkAuth` (in `src/lib/proof/auth.ts`) additionally reads
157
+ `X-Workspace`, looks up the workspace, and checks the agent's grant for it
158
+ (see §5). Returns `{ ok, agent, workspace, rootDir }`.
159
+
160
+ ### 3. Namespacing the in-memory stores
161
+
162
+ Every store keyed by relPath becomes keyed by `wsId + "\0" + relPath` (the
163
+ workspace id is a stable prefix, NUL separator avoids collisions).
164
+
165
+ - `lease.ts`: `setLease`, `hasActiveLease`, `clearLease`, `leaseGeneration`
166
+ gain a `wsId` first arg → internal key `${wsId}\0${relPath}`.
167
+ `computeCollabState(rootDir, relPath)` already has rootDir; add `wsId` (or
168
+ derive a key from rootDir — but explicit wsId is cleaner). Update
169
+ `collab-state.ts` and `presence/route.ts` callers.
170
+ - `mutex.ts` / `file-lock.ts`: callers pass a key already; change every call
171
+ site to prefix with `wsId` (e.g. `withFileMutex(`${wsId}\0${relPath}`, ...)`).
172
+ Cross-process sentinel hash then differs per workspace automatically.
173
+ - `idempotency.ts`: prefix the key with `wsId` at the call site
174
+ (`events/[...path]/route.ts`, `files/[...path]/route.ts`).
175
+ - `registry.ts` `lastSeenWriteAt` throttle map is keyed by agentId — fine, no
176
+ change.
177
+
178
+ ### 4. Threading rootDir through routes (29 files)
179
+
180
+ Pattern, every session route:
181
+
182
+ ```ts
183
+ const ctx = await resolveWorkspaceForUser(request);
184
+ if (!ctx.ok)
185
+ return NextResponse.json({ error: ctx.code }, { status: ctx.status });
186
+ const { ws, rootDir } = ctx;
187
+ const filePath = safeWorkspacePath(rootDir, rel);
188
+ ```
189
+
190
+ Replace all `getRootDir()` → `ctx.rootDir`, `safeRootPath(rel)` →
191
+ `safeWorkspacePath(rootDir, rel)`, `isRootDirSet()` → existence of ctx.
192
+
193
+ Route inventory (all under `src/app/api`):
194
+
195
+ - `wiki/route.ts`, `wiki/content/route.ts`, `wiki/page/route.ts`,
196
+ `wiki/slugs/route.ts`, `wiki/file/route.ts`, `wiki/new-file/route.ts`,
197
+ `wiki/folder/route.ts`, `wiki/move/route.ts`, `wiki/upload/route.ts`,
198
+ `wiki/download/route.ts`, `wiki/app/route.ts`, `wiki/watch/route.ts`,
199
+ `wiki/presence/route.ts`
200
+ - `assets/[...path]/route.ts`, `upload/[...path]/route.ts`
201
+ - `system/reveal/route.ts`
202
+ - agent: `fs/file`, `fs/ls`, `fs/move`, `fs/search`, `files`, `events`,
203
+ `sidecar`, `activity`, `settings`, `internal/span`
204
+
205
+ `watch/route.ts` (SSE / chokidar): watcher must watch `ws.rootDir`. Each EventSource
206
+ connection is per-workspace; the client opens `/api/wiki/watch?ws=<id>`.
207
+
208
+ ### 5. Workspace-scoped agents
209
+
210
+ Agent grants gain a workspace dimension. In `registry.ts`:
211
+
212
+ ```ts
213
+ interface AgentScope {
214
+ workspaceId: string; // NEW — the workspace this grant applies to
215
+ paths: string[];
216
+ ops: Array<"read" | "mutate" | "delete">;
217
+ }
218
+ ```
219
+
220
+ (An agent id may appear once per workspace, or scope becomes an array of
221
+ per-workspace grants. Simpler: one registry row per (agentId, workspaceId).
222
+ Recommend: keep `Agent.id` unique, add `workspaceId` to the record; an agent
223
+ that needs two workspaces registers twice.)
224
+
225
+ - `register` route: request body includes target `workspaceId`. Approval UI in
226
+ the AI panel shows which workspace the request is for.
227
+ - `checkAuth`: resolve `X-Workspace`, then `enforceScope` also checks
228
+ `agent.scope.workspaceId === ws.id`. Mismatch → 403.
229
+ - Browser session users (`user:<id>`) are synthesized in `checkAuth` with full
230
+ scope; gate them with `userCanAccess(ws, userId)` instead.
231
+ - `audit.ts`: add `workspace_id` column to `agent_fs_audit`
232
+ (`ALTER TABLE ... ADD COLUMN`; tolerate existing DBs). Write it on every row.
233
+
234
+ ### 6. Config migration
235
+
236
+ On first read of `config.json` with the new code:
237
+
238
+ - If `workspaces` is absent but `lastOpenedPath` exists, synthesize one
239
+ workspace `{ id: ws_*, rootDir: lastOpenedPath, name: basename, pinnedPaths:
240
+ config.pinnedPaths }` and mark it lastOpened. Keep old fields for one release
241
+ for rollback safety; new writes use `workspaces`.
242
+ - `agents.json`: existing agents have no `workspaceId`. On load, backfill them
243
+ to the migrated default workspace id (and log once, mirroring the existing
244
+ legacy `ownerUserId` handling in the README checklist).
245
+
246
+ ### 7. API: workspace management routes
247
+
248
+ New under `src/app/api/system/workspaces`:
249
+
250
+ - `GET /api/system/workspaces` — list workspaces visible to the user
251
+ (filtered by `userCanAccess`).
252
+ - `POST /api/system/workspaces` — create `{ rootDir, name? }` (validates dir
253
+ exists, isDirectory). Replaces `set-root` semantics.
254
+ - `PATCH /api/system/workspaces/<id>` — rename / pin-unpin allowed for any
255
+ user with access; **editing `allowedUserIds` is admin-only** (enforced field
256
+ by field in the handler).
257
+ - `DELETE /api/system/workspaces/<id>` — **admin-only**. Unregister (directory
258
+ untouched).
259
+
260
+ Workspace create (`POST`) is **admin-only** and sets `createdBy`. `GET` is open
261
+ to any signed-in user but filtered: admins see all, others see only workspaces
262
+ they can access. Admins management: `GET/POST/DELETE /api/system/admins`.
263
+
264
+ Deprecate / re-point: `set-root`, `clear-root`, `root-status`, `pins`. Keep
265
+ thin back-compat shims that operate on "the user's current/last workspace" so
266
+ nothing 404s mid-migration, then remove in a follow-up.
267
+
268
+ ### 8. Client (browser)
269
+
270
+ - `src/app/page.tsx`: hold `activeWorkspaceId` in state. Read from URL `?ws=`
271
+ first, else last opened. Put `ws` into every fetch (`/api/wiki?ws=...`,
272
+ watch URL, presence body). Cleanest: a small `wsFetch(path, init)` helper /
273
+ thin client store (`workspace-store.ts`) that injects `ws` automatically so
274
+ we are not editing dozens of `fetch` calls by hand.
275
+ - Sidebar footer "Change" → opens a workspace switcher (list of workspaces the
276
+ user may access). "Add workspace" (reuses `DirPicker`) is shown **only to
277
+ admins**. Switching navigates to `?ws=<id>` (and `path` resets). Two tabs
278
+ with different `?ws=` are fully independent.
279
+ - `DirPicker` (`src/components/dir-picker.tsx`): admin-only path. "Select" now
280
+ POSTs `/api/system/workspaces` (create) instead of `set-root`, then routes
281
+ to `?ws=<newId>`. Pins move under the chosen workspace.
282
+ - Settings sheet, admins only: an **Admins** section (promote/demote users)
283
+ and a per-workspace **Access** editor (`allowedUserIds`). The client reads an
284
+ `isAdmin` flag (from `/api/system/admins` or root-status) to gate this UI.
285
+ - `wiki-slugs-store`, `editor-store`, `tree-store`: any cached state that is
286
+ keyed by path must also be reset on workspace switch (cheap: remount the
287
+ page subtree on `ws` change via a React `key={activeWorkspaceId}`).
288
+
289
+ ### 9. CLI / startup (`bin/wiki-viewer.js`, `ROOT_DIR`)
290
+
291
+ - `ROOT_DIR` (and `wiki-viewer ~/dir`) still works: on boot, if no workspaces
292
+ exist, auto-create one from `ROOT_DIR` and mark it default/lastOpened. So the
293
+ zero-config single-dir path is unchanged for existing users.
294
+ - README "switch the served directory" copy updates to mention multiple
295
+ concurrent workspaces.
296
+
297
+ ## Isolation guarantees (the "no bleed" contract)
298
+
299
+ | Surface | Before | After |
300
+ | -------------------- | ----------------- | ------------------------------- |
301
+ | Root dir | one global | per request, from `ws` |
302
+ | Sidecars / activity | `<root>/.proof/` | unchanged (already per-root) |
303
+ | Edit leases | key=relPath | key=`wsId\0relPath` |
304
+ | In-proc + file locks | key=relPath | key=`wsId\0relPath` |
305
+ | Idempotency LRU | key=idemKey | key=`wsId\0idemKey` |
306
+ | Audit DB | path only | `+ workspace_id` column |
307
+ | Agents | global path globs | `+ workspaceId` grant, enforced |
308
+ | Access control | any signed-in | admin-set `allowedUserIds`/ws |
309
+
310
+ Two workspaces sharing the same relative path (`notes.md`) can be edited
311
+ simultaneously without lock contention, lease confusion, or idempotency
312
+ cross-talk.
313
+
314
+ ## Testing
315
+
316
+ Extend `src/tests/proof/*`:
317
+
318
+ - Two-workspace lease test: lease on `(wsA, x.md)` does not make
319
+ `(wsB, x.md)` active.
320
+ - Mutex/lock test: concurrent writes to same relPath in two workspaces do not
321
+ serialize against each other (or at least do not corrupt).
322
+ - Idempotency: same key in two workspaces are independent.
323
+ - Scope test: agent granted `wsA` is 403 on `X-Workspace: wsB`.
324
+ - Access test: user not in `allowedUserIds` is 403; admin always passes.
325
+ - Admin test: non-admin POST /workspaces is 403; last-admin DELETE refused;
326
+ first-signup bootstrap promotes one admin.
327
+ - Migration test: old config.json (lastOpenedPath + pinnedPaths) yields one
328
+ workspace; old agents.json backfills workspaceId.
329
+ - Update existing tests: `setRootDir(tmpRoot)` calls in test setup become
330
+ "create a workspace + resolve it" (helper).
331
+
332
+ ## Rollout / sequencing
333
+
334
+ 0. Admin role: `auth/admin.ts`, `/api/system/admins`, bootstrap, settings UI
335
+ section. Independent, shippable alone.
336
+ 1. `workspaces.ts` + `workspace-context.ts` + config migration (no route
337
+ changes yet; default workspace mirrors old behavior).
338
+ 2. Namespacing stores (lease, mutex, file-lock, idempotency) with `wsId` arg,
339
+ defaulting to the single default workspace so nothing breaks.
340
+ 3. Thread `ctx.rootDir` through all 29 routes; delete `getRootDir` global.
341
+ 4. Agent workspace scoping + audit column.
342
+ 5. Workspace management API + client switcher + DirPicker repoint.
343
+ 6. Tests, README, CLI copy.
344
+
345
+ Steps 1-2 are non-breaking and shippable alone. Step 3 is the big mechanical
346
+ diff (good candidate for a `worker` pass with explicit outcomes per route).
347
+
348
+ ## Open questions / risks
349
+
350
+ - **`pnpm-workspace.yaml` naming clash**: this repo is itself a pnpm workspace.
351
+ Use "workspace" in UI/API, but consider an internal code name like `space` or
352
+ keep `workspace` and just be careful in docs. Low risk, naming only.
353
+ - **SSE watcher fan-out**: one chokidar watcher per (connection, workspace).
354
+ Many open tabs on the same workspace = many watchers. Acceptable now;
355
+ optimize later with a shared per-workspace watcher + ref count if needed.
356
+ - **Admin bootstrap timing**: writing the first user into `adminUserIds` must
357
+ happen exactly once. Better-auth has no built-in "first user" hook, so
358
+ either tap the signup callback or do it lazily on the first authenticated
359
+ request when the admin set is empty (race-safe behind the config write
360
+ mutex). Decide during step 0.
361
+ - **`allowedUserIds` default**: empty = any signed-in user (matches today's
362
+ behavior). Admins tighten per workspace via the Access editor.
363
+ - Removing a workspace leaves `.proof/` and `.locks/` sentinels on disk; fine
364
+ (directory untouched by design), but document it.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-viewer",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Local file viewer and wiki — view markdown, HTML, PDFs, notebooks, office docs, and more from any directory",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "wiki": "node bin/wiki-viewer.js",
20
20
  "postbuild": "node bin/postbuild.js",
21
21
  "prepublishOnly": "npm run build",
22
- "test": "tsx --test src/tests/proof/*.test.ts"
22
+ "test": "tsx --import ./src/tests/proof/preload.ts --test src/tests/proof/*.test.ts"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=18.0.0"
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { checkAuth, enforceScope } from "@/lib/proof/auth";
3
- import { getRootDir } from "@/lib/root-dir";
3
+ import { resolveWorkspaceForAgent } from "@/lib/workspace-context";
4
4
  import {
5
5
  aggregateActivity,
6
6
  ACTIVITY_DEFAULT_LIMIT,
@@ -16,14 +16,15 @@ export async function GET(req: Request): Promise<NextResponse> {
16
16
  return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
17
17
  }
18
18
 
19
- const scopeCheck = enforceScope(auth.agent, { op: "read" });
20
- if (!scopeCheck.ok) {
21
- return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
19
+ const wsx = await resolveWorkspaceForAgent(req);
20
+ if (!wsx.ok) {
21
+ return NextResponse.json({ error: wsx.code }, { status: wsx.status });
22
22
  }
23
+ const { ws, rootDir } = wsx;
23
24
 
24
- const rootDir = getRootDir();
25
- if (!rootDir) {
26
- return NextResponse.json({ error: "ROOT_NOT_SET" }, { status: 503 });
25
+ const scopeCheck = enforceScope(auth.agent, { op: "read", workspaceId: ws.id });
26
+ if (!scopeCheck.ok) {
27
+ return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
27
28
  }
28
29
 
29
30
  const url = new URL(req.url);
@@ -32,7 +33,7 @@ export async function GET(req: Request): Promise<NextResponse> {
32
33
 
33
34
  // Enforce scope on explicit file query parameter
34
35
  if (fileFilter) {
35
- const fileScope = enforceScope(auth.agent, { filePath: fileFilter, op: "read" });
36
+ const fileScope = enforceScope(auth.agent, { filePath: fileFilter, op: "read", workspaceId: ws.id });
36
37
  if (!fileScope.ok) {
37
38
  return NextResponse.json({ error: fileScope.code, message: fileScope.message }, { status: 403 });
38
39
  }
@@ -3,7 +3,8 @@ import { checkAuth, enforceScope, verifyBy } from "@/lib/proof/auth";
3
3
  import { readSidecar, writeSidecar, emptySidecar } from "@/lib/proof/sidecar";
4
4
  import { pollEvents } from "@/lib/proof/event-bus";
5
5
  import { withFileMutex } from "@/lib/proof/mutex";
6
- import { getRootDir, safeRootPath } from "@/lib/root-dir";
6
+ import { resolveWorkspaceForAgent } from "@/lib/workspace-context";
7
+ import { safeWorkspacePath } from "@/lib/workspaces";
7
8
 
8
9
  export const runtime = "nodejs";
9
10
 
@@ -28,6 +29,12 @@ export async function GET(
28
29
  return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
29
30
  }
30
31
 
32
+ const wsx = await resolveWorkspaceForAgent(req);
33
+ if (!wsx.ok) {
34
+ return NextResponse.json({ error: wsx.code }, { status: wsx.status });
35
+ }
36
+ const { ws, rootDir } = wsx;
37
+
31
38
  const { path: segments } = await params;
32
39
  const rel = mdPath(segments);
33
40
 
@@ -38,12 +45,12 @@ export async function GET(
38
45
  return NextResponse.json({ error: "INVALID_PATH", message: "Path must be .md or .markdown" }, { status: 400 });
39
46
  }
40
47
 
41
- const absPath = safeRootPath(rel);
48
+ const absPath = safeWorkspacePath(rootDir, rel);
42
49
  if (!absPath) {
43
50
  return NextResponse.json({ error: "INVALID_PATH", message: "Path traversal rejected" }, { status: 400 });
44
51
  }
45
52
 
46
- const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "read" });
53
+ const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "read", workspaceId: ws.id });
47
54
  if (!scopeCheck.ok) {
48
55
  return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
49
56
  }
@@ -65,7 +72,6 @@ export async function GET(
65
72
  limit = MAX_LIMIT;
66
73
  }
67
74
 
68
- const rootDir = getRootDir();
69
75
  const sidecar = (await readSidecar(rootDir, rel)) ?? emptySidecar(rel);
70
76
  const events = pollEvents(sidecar, after, limit);
71
77
  const lastEventId = sidecar.nextEventId - 1;
@@ -83,6 +89,12 @@ export async function POST(
83
89
  return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
84
90
  }
85
91
 
92
+ const wsx = await resolveWorkspaceForAgent(req);
93
+ if (!wsx.ok) {
94
+ return NextResponse.json({ error: wsx.code }, { status: wsx.status });
95
+ }
96
+ const { ws, rootDir } = wsx;
97
+
86
98
  const { path: segments } = await params;
87
99
  const rel = mdPath(segments);
88
100
 
@@ -93,12 +105,12 @@ export async function POST(
93
105
  return NextResponse.json({ error: "INVALID_PATH", message: "Path must be .md or .markdown" }, { status: 400 });
94
106
  }
95
107
 
96
- const absPath = safeRootPath(rel);
108
+ const absPath = safeWorkspacePath(rootDir, rel);
97
109
  if (!absPath) {
98
110
  return NextResponse.json({ error: "INVALID_PATH", message: "Path traversal rejected" }, { status: 400 });
99
111
  }
100
112
 
101
- const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "mutate" });
113
+ const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "mutate", workspaceId: ws.id });
102
114
  if (!scopeCheck.ok) {
103
115
  return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
104
116
  }
@@ -122,11 +134,11 @@ export async function POST(
122
134
  return NextResponse.json({ error: byCheck.code, message: byCheck.message }, { status: 403 });
123
135
  }
124
136
 
125
- const rootDir = getRootDir();
126
137
  const upToId = body.upToId as number;
127
138
  const by = body.by as string;
128
139
 
129
- await withFileMutex(rel, async () => {
140
+ // Namespace mutex key with rootDir to prevent cross-workspace lock contention.
141
+ await withFileMutex(`${rootDir}\0${rel}`, async () => {
130
142
  const sidecar = (await readSidecar(rootDir, rel)) ?? emptySidecar(rel);
131
143
  sidecar.lastAck[by] = upToId;
132
144
  await writeSidecar(rootDir, rel, sidecar);
@@ -18,7 +18,8 @@ import { NextResponse } from "next/server";
18
18
  import { checkAuth, enforceScope, verifyBy } from "@/lib/proof/auth";
19
19
  import { applyOps, readSnapshot } from "@/lib/proof/ops-applier";
20
20
  import { idempotency } from "@/lib/proof/idempotency";
21
- import { getRootDir, safeRootPath } from "@/lib/root-dir";
21
+ import { resolveWorkspaceForAgent } from "@/lib/workspace-context";
22
+ import { safeWorkspacePath } from "@/lib/workspaces";
22
23
  import type { Op } from "@/lib/proof/types";
23
24
  import { checkAndConsume } from "@/lib/proof/rate-limit";
24
25
  import { computeCollabState } from "@/lib/proof/collab-state";
@@ -52,17 +53,20 @@ export async function GET(
52
53
  return NextResponse.json({ error: "INVALID_PATH", message: "Path must be .md or .markdown" }, { status: 400 });
53
54
  }
54
55
 
55
- const absPath = safeRootPath(rel);
56
+ const wsx = await resolveWorkspaceForAgent(req);
57
+ if (!wsx.ok) return NextResponse.json({ error: wsx.code }, { status: wsx.status });
58
+ const { ws, rootDir } = wsx;
59
+
60
+ const absPath = safeWorkspacePath(rootDir, rel);
56
61
  if (!absPath) {
57
62
  return NextResponse.json({ error: "INVALID_PATH", message: "Path traversal rejected" }, { status: 400 });
58
63
  }
59
64
 
60
- const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "read" });
65
+ const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "read", workspaceId: ws.id });
61
66
  if (!scopeCheck.ok) {
62
67
  return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
63
68
  }
64
69
 
65
- const rootDir = getRootDir();
66
70
  const snapshot = await readSnapshot(rootDir, rel);
67
71
  if (!snapshot) {
68
72
  return NextResponse.json({ error: "NOT_FOUND", message: "File not found" }, { status: 404 });
@@ -107,7 +111,11 @@ export async function POST(
107
111
  return NextResponse.json({ error: "INVALID_PATH", message: "Path must be .md or .markdown" }, { status: 400 });
108
112
  }
109
113
 
110
- const absPath = safeRootPath(rel);
114
+ const wsx = await resolveWorkspaceForAgent(req);
115
+ if (!wsx.ok) return NextResponse.json({ error: wsx.code }, { status: wsx.status });
116
+ const { ws, rootDir } = wsx;
117
+
118
+ const absPath = safeWorkspacePath(rootDir, rel);
111
119
  if (!absPath) {
112
120
  return NextResponse.json({ error: "INVALID_PATH", message: "Path traversal rejected" }, { status: 400 });
113
121
  }
@@ -123,7 +131,7 @@ export async function POST(
123
131
  const payloadHash = createHash("sha256").update(rawBody, "utf8").digest("hex");
124
132
 
125
133
  // Check idempotency cache
126
- const cached = idempotency.get(idempotencyKey);
134
+ const cached = idempotency.get(`${rootDir}\u0000${idempotencyKey}`);
127
135
  if (cached) {
128
136
  if (cached.payloadHash !== payloadHash) {
129
137
  return NextResponse.json(
@@ -154,7 +162,7 @@ export async function POST(
154
162
  return NextResponse.json({ error: "INVALID_PAYLOAD", message: "ops (array) required" }, { status: 400 });
155
163
  }
156
164
 
157
- const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "mutate" });
165
+ const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "mutate", workspaceId: ws.id });
158
166
  if (!scopeCheck.ok) {
159
167
  return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
160
168
  }
@@ -181,7 +189,6 @@ export async function POST(
181
189
  );
182
190
  }
183
191
 
184
- const rootDir = getRootDir();
185
192
  const result = await applyOps({
186
193
  rootDir,
187
194
  mdPath: rel,
@@ -203,7 +210,7 @@ export async function POST(
203
210
  responseBody = JSON.stringify(payload);
204
211
  }
205
212
 
206
- idempotency.set(idempotencyKey, { payloadHash, status, body: responseBody });
213
+ idempotency.set(`${rootDir}\u0000${idempotencyKey}`, { payloadHash, status, body: responseBody });
207
214
 
208
215
  return new NextResponse(responseBody, {
209
216
  status,