@bevel-software/platform-core-backend 0.1.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 (720) hide show
  1. package/LICENSE +202 -0
  2. package/dist/assets.d.ts +5 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +23 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/core/core-ports.d.ts +83 -0
  7. package/dist/core/core-ports.d.ts.map +1 -0
  8. package/dist/core/core-ports.js +12 -0
  9. package/dist/core/core-ports.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +62 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -0
  12. package/dist/core/create-core-server.js +204 -0
  13. package/dist/core/create-core-server.js.map +1 -0
  14. package/dist/core/create-core-services.d.ts +95 -0
  15. package/dist/core/create-core-services.d.ts.map +1 -0
  16. package/dist/core/create-core-services.js +352 -0
  17. package/dist/core/create-core-services.js.map +1 -0
  18. package/dist/core-config.d.ts +119 -0
  19. package/dist/core-config.d.ts.map +1 -0
  20. package/dist/core-config.js +240 -0
  21. package/dist/core-config.js.map +1 -0
  22. package/dist/index.d.ts +36 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +36 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/modules/access/access-control.interface.d.ts +348 -0
  27. package/dist/modules/access/access-control.interface.d.ts.map +1 -0
  28. package/dist/modules/access/access-control.interface.js +2 -0
  29. package/dist/modules/access/access-control.interface.js.map +1 -0
  30. package/dist/modules/access/access-control.service.d.ts +323 -0
  31. package/dist/modules/access/access-control.service.d.ts.map +1 -0
  32. package/dist/modules/access/access-control.service.js +1431 -0
  33. package/dist/modules/access/access-control.service.js.map +1 -0
  34. package/dist/modules/access/access-errors.d.ts +34 -0
  35. package/dist/modules/access/access-errors.d.ts.map +1 -0
  36. package/dist/modules/access/access-errors.js +40 -0
  37. package/dist/modules/access/access-errors.js.map +1 -0
  38. package/dist/modules/access/access-mutation.service.d.ts +176 -0
  39. package/dist/modules/access/access-mutation.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-mutation.service.js +307 -0
  41. package/dist/modules/access/access-mutation.service.js.map +1 -0
  42. package/dist/modules/access/access-splice.d.ts +98 -0
  43. package/dist/modules/access/access-splice.d.ts.map +1 -0
  44. package/dist/modules/access/access-splice.js +346 -0
  45. package/dist/modules/access/access-splice.js.map +1 -0
  46. package/dist/modules/access/access.routes.d.ts +10 -0
  47. package/dist/modules/access/access.routes.d.ts.map +1 -0
  48. package/dist/modules/access/access.routes.js +757 -0
  49. package/dist/modules/access/access.routes.js.map +1 -0
  50. package/dist/modules/access/creator-access.d.ts +113 -0
  51. package/dist/modules/access/creator-access.d.ts.map +1 -0
  52. package/dist/modules/access/creator-access.js +212 -0
  53. package/dist/modules/access/creator-access.js.map +1 -0
  54. package/dist/modules/access/kb-read-filter.d.ts +41 -0
  55. package/dist/modules/access/kb-read-filter.d.ts.map +1 -0
  56. package/dist/modules/access/kb-read-filter.js +60 -0
  57. package/dist/modules/access/kb-read-filter.js.map +1 -0
  58. package/dist/modules/access/roles-admin.service.d.ts +240 -0
  59. package/dist/modules/access/roles-admin.service.d.ts.map +1 -0
  60. package/dist/modules/access/roles-admin.service.js +630 -0
  61. package/dist/modules/access/roles-admin.service.js.map +1 -0
  62. package/dist/modules/access/roles-edit.d.ts +91 -0
  63. package/dist/modules/access/roles-edit.d.ts.map +1 -0
  64. package/dist/modules/access/roles-edit.js +232 -0
  65. package/dist/modules/access/roles-edit.js.map +1 -0
  66. package/dist/modules/access/roles-yaml-guard.d.ts +56 -0
  67. package/dist/modules/access/roles-yaml-guard.d.ts.map +1 -0
  68. package/dist/modules/access/roles-yaml-guard.js +79 -0
  69. package/dist/modules/access/roles-yaml-guard.js.map +1 -0
  70. package/dist/modules/admin/admin-access.routes.d.ts +18 -0
  71. package/dist/modules/admin/admin-access.routes.d.ts.map +1 -0
  72. package/dist/modules/admin/admin-access.routes.js +26 -0
  73. package/dist/modules/admin/admin-access.routes.js.map +1 -0
  74. package/dist/modules/admin/admin-access.service.d.ts +24 -0
  75. package/dist/modules/admin/admin-access.service.d.ts.map +1 -0
  76. package/dist/modules/admin/admin-access.service.js +41 -0
  77. package/dist/modules/admin/admin-access.service.js.map +1 -0
  78. package/dist/modules/admin/admin.interface.d.ts +11 -0
  79. package/dist/modules/admin/admin.interface.d.ts.map +1 -0
  80. package/dist/modules/admin/admin.interface.js +2 -0
  81. package/dist/modules/admin/admin.interface.js.map +1 -0
  82. package/dist/modules/auth/account-erasure.service.d.ts +80 -0
  83. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -0
  84. package/dist/modules/auth/account-erasure.service.js +91 -0
  85. package/dist/modules/auth/account-erasure.service.js.map +1 -0
  86. package/dist/modules/auth/auth.middleware.d.ts +23 -0
  87. package/dist/modules/auth/auth.middleware.d.ts.map +1 -0
  88. package/dist/modules/auth/auth.middleware.js +68 -0
  89. package/dist/modules/auth/auth.middleware.js.map +1 -0
  90. package/dist/modules/auth/auth.routes.d.ts +25 -0
  91. package/dist/modules/auth/auth.routes.d.ts.map +1 -0
  92. package/dist/modules/auth/auth.routes.js +72 -0
  93. package/dist/modules/auth/auth.routes.js.map +1 -0
  94. package/dist/modules/auth/auth.service.d.ts +76 -0
  95. package/dist/modules/auth/auth.service.d.ts.map +1 -0
  96. package/dist/modules/auth/auth.service.js +168 -0
  97. package/dist/modules/auth/auth.service.js.map +1 -0
  98. package/dist/modules/code-mode/code-mode-names.d.ts +16 -0
  99. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -0
  100. package/dist/modules/code-mode/code-mode-names.js +30 -0
  101. package/dist/modules/code-mode/code-mode-names.js.map +1 -0
  102. package/dist/modules/code-mode/code-mode.tool.d.ts +13 -0
  103. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -0
  104. package/dist/modules/code-mode/code-mode.tool.js +112 -0
  105. package/dist/modules/code-mode/code-mode.tool.js.map +1 -0
  106. package/dist/modules/code-mode/index.d.ts +3 -0
  107. package/dist/modules/code-mode/index.d.ts.map +1 -0
  108. package/dist/modules/code-mode/index.js +3 -0
  109. package/dist/modules/code-mode/index.js.map +1 -0
  110. package/dist/modules/database/connection.d.ts +6 -0
  111. package/dist/modules/database/connection.d.ts.map +1 -0
  112. package/dist/modules/database/connection.js +12 -0
  113. package/dist/modules/database/connection.js.map +1 -0
  114. package/dist/modules/database/core-schema.d.ts +2273 -0
  115. package/dist/modules/database/core-schema.d.ts.map +1 -0
  116. package/dist/modules/database/core-schema.js +413 -0
  117. package/dist/modules/database/core-schema.js.map +1 -0
  118. package/dist/modules/database/migrate.d.ts +10 -0
  119. package/dist/modules/database/migrate.d.ts.map +1 -0
  120. package/dist/modules/database/migrate.js +40 -0
  121. package/dist/modules/database/migrate.js.map +1 -0
  122. package/dist/modules/database/schema.d.ts +12 -0
  123. package/dist/modules/database/schema.d.ts.map +1 -0
  124. package/dist/modules/database/schema.js +12 -0
  125. package/dist/modules/database/schema.js.map +1 -0
  126. package/dist/modules/diff/diff-paths.d.ts +2 -0
  127. package/dist/modules/diff/diff-paths.d.ts.map +1 -0
  128. package/dist/modules/diff/diff-paths.js +9 -0
  129. package/dist/modules/diff/diff-paths.js.map +1 -0
  130. package/dist/modules/diff/diff.config.d.ts +8 -0
  131. package/dist/modules/diff/diff.config.d.ts.map +1 -0
  132. package/dist/modules/diff/diff.config.js +13 -0
  133. package/dist/modules/diff/diff.config.js.map +1 -0
  134. package/dist/modules/diff/diff.interface.d.ts +73 -0
  135. package/dist/modules/diff/diff.interface.d.ts.map +1 -0
  136. package/dist/modules/diff/diff.interface.js +2 -0
  137. package/dist/modules/diff/diff.interface.js.map +1 -0
  138. package/dist/modules/diff/diff.routes.d.ts +35 -0
  139. package/dist/modules/diff/diff.routes.d.ts.map +1 -0
  140. package/dist/modules/diff/diff.routes.js +228 -0
  141. package/dist/modules/diff/diff.routes.js.map +1 -0
  142. package/dist/modules/diff/diff.service.d.ts +51 -0
  143. package/dist/modules/diff/diff.service.d.ts.map +1 -0
  144. package/dist/modules/diff/diff.service.js +489 -0
  145. package/dist/modules/diff/diff.service.js.map +1 -0
  146. package/dist/modules/diff/line-diff.d.ts +16 -0
  147. package/dist/modules/diff/line-diff.d.ts.map +1 -0
  148. package/dist/modules/diff/line-diff.js +67 -0
  149. package/dist/modules/diff/line-diff.js.map +1 -0
  150. package/dist/modules/mcp/mcp-auth.middleware.d.ts +27 -0
  151. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -0
  152. package/dist/modules/mcp/mcp-auth.middleware.js +116 -0
  153. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -0
  154. package/dist/modules/mcp/mcp-session-store.d.ts +74 -0
  155. package/dist/modules/mcp/mcp-session-store.d.ts.map +1 -0
  156. package/dist/modules/mcp/mcp-session-store.js +131 -0
  157. package/dist/modules/mcp/mcp-session-store.js.map +1 -0
  158. package/dist/modules/mcp/mcp.routes.d.ts +24 -0
  159. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -0
  160. package/dist/modules/mcp/mcp.routes.js +229 -0
  161. package/dist/modules/mcp/mcp.routes.js.map +1 -0
  162. package/dist/modules/mcp/mcp.service.d.ts +198 -0
  163. package/dist/modules/mcp/mcp.service.d.ts.map +1 -0
  164. package/dist/modules/mcp/mcp.service.js +840 -0
  165. package/dist/modules/mcp/mcp.service.js.map +1 -0
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +76 -0
  167. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -0
  168. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +291 -0
  169. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -0
  170. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +18 -0
  171. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -0
  172. package/dist/modules/mcp/oauth/oauth-consent.routes.js +52 -0
  173. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -0
  174. package/dist/modules/mcp/oauth/oauth-state.d.ts +32 -0
  175. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -0
  176. package/dist/modules/mcp/oauth/oauth-state.js +34 -0
  177. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -0
  178. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +92 -0
  179. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -0
  180. package/dist/modules/secrets-vault/db-secrets-vault.service.js +728 -0
  181. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -0
  182. package/dist/modules/secrets-vault/index.d.ts +7 -0
  183. package/dist/modules/secrets-vault/index.d.ts.map +1 -0
  184. package/dist/modules/secrets-vault/index.js +6 -0
  185. package/dist/modules/secrets-vault/index.js.map +1 -0
  186. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts +63 -0
  187. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -0
  188. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +269 -0
  189. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -0
  190. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts +46 -0
  191. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -0
  192. package/dist/modules/secrets-vault/secrets-variable-loader.js +87 -0
  193. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -0
  194. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +210 -0
  195. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -0
  196. package/dist/modules/secrets-vault/secrets-vault.contract.js +90 -0
  197. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -0
  198. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +30 -0
  199. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -0
  200. package/dist/modules/secrets-vault/secrets-vault.routes.js +490 -0
  201. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -0
  202. package/dist/modules/skills/index.d.ts +5 -0
  203. package/dist/modules/skills/index.d.ts.map +1 -0
  204. package/dist/modules/skills/index.js +4 -0
  205. package/dist/modules/skills/index.js.map +1 -0
  206. package/dist/modules/skills/skills.contract.d.ts +58 -0
  207. package/dist/modules/skills/skills.contract.d.ts.map +1 -0
  208. package/dist/modules/skills/skills.contract.js +10 -0
  209. package/dist/modules/skills/skills.contract.js.map +1 -0
  210. package/dist/modules/skills/skills.routes.d.ts +15 -0
  211. package/dist/modules/skills/skills.routes.d.ts.map +1 -0
  212. package/dist/modules/skills/skills.routes.js +34 -0
  213. package/dist/modules/skills/skills.routes.js.map +1 -0
  214. package/dist/modules/skills/skills.service.d.ts +21 -0
  215. package/dist/modules/skills/skills.service.d.ts.map +1 -0
  216. package/dist/modules/skills/skills.service.js +202 -0
  217. package/dist/modules/skills/skills.service.js.map +1 -0
  218. package/dist/modules/skills/skills.tools.d.ts +15 -0
  219. package/dist/modules/skills/skills.tools.d.ts.map +1 -0
  220. package/dist/modules/skills/skills.tools.js +96 -0
  221. package/dist/modules/skills/skills.tools.js.map +1 -0
  222. package/dist/modules/tool-auth/external-api-key.errors.d.ts +14 -0
  223. package/dist/modules/tool-auth/external-api-key.errors.d.ts.map +1 -0
  224. package/dist/modules/tool-auth/external-api-key.errors.js +23 -0
  225. package/dist/modules/tool-auth/external-api-key.errors.js.map +1 -0
  226. package/dist/modules/tool-auth/external-api-key.interface.d.ts +90 -0
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -0
  228. package/dist/modules/tool-auth/external-api-key.interface.js +2 -0
  229. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -0
  230. package/dist/modules/tool-auth/external-api-key.service.d.ts +25 -0
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -0
  232. package/dist/modules/tool-auth/external-api-key.service.js +164 -0
  233. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -0
  234. package/dist/modules/tool-auth/internal-token.service.d.ts +62 -0
  235. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -0
  236. package/dist/modules/tool-auth/internal-token.service.js +85 -0
  237. package/dist/modules/tool-auth/internal-token.service.js.map +1 -0
  238. package/dist/modules/tool-auth/llm-usage-meter.d.ts +16 -0
  239. package/dist/modules/tool-auth/llm-usage-meter.d.ts.map +1 -0
  240. package/dist/modules/tool-auth/llm-usage-meter.js +6 -0
  241. package/dist/modules/tool-auth/llm-usage-meter.js.map +1 -0
  242. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +101 -0
  243. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -0
  244. package/dist/modules/tool-auth/tool-auth.middleware.js +166 -0
  245. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -0
  246. package/dist/modules/tool-helpers/index.d.ts +19 -0
  247. package/dist/modules/tool-helpers/index.d.ts.map +1 -0
  248. package/dist/modules/tool-helpers/index.js +18 -0
  249. package/dist/modules/tool-helpers/index.js.map +1 -0
  250. package/dist/modules/tool-helpers/tool-context.d.ts +35 -0
  251. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -0
  252. package/dist/modules/tool-helpers/tool-context.js +66 -0
  253. package/dist/modules/tool-helpers/tool-context.js.map +1 -0
  254. package/dist/modules/tool-helpers/tool-def.d.ts +33 -0
  255. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -0
  256. package/dist/modules/tool-helpers/tool-def.js +57 -0
  257. package/dist/modules/tool-helpers/tool-def.js.map +1 -0
  258. package/dist/modules/tool-helpers/tool-handler.d.ts +19 -0
  259. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -0
  260. package/dist/modules/tool-helpers/tool-handler.js +84 -0
  261. package/dist/modules/tool-helpers/tool-handler.js.map +1 -0
  262. package/dist/modules/tool-helpers/tool.contract.d.ts +64 -0
  263. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -0
  264. package/dist/modules/tool-helpers/tool.contract.js +24 -0
  265. package/dist/modules/tool-helpers/tool.contract.js.map +1 -0
  266. package/dist/modules/tool-helpers/validate-token.d.ts +31 -0
  267. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -0
  268. package/dist/modules/tool-helpers/validate-token.js +20 -0
  269. package/dist/modules/tool-helpers/validate-token.js.map +1 -0
  270. package/dist/modules/tool-manuals/index.d.ts +5 -0
  271. package/dist/modules/tool-manuals/index.d.ts.map +1 -0
  272. package/dist/modules/tool-manuals/index.js +5 -0
  273. package/dist/modules/tool-manuals/index.js.map +1 -0
  274. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +182 -0
  275. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -0
  276. package/dist/modules/tool-manuals/tool-manuals.contract.js +19 -0
  277. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -0
  278. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +24 -0
  279. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -0
  280. package/dist/modules/tool-manuals/tool-manuals.routes.js +102 -0
  281. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -0
  282. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +108 -0
  283. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -0
  284. package/dist/modules/tool-manuals/tool-manuals.service.js +710 -0
  285. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -0
  286. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +35 -0
  287. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -0
  288. package/dist/modules/tool-manuals/tool-manuals.tools.js +157 -0
  289. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -0
  290. package/dist/modules/tool-registry/manual.routes.d.ts +6 -0
  291. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -0
  292. package/dist/modules/tool-registry/manual.routes.js +76 -0
  293. package/dist/modules/tool-registry/manual.routes.js.map +1 -0
  294. package/dist/modules/tool-registry/tool-registry.d.ts +23 -0
  295. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -0
  296. package/dist/modules/tool-registry/tool-registry.js +46 -0
  297. package/dist/modules/tool-registry/tool-registry.js.map +1 -0
  298. package/dist/modules/tool-registry/tool.contract.d.ts +48 -0
  299. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -0
  300. package/dist/modules/tool-registry/tool.contract.js +2 -0
  301. package/dist/modules/tool-registry/tool.contract.js.map +1 -0
  302. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +14 -0
  303. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -0
  304. package/dist/modules/workflow/agent-tools/workflow.tools.js +437 -0
  305. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -0
  306. package/dist/modules/workflow/event-bus.d.ts +107 -0
  307. package/dist/modules/workflow/event-bus.d.ts.map +1 -0
  308. package/dist/modules/workflow/event-bus.js +224 -0
  309. package/dist/modules/workflow/event-bus.js.map +1 -0
  310. package/dist/modules/workflow/events.routes.d.ts +80 -0
  311. package/dist/modules/workflow/events.routes.d.ts.map +1 -0
  312. package/dist/modules/workflow/events.routes.js +216 -0
  313. package/dist/modules/workflow/events.routes.js.map +1 -0
  314. package/dist/modules/workflow/file-change-notifier.d.ts +38 -0
  315. package/dist/modules/workflow/file-change-notifier.d.ts.map +1 -0
  316. package/dist/modules/workflow/file-change-notifier.js +22 -0
  317. package/dist/modules/workflow/file-change-notifier.js.map +1 -0
  318. package/dist/modules/workflow/file-lock.service.d.ts +57 -0
  319. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -0
  320. package/dist/modules/workflow/file-lock.service.js +202 -0
  321. package/dist/modules/workflow/file-lock.service.js.map +1 -0
  322. package/dist/modules/workflow/git/branch-name.d.ts +10 -0
  323. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -0
  324. package/dist/modules/workflow/git/branch-name.js +76 -0
  325. package/dist/modules/workflow/git/branch-name.js.map +1 -0
  326. package/dist/modules/workflow/git/clone-config.d.ts +61 -0
  327. package/dist/modules/workflow/git/clone-config.d.ts.map +1 -0
  328. package/dist/modules/workflow/git/clone-config.js +69 -0
  329. package/dist/modules/workflow/git/clone-config.js.map +1 -0
  330. package/dist/modules/workflow/git/git-version.d.ts +34 -0
  331. package/dist/modules/workflow/git/git-version.d.ts.map +1 -0
  332. package/dist/modules/workflow/git/git-version.js +63 -0
  333. package/dist/modules/workflow/git/git-version.js.map +1 -0
  334. package/dist/modules/workflow/git/git.service.d.ts +427 -0
  335. package/dist/modules/workflow/git/git.service.d.ts.map +1 -0
  336. package/dist/modules/workflow/git/git.service.js +1987 -0
  337. package/dist/modules/workflow/git/git.service.js.map +1 -0
  338. package/dist/modules/workflow/git/mutex.d.ts +11 -0
  339. package/dist/modules/workflow/git/mutex.d.ts.map +1 -0
  340. package/dist/modules/workflow/git/mutex.js +23 -0
  341. package/dist/modules/workflow/git/mutex.js.map +1 -0
  342. package/dist/modules/workflow/git/pull-request.service.d.ts +124 -0
  343. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -0
  344. package/dist/modules/workflow/git/pull-request.service.js +328 -0
  345. package/dist/modules/workflow/git/pull-request.service.js.map +1 -0
  346. package/dist/modules/workflow/locking-filesystem.d.ts +133 -0
  347. package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -0
  348. package/dist/modules/workflow/locking-filesystem.js +400 -0
  349. package/dist/modules/workflow/locking-filesystem.js.map +1 -0
  350. package/dist/modules/workflow/pending-commits.service.d.ts +221 -0
  351. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -0
  352. package/dist/modules/workflow/pending-commits.service.js +364 -0
  353. package/dist/modules/workflow/pending-commits.service.js.map +1 -0
  354. package/dist/modules/workflow/pending-commits.worker.d.ts +148 -0
  355. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -0
  356. package/dist/modules/workflow/pending-commits.worker.js +245 -0
  357. package/dist/modules/workflow/pending-commits.worker.js.map +1 -0
  358. package/dist/modules/workflow/read-only-filesystem.d.ts +24 -0
  359. package/dist/modules/workflow/read-only-filesystem.d.ts.map +1 -0
  360. package/dist/modules/workflow/read-only-filesystem.js +39 -0
  361. package/dist/modules/workflow/read-only-filesystem.js.map +1 -0
  362. package/dist/modules/workflow/recovery-bot.d.ts +31 -0
  363. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -0
  364. package/dist/modules/workflow/recovery-bot.js +64 -0
  365. package/dist/modules/workflow/recovery-bot.js.map +1 -0
  366. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +137 -0
  367. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -0
  368. package/dist/modules/workflow/review-workflow/review-workflow.interface.js +2 -0
  369. package/dist/modules/workflow/review-workflow/review-workflow.interface.js.map +1 -0
  370. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +26 -0
  371. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -0
  372. package/dist/modules/workflow/review-workflow/review-workflow.service.js +636 -0
  373. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -0
  374. package/dist/modules/workflow/sanitize-error.d.ts +29 -0
  375. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -0
  376. package/dist/modules/workflow/sanitize-error.js +60 -0
  377. package/dist/modules/workflow/sanitize-error.js.map +1 -0
  378. package/dist/modules/workflow/session-ontology.policy.d.ts +52 -0
  379. package/dist/modules/workflow/session-ontology.policy.d.ts.map +1 -0
  380. package/dist/modules/workflow/session-ontology.policy.js +62 -0
  381. package/dist/modules/workflow/session-ontology.policy.js.map +1 -0
  382. package/dist/modules/workflow/session-ontology.service.d.ts +100 -0
  383. package/dist/modules/workflow/session-ontology.service.d.ts.map +1 -0
  384. package/dist/modules/workflow/session-ontology.service.js +143 -0
  385. package/dist/modules/workflow/session-ontology.service.js.map +1 -0
  386. package/dist/modules/workflow/workflow-hooks.d.ts +87 -0
  387. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -0
  388. package/dist/modules/workflow/workflow-hooks.js +35 -0
  389. package/dist/modules/workflow/workflow-hooks.js.map +1 -0
  390. package/dist/modules/workflow/workflow.errors.d.ts +171 -0
  391. package/dist/modules/workflow/workflow.errors.d.ts.map +1 -0
  392. package/dist/modules/workflow/workflow.errors.js +260 -0
  393. package/dist/modules/workflow/workflow.errors.js.map +1 -0
  394. package/dist/modules/workflow/workflow.routes.d.ts +26 -0
  395. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -0
  396. package/dist/modules/workflow/workflow.routes.js +804 -0
  397. package/dist/modules/workflow/workflow.routes.js.map +1 -0
  398. package/dist/modules/workflow/workflow.service.d.ts +385 -0
  399. package/dist/modules/workflow/workflow.service.d.ts.map +1 -0
  400. package/dist/modules/workflow/workflow.service.js +1155 -0
  401. package/dist/modules/workflow/workflow.service.js.map +1 -0
  402. package/dist/modules/workspace/bevel-ignore.d.ts +25 -0
  403. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -0
  404. package/dist/modules/workspace/bevel-ignore.js +57 -0
  405. package/dist/modules/workspace/bevel-ignore.js.map +1 -0
  406. package/dist/modules/workspace/kb-seed.interface.d.ts +36 -0
  407. package/dist/modules/workspace/kb-seed.interface.d.ts.map +1 -0
  408. package/dist/modules/workspace/kb-seed.interface.js +2 -0
  409. package/dist/modules/workspace/kb-seed.interface.js.map +1 -0
  410. package/dist/modules/workspace/kb-seed.service.d.ts +70 -0
  411. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -0
  412. package/dist/modules/workspace/kb-seed.service.js +325 -0
  413. package/dist/modules/workspace/kb-seed.service.js.map +1 -0
  414. package/dist/modules/workspace/routine-write-policy.d.ts +49 -0
  415. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -0
  416. package/dist/modules/workspace/routine-write-policy.js +46 -0
  417. package/dist/modules/workspace/routine-write-policy.js.map +1 -0
  418. package/dist/modules/workspace/session-ontology.gate.d.ts +112 -0
  419. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -0
  420. package/dist/modules/workspace/session-ontology.gate.js +161 -0
  421. package/dist/modules/workspace/session-ontology.gate.js.map +1 -0
  422. package/dist/modules/workspace/session-sink.d.ts +25 -0
  423. package/dist/modules/workspace/session-sink.d.ts.map +1 -0
  424. package/dist/modules/workspace/session-sink.js +8 -0
  425. package/dist/modules/workspace/session-sink.js.map +1 -0
  426. package/dist/modules/workspace/spill-store.d.ts +24 -0
  427. package/dist/modules/workspace/spill-store.d.ts.map +1 -0
  428. package/dist/modules/workspace/spill-store.js +87 -0
  429. package/dist/modules/workspace/spill-store.js.map +1 -0
  430. package/dist/modules/workspace/workspace.routes.d.ts +25 -0
  431. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -0
  432. package/dist/modules/workspace/workspace.routes.js +1046 -0
  433. package/dist/modules/workspace/workspace.routes.js.map +1 -0
  434. package/dist/modules/workspace/workspace.service.d.ts +339 -0
  435. package/dist/modules/workspace/workspace.service.d.ts.map +1 -0
  436. package/dist/modules/workspace/workspace.service.js +1221 -0
  437. package/dist/modules/workspace/workspace.service.js.map +1 -0
  438. package/dist/modules/workspace/workspace.tools.d.ts +19 -0
  439. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -0
  440. package/dist/modules/workspace/workspace.tools.js +767 -0
  441. package/dist/modules/workspace/workspace.tools.js.map +1 -0
  442. package/dist/shared/frontmatter-id.d.ts +35 -0
  443. package/dist/shared/frontmatter-id.d.ts.map +1 -0
  444. package/dist/shared/frontmatter-id.js +68 -0
  445. package/dist/shared/frontmatter-id.js.map +1 -0
  446. package/dist/shared/fs-walk.d.ts +9 -0
  447. package/dist/shared/fs-walk.d.ts.map +1 -0
  448. package/dist/shared/fs-walk.js +34 -0
  449. package/dist/shared/fs-walk.js.map +1 -0
  450. package/dist/shared/hash-email.d.ts +11 -0
  451. package/dist/shared/hash-email.d.ts.map +1 -0
  452. package/dist/shared/hash-email.js +14 -0
  453. package/dist/shared/hash-email.js.map +1 -0
  454. package/dist/shared/kb-layout.d.ts +38 -0
  455. package/dist/shared/kb-layout.d.ts.map +1 -0
  456. package/dist/shared/kb-layout.js +102 -0
  457. package/dist/shared/kb-layout.js.map +1 -0
  458. package/dist/shared/kb-layout.test.d.ts +2 -0
  459. package/dist/shared/kb-layout.test.d.ts.map +1 -0
  460. package/dist/shared/kb-layout.test.js +67 -0
  461. package/dist/shared/kb-layout.test.js.map +1 -0
  462. package/dist/shared/ssrf.d.ts +20 -0
  463. package/dist/shared/ssrf.d.ts.map +1 -0
  464. package/dist/shared/ssrf.js +89 -0
  465. package/dist/shared/ssrf.js.map +1 -0
  466. package/dist/shared/token-crypto.d.ts +16 -0
  467. package/dist/shared/token-crypto.d.ts.map +1 -0
  468. package/dist/shared/token-crypto.js +49 -0
  469. package/dist/shared/token-crypto.js.map +1 -0
  470. package/dist/shared/ttl-cache.d.ts +16 -0
  471. package/dist/shared/ttl-cache.d.ts.map +1 -0
  472. package/dist/shared/ttl-cache.js +27 -0
  473. package/dist/shared/ttl-cache.js.map +1 -0
  474. package/dist/shared/utcp-namespace.d.ts +46 -0
  475. package/dist/shared/utcp-namespace.d.ts.map +1 -0
  476. package/dist/shared/utcp-namespace.js +82 -0
  477. package/dist/shared/utcp-namespace.js.map +1 -0
  478. package/dist/version.d.ts +17 -0
  479. package/dist/version.d.ts.map +1 -0
  480. package/dist/version.js +19 -0
  481. package/dist/version.js.map +1 -0
  482. package/kb-template/.bevelignore +21 -0
  483. package/kb-template/Agents/.gitkeep +0 -0
  484. package/kb-template/Agents/README.md +38 -0
  485. package/kb-template/CLAUDE.md +419 -0
  486. package/kb-template/Data/.gitkeep +0 -0
  487. package/kb-template/Data/README.md +39 -0
  488. package/kb-template/KnowledgeBase/.gitkeep +0 -0
  489. package/kb-template/KnowledgeBase/README.md +37 -0
  490. package/kb-template/Pipelines/.gitkeep +0 -0
  491. package/kb-template/Pipelines/README.md +38 -0
  492. package/kb-template/Skills/.gitkeep +0 -0
  493. package/kb-template/Skills/README.md +32 -0
  494. package/kb-template/Tools/.gitkeep +0 -0
  495. package/kb-template/Tools/README.md +42 -0
  496. package/kb-template/access.md +36 -0
  497. package/migrations/0000_core_init.sql +243 -0
  498. package/migrations/meta/0000_snapshot.json +1480 -0
  499. package/migrations/meta/_journal.json +13 -0
  500. package/package.json +80 -0
  501. package/src/assets.ts +25 -0
  502. package/src/core/core-ports.ts +91 -0
  503. package/src/core/create-core-server.ts +338 -0
  504. package/src/core/create-core-services.ts +501 -0
  505. package/src/core-config.ts +257 -0
  506. package/src/index.ts +69 -0
  507. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -0
  508. package/src/modules/access/__tests__/access-control.read-batch.test.ts +132 -0
  509. package/src/modules/access/__tests__/access-control.service.test.ts +1011 -0
  510. package/src/modules/access/__tests__/access-mutation.service.test.ts +439 -0
  511. package/src/modules/access/__tests__/access-splice.test.ts +247 -0
  512. package/src/modules/access/__tests__/access.routes.revoke.test.ts +407 -0
  513. package/src/modules/access/__tests__/creator-access.test.ts +217 -0
  514. package/src/modules/access/__tests__/grant-sources.test.ts +204 -0
  515. package/src/modules/access/__tests__/kb-read-filter.test.ts +70 -0
  516. package/src/modules/access/__tests__/roles-admin.service.test.ts +469 -0
  517. package/src/modules/access/__tests__/roles-edit.test.ts +138 -0
  518. package/src/modules/access/__tests__/roles-yaml-guard.test.ts +70 -0
  519. package/src/modules/access/__tests__/roles.routes.test.ts +234 -0
  520. package/src/modules/access/access-control.interface.ts +383 -0
  521. package/src/modules/access/access-control.service.ts +1794 -0
  522. package/src/modules/access/access-errors.ts +60 -0
  523. package/src/modules/access/access-mutation.service.ts +369 -0
  524. package/src/modules/access/access-splice.ts +413 -0
  525. package/src/modules/access/access.routes.ts +894 -0
  526. package/src/modules/access/creator-access.ts +270 -0
  527. package/src/modules/access/kb-read-filter.ts +76 -0
  528. package/src/modules/access/roles-admin.service.ts +741 -0
  529. package/src/modules/access/roles-edit.ts +248 -0
  530. package/src/modules/access/roles-yaml-guard.ts +89 -0
  531. package/src/modules/admin/admin-access.routes.ts +29 -0
  532. package/src/modules/admin/admin-access.service.ts +38 -0
  533. package/src/modules/admin/admin.interface.ts +10 -0
  534. package/src/modules/auth/account-erasure.service.ts +183 -0
  535. package/src/modules/auth/auth.middleware.ts +78 -0
  536. package/src/modules/auth/auth.routes.ts +97 -0
  537. package/src/modules/auth/auth.service.ts +199 -0
  538. package/src/modules/code-mode/code-mode-names.ts +38 -0
  539. package/src/modules/code-mode/code-mode.tool.ts +117 -0
  540. package/src/modules/code-mode/index.ts +6 -0
  541. package/src/modules/database/connection.ts +15 -0
  542. package/src/modules/database/core-schema.ts +431 -0
  543. package/src/modules/database/migrate.ts +43 -0
  544. package/src/modules/database/schema.ts +11 -0
  545. package/src/modules/diff/__tests__/diff.routes.read-gate.test.ts +87 -0
  546. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -0
  547. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +184 -0
  548. package/src/modules/diff/__tests__/diff.service.test.ts +202 -0
  549. package/src/modules/diff/diff-paths.ts +9 -0
  550. package/src/modules/diff/diff.config.ts +12 -0
  551. package/src/modules/diff/diff.interface.ts +78 -0
  552. package/src/modules/diff/diff.routes.ts +277 -0
  553. package/src/modules/diff/diff.service.ts +508 -0
  554. package/src/modules/diff/line-diff.ts +79 -0
  555. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +287 -0
  556. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +249 -0
  557. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +253 -0
  558. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +151 -0
  559. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +150 -0
  560. package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +92 -0
  561. package/src/modules/mcp/__tests__/mcp.service.test.ts +414 -0
  562. package/src/modules/mcp/mcp-auth.middleware.ts +126 -0
  563. package/src/modules/mcp/mcp-session-store.ts +162 -0
  564. package/src/modules/mcp/mcp.routes.ts +263 -0
  565. package/src/modules/mcp/mcp.service.ts +973 -0
  566. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +401 -0
  567. package/src/modules/mcp/oauth/oauth-consent.routes.ts +60 -0
  568. package/src/modules/mcp/oauth/oauth-state.ts +61 -0
  569. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +277 -0
  570. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +206 -0
  571. package/src/modules/secrets-vault/__tests__/mcp-oauth-discovery.service.test.ts +178 -0
  572. package/src/modules/secrets-vault/__tests__/scopes-covered.test.ts +101 -0
  573. package/src/modules/secrets-vault/__tests__/secrets-variable-loader.test.ts +54 -0
  574. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -0
  575. package/src/modules/secrets-vault/db-secrets-vault.service.ts +792 -0
  576. package/src/modules/secrets-vault/index.ts +25 -0
  577. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +289 -0
  578. package/src/modules/secrets-vault/secrets-variable-loader.ts +104 -0
  579. package/src/modules/secrets-vault/secrets-vault.contract.ts +270 -0
  580. package/src/modules/secrets-vault/secrets-vault.routes.ts +526 -0
  581. package/src/modules/skills/__tests__/skills.service.test.ts +176 -0
  582. package/src/modules/skills/index.ts +10 -0
  583. package/src/modules/skills/skills.contract.ts +54 -0
  584. package/src/modules/skills/skills.routes.ts +38 -0
  585. package/src/modules/skills/skills.service.ts +245 -0
  586. package/src/modules/skills/skills.tools.ts +121 -0
  587. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +358 -0
  588. package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +79 -0
  589. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +88 -0
  590. package/src/modules/tool-auth/external-api-key.errors.ts +25 -0
  591. package/src/modules/tool-auth/external-api-key.interface.ts +99 -0
  592. package/src/modules/tool-auth/external-api-key.service.ts +209 -0
  593. package/src/modules/tool-auth/internal-token.service.ts +124 -0
  594. package/src/modules/tool-auth/llm-usage-meter.ts +19 -0
  595. package/src/modules/tool-auth/tool-auth.middleware.ts +220 -0
  596. package/src/modules/tool-helpers/__tests__/phase0-loopback-spike.test.ts +129 -0
  597. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +95 -0
  598. package/src/modules/tool-helpers/__tests__/tools-surface.test.ts +146 -0
  599. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +75 -0
  600. package/src/modules/tool-helpers/index.ts +18 -0
  601. package/src/modules/tool-helpers/tool-context.ts +98 -0
  602. package/src/modules/tool-helpers/tool-def.ts +73 -0
  603. package/src/modules/tool-helpers/tool-handler.ts +91 -0
  604. package/src/modules/tool-helpers/tool.contract.ts +85 -0
  605. package/src/modules/tool-helpers/validate-token.ts +50 -0
  606. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +200 -0
  607. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +499 -0
  608. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -0
  609. package/src/modules/tool-manuals/index.ts +12 -0
  610. package/src/modules/tool-manuals/tool-manuals.contract.ts +183 -0
  611. package/src/modules/tool-manuals/tool-manuals.routes.ts +110 -0
  612. package/src/modules/tool-manuals/tool-manuals.service.ts +781 -0
  613. package/src/modules/tool-manuals/tool-manuals.tools.ts +202 -0
  614. package/src/modules/tool-registry/__tests__/tool-registry.test.ts +64 -0
  615. package/src/modules/tool-registry/manual.routes.ts +87 -0
  616. package/src/modules/tool-registry/tool-registry.ts +55 -0
  617. package/src/modules/tool-registry/tool.contract.ts +52 -0
  618. package/src/modules/workflow/__tests__/event-bus.test.ts +322 -0
  619. package/src/modules/workflow/__tests__/file-change-notifier.test.ts +51 -0
  620. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +67 -0
  621. package/src/modules/workflow/__tests__/locking-filesystem.test.ts +445 -0
  622. package/src/modules/workflow/__tests__/pending-commits.canonical.test.ts +36 -0
  623. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +243 -0
  624. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +308 -0
  625. package/src/modules/workflow/__tests__/sanitize-error.test.ts +67 -0
  626. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +62 -0
  627. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +200 -0
  628. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +232 -0
  629. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +380 -0
  630. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +340 -0
  631. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +192 -0
  632. package/src/modules/workflow/agent-tools/workflow.tools.ts +502 -0
  633. package/src/modules/workflow/event-bus.ts +262 -0
  634. package/src/modules/workflow/events.routes.ts +266 -0
  635. package/src/modules/workflow/file-change-notifier.ts +54 -0
  636. package/src/modules/workflow/file-lock.service.ts +269 -0
  637. package/src/modules/workflow/git/__tests__/branch-name.test.ts +86 -0
  638. package/src/modules/workflow/git/__tests__/branchAuthor.test.ts +80 -0
  639. package/src/modules/workflow/git/__tests__/clone-config.test.ts +15 -0
  640. package/src/modules/workflow/git/__tests__/git-test-helpers.ts +47 -0
  641. package/src/modules/workflow/git/__tests__/git-version.failure.test.ts +24 -0
  642. package/src/modules/workflow/git/__tests__/git-version.test.ts +49 -0
  643. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +389 -0
  644. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  645. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +105 -0
  646. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +211 -0
  647. package/src/modules/workflow/git/__tests__/git.service.createBranch.test.ts +154 -0
  648. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +320 -0
  649. package/src/modules/workflow/git/__tests__/git.service.diffFileAtCommit.test.ts +202 -0
  650. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +453 -0
  651. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +198 -0
  652. package/src/modules/workflow/git/__tests__/git.service.listTrackedFiles.test.ts +125 -0
  653. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +219 -0
  654. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +145 -0
  655. package/src/modules/workflow/git/__tests__/git.service.pendingChanges.test.ts +240 -0
  656. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +272 -0
  657. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +232 -0
  658. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +101 -0
  659. package/src/modules/workflow/git/__tests__/git.service.status.test.ts +127 -0
  660. package/src/modules/workflow/git/__tests__/mutex.test.ts +78 -0
  661. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +225 -0
  662. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +101 -0
  663. package/src/modules/workflow/git/branch-name.ts +70 -0
  664. package/src/modules/workflow/git/clone-config.ts +71 -0
  665. package/src/modules/workflow/git/git-version.ts +70 -0
  666. package/src/modules/workflow/git/git.service.ts +2248 -0
  667. package/src/modules/workflow/git/mutex.ts +22 -0
  668. package/src/modules/workflow/git/pull-request.service.ts +447 -0
  669. package/src/modules/workflow/locking-filesystem.ts +489 -0
  670. package/src/modules/workflow/pending-commits.service.ts +444 -0
  671. package/src/modules/workflow/pending-commits.worker.ts +361 -0
  672. package/src/modules/workflow/read-only-filesystem.ts +54 -0
  673. package/src/modules/workflow/recovery-bot.ts +73 -0
  674. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +560 -0
  675. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +176 -0
  676. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +258 -0
  677. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +199 -0
  678. package/src/modules/workflow/review-workflow/review-workflow.service.ts +806 -0
  679. package/src/modules/workflow/sanitize-error.ts +62 -0
  680. package/src/modules/workflow/session-ontology.policy.ts +70 -0
  681. package/src/modules/workflow/session-ontology.service.ts +178 -0
  682. package/src/modules/workflow/workflow-hooks.ts +101 -0
  683. package/src/modules/workflow/workflow.errors.ts +285 -0
  684. package/src/modules/workflow/workflow.routes.ts +858 -0
  685. package/src/modules/workflow/workflow.service.ts +1555 -0
  686. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +213 -0
  687. package/src/modules/workspace/__tests__/routine-write-policy.test.ts +68 -0
  688. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -0
  689. package/src/modules/workspace/__tests__/spill-store.test.ts +43 -0
  690. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +233 -0
  691. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +176 -0
  692. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +303 -0
  693. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +276 -0
  694. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +120 -0
  695. package/src/modules/workspace/__tests__/workspace.service.test.ts +565 -0
  696. package/src/modules/workspace/__tests__/workspace.tools.test.ts +504 -0
  697. package/src/modules/workspace/bevel-ignore.ts +62 -0
  698. package/src/modules/workspace/kb-seed.interface.ts +36 -0
  699. package/src/modules/workspace/kb-seed.service.ts +361 -0
  700. package/src/modules/workspace/routine-write-policy.ts +79 -0
  701. package/src/modules/workspace/session-ontology.gate.ts +189 -0
  702. package/src/modules/workspace/session-sink.ts +25 -0
  703. package/src/modules/workspace/spill-store.ts +87 -0
  704. package/src/modules/workspace/workspace.routes.ts +1114 -0
  705. package/src/modules/workspace/workspace.service.ts +1343 -0
  706. package/src/modules/workspace/workspace.tools.ts +885 -0
  707. package/src/shared/__tests__/frontmatter-id.test.ts +88 -0
  708. package/src/shared/__tests__/fs-walk.test.ts +42 -0
  709. package/src/shared/__tests__/ssrf.test.ts +53 -0
  710. package/src/shared/__tests__/utcp-namespace.test.ts +19 -0
  711. package/src/shared/frontmatter-id.ts +72 -0
  712. package/src/shared/fs-walk.ts +30 -0
  713. package/src/shared/hash-email.ts +14 -0
  714. package/src/shared/kb-layout.test.ts +88 -0
  715. package/src/shared/kb-layout.ts +101 -0
  716. package/src/shared/ssrf.ts +78 -0
  717. package/src/shared/token-crypto.ts +54 -0
  718. package/src/shared/ttl-cache.ts +27 -0
  719. package/src/shared/utcp-namespace.ts +88 -0
  720. package/src/version.ts +19 -0
@@ -0,0 +1,1046 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import express from 'express';
4
+ import { AGENTS_DIR, DATA_DIR, DEFAULT_BRANCH, KNOWLEDGE_BASE_DIR, KNOWLEDGE_DIR, PIPELINES_DIR, SKILLS_DIR, TOOLS_DIR, } from '@bevel-software/platform-shared';
5
+ import { branchForWorkspaceId, FolderTooLargeError } from './workspace.service.js';
6
+ import { resolveReadableMap, toKbRelative } from '../access/kb-read-filter.js';
7
+ import { isRolesYamlPath, assertRolesYamlParsable } from '../access/roles-yaml-guard.js';
8
+ import { WorkflowDomainError } from '../workflow/workflow.errors.js';
9
+ import '../auth/auth.middleware.js'; // Express Request augmentation
10
+ const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
11
+ /**
12
+ * Workspaces are per-branch (PLAN §3). Any authenticated user can access
13
+ * any branch's workspace; coordination of concurrent edits lives in the
14
+ * file-lock service, not in giving each user their own clone. We therefore
15
+ * gate every handler on authentication only — there is no "this is my
16
+ * workspace" ownership check.
17
+ *
18
+ * Every file-mutating route on this surface — `PUT /file`, `DELETE /file`,
19
+ * `PATCH /file` (move), `POST /directory`, `POST /upload`, `POST /unzip` —
20
+ * routes its write through `withLock(path, op)`: acquire the per-file
21
+ * workflow lock, perform the op, then release. The release commits the
22
+ * file as a one-file change attributed to the user and pushes it to origin,
23
+ * exactly like the lock-aware filesystem the agent writes through. There
24
+ * is no separate "share" step; save and share are fused.
25
+ */
26
+ export function createWorkspaceRoutes(workspaceService, authService, workflowService, eventBus, accessControl, kbDirName, creatorAccess) {
27
+ const router = express.Router();
28
+ function authenticated(req, res) {
29
+ if (!req.userId) {
30
+ res.status(401).json({ error: 'Unauthenticated' });
31
+ return null;
32
+ }
33
+ const id = req.params.id;
34
+ if (id !== undefined && typeof id !== 'string') {
35
+ res.status(400).json({ error: 'Invalid workspace id' });
36
+ return null;
37
+ }
38
+ return typeof id === 'string' ? id : '';
39
+ }
40
+ /**
41
+ * Resolve `req.userId` to the full `AuthUser` record. Mirrors the helper
42
+ * in `workflow.routes.ts` so file-mutation routes can pass the user into
43
+ * `acquireLock`/`releaseLock` (which need name + email for commit
44
+ * attribution). Returns `null` and writes the response if unauthenticated.
45
+ */
46
+ async function requireUser(req, res) {
47
+ if (!req.userId) {
48
+ res.status(401).json({ error: 'Unauthenticated' });
49
+ return null;
50
+ }
51
+ try {
52
+ const user = await authService.getUserById(req.userId);
53
+ if (!user) {
54
+ res.status(401).json({ error: 'User not found' });
55
+ return null;
56
+ }
57
+ return user;
58
+ }
59
+ catch (err) {
60
+ console.error('[workspace.routes] requireUser failed:', err);
61
+ res.status(500).json({ error: 'Internal server error' });
62
+ return null;
63
+ }
64
+ }
65
+ /**
66
+ * Acquire the workflow lock for `(workspaceId, branch, targetPath)`, run
67
+ * `op`, then release. Release commits + pushes the file as a one-file
68
+ * change attributed to `user` — same pipeline the lock-aware filesystem
69
+ * uses for the agent. Contention surfaces immediately as a 409-style
70
+ * error rather than retrying: human-initiated routes should feel
71
+ * responsive, and the caller will see "locked by X" and can decide what
72
+ * to do next.
73
+ *
74
+ * The release runs in a `finally` so the op's own failure (e.g. validator
75
+ * 422, access denied, path traversal) still releases the lock — otherwise
76
+ * a single bad upload would hold a file hostage. When the op succeeded
77
+ * but the file wasn't actually dirty (e.g. mkdir of an already-populated
78
+ * dir), the release commits nothing and returns `null`.
79
+ */
80
+ async function withLock(workspaceId, user, targetPath, op,
81
+ /**
82
+ * Batch-mode option used by the recursive folder-delete branch: skip
83
+ * the per-file `fs-tree-changed` so the caller can emit a single
84
+ * tree-refresh signal at the end instead of N noisy ones. (The old
85
+ * `skipPush` option is gone — under the queue model commits +
86
+ * pushes happen out of band in the worker, not inline here.)
87
+ */
88
+ options) {
89
+ const branch = branchForWorkspaceId(workspaceId);
90
+ // If the caller already holds the lock, do NOT acquire-and-release
91
+ // here. The editor's lock-aware path (frontend `saveAndRelease`)
92
+ // explicitly manages the lock lifecycle — acquire on Edit, release
93
+ // on Save — and calls PUT /file in between to persist bytes. If
94
+ // this route auto-released after the write, the subsequent explicit
95
+ // `releaseLockApi` would find the lock gone and 400. Detect that
96
+ // case by reading the lock first; when the caller is the holder,
97
+ // run the op without touching the lock and let the caller release
98
+ // it on their own schedule (which is what fuses commit + push into
99
+ // one atomic step the user explicitly triggered).
100
+ const existing = await workflowService.getLock(workspaceId, branch, targetPath);
101
+ if (existing && existing.holderUserId === user.id) {
102
+ const result = await op();
103
+ // Disk write landed but no commit yet (caller will release later,
104
+ // at which point releaseLock fires its own `file-changed` with a
105
+ // real `newSha`). Emit a null-sha file-changed now so any other
106
+ // session tailing this file refetches the latest bytes immediately
107
+ // instead of waiting for the eventual release.
108
+ eventBus.emit({
109
+ kind: 'file-changed',
110
+ workspaceId,
111
+ branch,
112
+ path: targetPath,
113
+ newSha: null,
114
+ byUserId: user.id,
115
+ byUserName: user.name,
116
+ });
117
+ // Also signal "the file tree may have changed" — covers create /
118
+ // delete / rename through this same path so sidebars refresh even
119
+ // when no specific open tab matches the path.
120
+ eventBus.emit({ kind: 'fs-tree-changed', workspaceId, branch });
121
+ return result;
122
+ }
123
+ const acquired = await workflowService.acquireLock(workspaceId, branch, targetPath, user);
124
+ if (!acquired.acquired) {
125
+ const holder = acquired.lock.holderName || 'another user';
126
+ const err = new Error(`"${targetPath}" is being edited by ${holder}. Try again in a moment.`);
127
+ err.status = 409;
128
+ throw err;
129
+ }
130
+ // Two release modes, depending on whether the op succeeded:
131
+ //
132
+ // - op() FAILED → drop the lock WITHOUT enqueueing a commit. The
133
+ // op may have written partial bytes to disk before throwing
134
+ // (write that errored mid-stream, etc.). A normal `releaseLock`
135
+ // would enqueue a commit for whatever's on disk and the worker
136
+ // would silently persist that partial state as a real committed
137
+ // change. `releaseLockNoCommit` drops the lock row only —
138
+ // partial disk state stays where it is but never becomes a
139
+ // committed change with the user's name on it.
140
+ //
141
+ // - op() SUCCEEDED → release. The new releaseLock enqueues a
142
+ // pending-commit row (the actual `commitFile + push` runs out
143
+ // of band in the worker) and drops the lock immediately. We
144
+ // return success once disk + lock + enqueue are all done —
145
+ // the user-visible "save = share" guarantee is satisfied by
146
+ // the shared per-branch workspace, not by waiting on git.
147
+ let opSucceeded = false;
148
+ let result;
149
+ try {
150
+ result = await op();
151
+ opSucceeded = true;
152
+ }
153
+ catch (err) {
154
+ try {
155
+ await workflowService.releaseLockNoCommit(workspaceId, branch, targetPath, user);
156
+ }
157
+ catch (releaseErr) {
158
+ console.warn(`[workspace.routes] releaseLockNoCommit failed after op error for "${targetPath}":`, releaseErr instanceof Error ? releaseErr.message : releaseErr);
159
+ }
160
+ throw err;
161
+ }
162
+ if (!opSucceeded) {
163
+ // Unreachable — `opSucceeded` is set unconditionally after `op()`
164
+ // returns and the catch arm above always rethrows. Keeping the
165
+ // explicit branch makes the intent obvious to readers.
166
+ throw new Error('unreachable: opSucceeded false after try');
167
+ }
168
+ await workflowService.releaseLock(workspaceId, branch, targetPath, user);
169
+ // Signal sidebar / explorer to refresh even when no open tab matched
170
+ // the changed path (e.g. someone else just created a new file).
171
+ // `releaseLock` already fires `file-changed` for the path, but
172
+ // file-changed's frontend handler short-circuits when no matching tab
173
+ // exists. fs-tree-changed is the dedicated structural-change signal.
174
+ //
175
+ // The folder-delete batch suppresses this per-file emit and fires a
176
+ // single `fs-tree-changed` after the loop, cutting ~30 redundant tree
177
+ // refreshes down to 1 for a 30-file folder delete.
178
+ if (!options?.skipFsTreeEvent) {
179
+ eventBus.emit({ kind: 'fs-tree-changed', workspaceId, branch });
180
+ }
181
+ return result;
182
+ }
183
+ /**
184
+ * Map any thrown error to an HTTP response. Centralises the response
185
+ * shape so each route handler stays focused on its own logic.
186
+ * - `WorkflowDomainError` → its `.status` + `.payload`
187
+ * - Path traversal → 403
188
+ * - Invalid path / zip → 400 (Only .zip is also 400)
189
+ * - Could not read zip → 422
190
+ * - Anything else with a `.status` → that status
191
+ * - Default → 500
192
+ */
193
+ function sendError(res, err) {
194
+ if (err instanceof WorkflowDomainError) {
195
+ res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
196
+ return;
197
+ }
198
+ const msg = err instanceof Error ? err.message : 'Unknown error';
199
+ const status = err?.status;
200
+ if (typeof status === 'number') {
201
+ res.status(status).json({ error: msg });
202
+ return;
203
+ }
204
+ if (msg === 'Path traversal detected') {
205
+ res.status(403).json({ error: msg });
206
+ return;
207
+ }
208
+ if (msg.startsWith('Invalid path') || msg.startsWith('Only .zip')) {
209
+ res.status(400).json({ error: msg });
210
+ return;
211
+ }
212
+ if (msg.startsWith('Could not read zip file')) {
213
+ res.status(422).json({ error: msg });
214
+ return;
215
+ }
216
+ res.status(500).json({ error: msg });
217
+ }
218
+ // GET /api/workspace[?branch=<branch>]
219
+ // Bootstrap (or fetch) the workspace for a branch. Defaults to the
220
+ // platform's working branch when no `branch` query param is given —
221
+ // matches the previous "just give me a workspace" behavior except now
222
+ // the workspace is keyed by branch instead of by user.
223
+ router.get('/workspace', async (req, res) => {
224
+ try {
225
+ if (!req.userId) {
226
+ res.status(401).json({ error: 'Unauthenticated' });
227
+ return;
228
+ }
229
+ const user = await authService.getUserById(req.userId);
230
+ if (!user) {
231
+ res.status(401).json({ error: 'User not found' });
232
+ return;
233
+ }
234
+ const branch = typeof req.query.branch === 'string' && req.query.branch.length > 0
235
+ ? req.query.branch
236
+ : DEFAULT_BRANCH;
237
+ const workspace = await workspaceService.getOrCreateForBranch(branch);
238
+ // Bootstrap must use the same read-filtered tree as GET /workspace/:id/files.
239
+ // Otherwise the first sidebar render can expose restricted nodes until the
240
+ // next explicit tree refresh, and clicking one then correctly 403s.
241
+ const fileTree = await workspaceService.listFiles(workspace.id, buildTreeReadFilter(workspace.id, user.email));
242
+ res.json({ workspace, fileTree });
243
+ }
244
+ catch (error) {
245
+ // Log the full stack so the next 500 isn't a guessing game — the
246
+ // bare `error.message` we returned before lost most diagnostic
247
+ // signal (cause chain, stack frames, error class).
248
+ console.error(`[workspace.routes] GET /workspace failed branch=${req.query.branch ?? '(default)'}:`, error instanceof Error ? error.stack ?? error.message : error);
249
+ const msg = error instanceof Error ? error.message : 'Unknown error';
250
+ res.status(500).json({ error: msg });
251
+ }
252
+ });
253
+ router.delete('/workspace/:id', async (req, res) => {
254
+ const id = authenticated(req, res);
255
+ if (id === null)
256
+ return;
257
+ try {
258
+ await workspaceService.deleteWorkspace(id);
259
+ res.json({ status: 'deleted' });
260
+ }
261
+ catch (error) {
262
+ const msg = error instanceof Error ? error.message : 'Unknown error';
263
+ res.status(500).json({ error: msg });
264
+ }
265
+ });
266
+ router.get('/workspace/:id/files', async (req, res) => {
267
+ const id = authenticated(req, res);
268
+ if (id === null)
269
+ return;
270
+ const user = await requireUser(req, res);
271
+ if (!user)
272
+ return;
273
+ try {
274
+ const fileTree = await workspaceService.listFiles(id, buildTreeReadFilter(id, user.email));
275
+ res.json(fileTree);
276
+ }
277
+ catch (error) {
278
+ const msg = error instanceof Error ? error.message : 'Unknown error';
279
+ res.status(500).json({ error: msg });
280
+ }
281
+ });
282
+ // Every `.md` under the KB repo, keyed by repo-root path, in one response.
283
+ // The HTML renderer's `buildGraph()` bridge uses this instead of one fetch
284
+ // per file (the KB is thousands of files — per-file fetching exhausts the
285
+ // browser's connection pool: net::ERR_INSUFFICIENT_RESOURCES).
286
+ router.get('/workspace/:id/kb-files', async (req, res) => {
287
+ const id = authenticated(req, res);
288
+ if (id === null)
289
+ return;
290
+ const user = await requireUser(req, res);
291
+ if (!user)
292
+ return;
293
+ try {
294
+ const files = await workspaceService.readAllKbFiles(id);
295
+ // Keys are repo-relative KB-node paths — gate them with the FULL read
296
+ // check (per-node frontmatter honoured, matching the embed graph). The
297
+ // graph is fetched rarely, so the per-file cost is acceptable here. Drop
298
+ // every node the caller can't read; fail-closed on anything not allowed.
299
+ const keys = Object.keys(files);
300
+ const verdict = await accessControl.canReadBatch(id, user.email, keys);
301
+ const filtered = {};
302
+ for (const k of keys) {
303
+ if (verdict.get(k) === true)
304
+ filtered[k] = files[k];
305
+ }
306
+ res.json({ files: filtered });
307
+ }
308
+ catch (error) {
309
+ const msg = error instanceof Error ? error.message : 'Unknown error';
310
+ res.status(500).json({ error: msg });
311
+ }
312
+ });
313
+ // (The KB-graph read routes — `/workspace/:id/graph`, `/resolve-id`,
314
+ // `/resolve-path` — moved to `modules/kb-graph/kb-graph.routes.ts`: they
315
+ // are enterprise surfaces over the kb-graph service, mounted by the
316
+ // enterprise server extension. Same paths, so the frontend is unchanged.)
317
+ /**
318
+ * Gate a download request on the dedicated `Download` role from roles.yaml.
319
+ * Returns `true` if the request was authorised (the caller proceeds);
320
+ * returns `false` if a response was already sent (401 / 403 / 500) and the
321
+ * caller should bail. Centralised so the file and folder download routes
322
+ * can't drift on the check, the role name, or the error wording.
323
+ *
324
+ * Orthogonal to admin: a user listed in `Admin` does NOT implicitly get
325
+ * Download. The two roles are independent grants in roles.yaml.
326
+ *
327
+ * `hasRole` can throw `AccessConfigError` when `roles.yaml` / `access.md`
328
+ * fails to load (missing role refs, malformed YAML). Without a catch the
329
+ * async rejection bubbles past Express and the request hangs. Route it
330
+ * through `sendError` so the rich `WorkflowDomainError` payload (status +
331
+ * `accessConfigErrors`) surfaces to the caller — same shape the
332
+ * `/access/has-role` endpoint returns.
333
+ */
334
+ async function requireDownloadPermission(req, res, workspaceId, relativePath) {
335
+ const user = await requireUser(req, res);
336
+ if (!user)
337
+ return false;
338
+ let allowed;
339
+ try {
340
+ // `download` is a per-path verb in access.md, resolved via the same
341
+ // chain walk as `write`. Strip the kbDirName prefix so the resolver
342
+ // receives a repo-relative path (mirrors the lock-acquire gate's
343
+ // strip in workflow.service.ts's assertCanWriteAtPath).
344
+ const repoRelative = relativePath.startsWith(`${kbDirName}/`)
345
+ ? relativePath.slice(kbDirName.length + 1)
346
+ : relativePath;
347
+ allowed = await accessControl.canDownload(workspaceId, user.email, repoRelative);
348
+ }
349
+ catch (err) {
350
+ sendError(res, err);
351
+ return false;
352
+ }
353
+ if (!allowed) {
354
+ res.status(403).json({ error: 'Download permission required' });
355
+ return false;
356
+ }
357
+ return true;
358
+ }
359
+ /**
360
+ * Gate a content read on the `read:` verb. Returns `true` if the caller may
361
+ * read `relativePath`; otherwise sends a 403 and returns `false`. Uses the
362
+ * FULL `canRead` (per-node frontmatter honoured) — this is a single-file
363
+ * check, so the per-file read is cheap. Maps the workspace-relative path to
364
+ * its KB-repo-relative form via the shared `toKbRelative`; a path outside the
365
+ * KB repo (reserved workspace files, the KB dir itself) carries no `read:`
366
+ * rules and is never gated — matching the agent tools and diff routes. `read`
367
+ * is default-deny: a KB path must have an effective read grant, such as
368
+ * `read: everyone`, a listed role/user, or an owner grant. Errors fail closed.
369
+ */
370
+ async function requireReadPermission(req, res, workspaceId, relativePath) {
371
+ const user = await requireUser(req, res);
372
+ if (!user)
373
+ return false;
374
+ const repoRelative = toKbRelative(relativePath, kbDirName);
375
+ if (repoRelative === null)
376
+ return true; // non-KB path — no read rules apply
377
+ let allowed;
378
+ try {
379
+ allowed = await accessControl.canRead(workspaceId, user.email, repoRelative);
380
+ }
381
+ catch (err) {
382
+ sendError(res, err);
383
+ return false;
384
+ }
385
+ if (!allowed) {
386
+ res.status(403).json({ error: `You don't have permission to read "${relativePath}".` });
387
+ return false;
388
+ }
389
+ return true;
390
+ }
391
+ /**
392
+ * Build the file-tree read filter for `userEmail`: a per-directory batched
393
+ * verdict over workspace-relative paths, backed by the FULL `canReadBatch`
394
+ * — folder `access.md` chain AND each node's own frontmatter, so the tree
395
+ * shows exactly what the content routes will let the user open (a
396
+ * frontmatter `read:` deny hides the file; a frontmatter grant makes it
397
+ * discoverable). The per-file frontmatter reads are memoized inside the
398
+ * access service (invalidated on commit/pull/switch), so repeat tree builds
399
+ * don't re-walk the disk. Returns undefined-free `ReadTreeFilter`;
400
+ * `listFiles(id)` with no filter is the unfiltered (pre-feature) behaviour
401
+ * for callers that don't gate.
402
+ */
403
+ function buildTreeReadFilter(workspaceId, userEmail) {
404
+ return async (wsRelPaths) => {
405
+ const verdict = await resolveReadableMap((w, e, rels) => accessControl.canReadBatch(w, e, rels), workspaceId, userEmail, kbDirName, wsRelPaths);
406
+ // The structural top-level folders are always shown as folders, even to
407
+ // a user who can't read into them. Their existence isn't sensitive
408
+ // (every KB has them), and keeping them visible lets the explorer render
409
+ // its Knowledge/Skills section view instead of collapsing to an empty
410
+ // flat tree. `KnowledgeBase/`, `Data/`, `Agents/`, `Pipelines/`,
411
+ // `Skills/` + `Tools/` are the current layout; a ROOT-LEVEL `Knowledge/`
412
+ // is the legacy pre-split layout's knowledge root (kb-layout.ts calls it
413
+ // the neutral bucket) and gets the same treatment so legacy clones don't
414
+ // collapse. Only the folders themselves are forced visible — their
415
+ // contents stay gated by the verdict above.
416
+ const structuralRoots = new Set([
417
+ KNOWLEDGE_BASE_DIR,
418
+ DATA_DIR,
419
+ AGENTS_DIR,
420
+ PIPELINES_DIR,
421
+ SKILLS_DIR,
422
+ TOOLS_DIR,
423
+ KNOWLEDGE_DIR,
424
+ ]);
425
+ for (const wp of wsRelPaths) {
426
+ const rel = toKbRelative(wp, kbDirName);
427
+ if (rel !== null && structuralRoots.has(rel))
428
+ verdict.set(wp, true);
429
+ }
430
+ return verdict;
431
+ };
432
+ }
433
+ /**
434
+ * Land a `seed-access-md` creation-grant plan: merge the grant into the new
435
+ * directory's `access.md` through the same lock+commit cycle as any other
436
+ * save, then drop the resolver cache so the very next tree build sees the
437
+ * grant. The current bytes are re-read UNDER the lock and the grant spliced
438
+ * into them — never a blind overwrite, so a concurrent creator's just-landed
439
+ * grant on the same new directory survives. Runs BEFORE the creation itself
440
+ * so the explorer never shows-then-hides the new subtree; if the creation
441
+ * subsequently fails, the leftover is an empty new folder readable only by
442
+ * its creator. Best-effort by contract: a failure here logs and never
443
+ * blocks the creation.
444
+ */
445
+ async function seedCreatorAccessMd(workspaceId, user, plan) {
446
+ try {
447
+ await withLock(workspaceId, user, plan.wsRelPath, async () => {
448
+ let current = '';
449
+ try {
450
+ current = await workspaceService.readFile(workspaceId, plan.wsRelPath);
451
+ }
452
+ catch {
453
+ // Not there yet — the normal case for a brand-new directory.
454
+ }
455
+ const next = plan.apply(current);
456
+ if (next !== current) {
457
+ await workspaceService.writeFile(workspaceId, plan.wsRelPath, next);
458
+ }
459
+ });
460
+ creatorAccess.noteAccessFileWritten(workspaceId);
461
+ }
462
+ catch (err) {
463
+ console.warn(`[workspace.routes] creator access.md seed failed for "${plan.wsRelPath}":`, err instanceof Error ? err.message : err);
464
+ }
465
+ }
466
+ router.get('/workspace/:id/file/raw', async (req, res) => {
467
+ const id = authenticated(req, res);
468
+ if (id === null)
469
+ return;
470
+ const filePath = req.query.path;
471
+ if (!filePath) {
472
+ res.status(400).json({ error: 'path query parameter is required' });
473
+ return;
474
+ }
475
+ // `?download=1` flips this from inline-serve (used by PdfRenderer and
476
+ // the image renderers) to "save to disk" — and the save path is gated
477
+ // on per-path `download:` rules in access.md. The inline path stays
478
+ // open to any authenticated user so the PdfRenderer / image renderers
479
+ // keep working for everyone.
480
+ // Read gate first — inline serve (PdfRenderer/image renderers) was open to
481
+ // any authenticated user; now a node the caller can't read is 403'd whether
482
+ // or not `?download=1` is set. Download additionally needs the `download:` verb.
483
+ if (!(await requireReadPermission(req, res, id, filePath)))
484
+ return;
485
+ const wantsDownload = req.query.download === '1';
486
+ if (wantsDownload) {
487
+ if (!(await requireDownloadPermission(req, res, id, filePath)))
488
+ return;
489
+ }
490
+ try {
491
+ const buffer = await workspaceService.readFileBinary(id, filePath);
492
+ const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
493
+ const mimeTypes = {
494
+ '.png': 'image/png',
495
+ '.jpg': 'image/jpeg',
496
+ '.jpeg': 'image/jpeg',
497
+ '.gif': 'image/gif',
498
+ '.webp': 'image/webp',
499
+ '.svg': 'image/svg+xml',
500
+ '.bmp': 'image/bmp',
501
+ '.ico': 'image/x-icon',
502
+ '.pdf': 'application/pdf',
503
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
504
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
505
+ };
506
+ // SVG is active web content (it can carry <script>). Inline view
507
+ // already trusts the source (the workspace owns its bytes), but a
508
+ // saved-to-disk SVG that's later re-opened in a browser tab would
509
+ // execute its scripts under the file:// origin — so when the user
510
+ // explicitly downloads, force octet-stream to defang it.
511
+ const downloadMime = ext === '.svg' ? 'application/octet-stream' : (mimeTypes[ext] || 'application/octet-stream');
512
+ res.setHeader('Content-Type', wantsDownload ? downloadMime : (mimeTypes[ext] || 'application/octet-stream'));
513
+ // Block MIME-sniffing so a misdeclared file can't be promoted to
514
+ // active content by the browser.
515
+ res.setHeader('X-Content-Type-Options', 'nosniff');
516
+ if (wantsDownload) {
517
+ // RFC 5987 UTF-8 filename encoding so unicode + spaces round-trip;
518
+ // CR/LF stripped to block header injection via a crafted path.
519
+ const basename = (filePath.split('/').pop() || 'file').replace(/[\r\n]/g, '');
520
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(basename)}`);
521
+ }
522
+ res.send(buffer);
523
+ }
524
+ catch (error) {
525
+ const msg = error instanceof Error ? error.message : 'Unknown error';
526
+ if (msg === 'Path traversal detected') {
527
+ res.status(403).json({ error: msg });
528
+ return;
529
+ }
530
+ res.status(404).json({ error: 'File not found' });
531
+ }
532
+ });
533
+ /**
534
+ * GET /workspace/:id/folder/zip?path=<folder>&download=1
535
+ *
536
+ * Build a zip of the folder (respecting the file tree's skip rules: no
537
+ * `.git/`, no `.gitkeep`, no `.bevelignore`d paths) and return it as an
538
+ * attachment. Gated on `download:` access on the folder path — same
539
+ * primitive as the file-download path. Entries inside the archive are
540
+ * prefixed with the folder name so unzipping produces `<folderName>/…`
541
+ * rather than spilling into the caller's working directory.
542
+ *
543
+ * `?download=1` is required to keep the URL shape consistent with the file
544
+ * route — a stray inline request would have no other meaningful behavior
545
+ * for a folder anyway.
546
+ */
547
+ router.get('/workspace/:id/folder/zip', async (req, res) => {
548
+ const id = authenticated(req, res);
549
+ if (id === null)
550
+ return;
551
+ // Reject array-shaped queries (`?path=a&path=b` → ['a','b']) and
552
+ // non-string values explicitly — the old `as string` cast was a lie
553
+ // and would crash on the downstream `.split('/')`.
554
+ const rawPath = req.query.path;
555
+ if (typeof rawPath !== 'string') {
556
+ res.status(400).json({ error: 'path query parameter is required' });
557
+ return;
558
+ }
559
+ // Trim whitespace and strip trailing slashes — `foo/` would otherwise
560
+ // basename to `''` and produce a misleading `.zip` filename. Done
561
+ // before the empty check so `' / '` is also caught.
562
+ const folderPath = rawPath.trim().replace(/\/+$/, '');
563
+ if (!folderPath) {
564
+ res.status(400).json({ error: 'path query parameter is required' });
565
+ return;
566
+ }
567
+ if (req.query.download !== '1') {
568
+ res.status(400).json({ error: 'download=1 is required for folder zip downloads' });
569
+ return;
570
+ }
571
+ if (!(await requireDownloadPermission(req, res, id, folderPath)))
572
+ return;
573
+ try {
574
+ const buffer = await workspaceService.createFolderZip(id, folderPath);
575
+ // `|| 'folder'` covers the edge case where `folderPath` itself was
576
+ // a single bare slash (`/`) that survived the trim — the service
577
+ // would still reject it as path traversal, but the basename
578
+ // default keeps the response sane if it ever gets here.
579
+ const basename = (folderPath.split('/').pop() || 'folder').replace(/[\r\n]/g, '');
580
+ const zipName = `${basename}.zip`;
581
+ res.setHeader('Content-Type', 'application/zip');
582
+ res.setHeader('X-Content-Type-Options', 'nosniff');
583
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(zipName)}`);
584
+ res.send(buffer);
585
+ }
586
+ catch (error) {
587
+ if (error instanceof FolderTooLargeError) {
588
+ res.status(413).json({ error: error.message });
589
+ return;
590
+ }
591
+ const msg = error instanceof Error ? error.message : 'Unknown error';
592
+ if (msg === 'Path traversal detected') {
593
+ res.status(403).json({ error: msg });
594
+ return;
595
+ }
596
+ if (msg === 'Not a directory') {
597
+ res.status(400).json({ error: msg });
598
+ return;
599
+ }
600
+ // ENOENT / other fs errors → 404 (matches the file route's fallback).
601
+ res.status(404).json({ error: 'Folder not found' });
602
+ }
603
+ });
604
+ router.get('/workspace/:id/file', async (req, res) => {
605
+ const id = authenticated(req, res);
606
+ if (id === null)
607
+ return;
608
+ const filePath = req.query.path;
609
+ if (!filePath) {
610
+ res.status(400).json({ error: 'path query parameter is required' });
611
+ return;
612
+ }
613
+ if (!(await requireReadPermission(req, res, id, filePath)))
614
+ return;
615
+ try {
616
+ const content = await workspaceService.readFile(id, filePath);
617
+ // No-store: this endpoint is used by the live-refresh path after a
618
+ // teammate's save (file-changed SSE → refetch). A cached response
619
+ // would yield stale bytes and silently defeat the auto-refresh.
620
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
621
+ res.json({ content });
622
+ }
623
+ catch (error) {
624
+ const msg = error instanceof Error ? error.message : 'Unknown error';
625
+ if (msg === 'Path traversal detected') {
626
+ res.status(403).json({ error: msg });
627
+ return;
628
+ }
629
+ res.status(404).json({ error: 'File not found' });
630
+ }
631
+ });
632
+ router.delete('/workspace/:id/file', async (req, res) => {
633
+ const id = authenticated(req, res);
634
+ if (id === null)
635
+ return;
636
+ const filePath = req.query.path;
637
+ if (!filePath) {
638
+ res.status(400).json({ error: 'path query parameter is required' });
639
+ return;
640
+ }
641
+ const user = await requireUser(req, res);
642
+ if (!user)
643
+ return;
644
+ try {
645
+ // Recursive dir delete decomposes into N per-file lock+commit cycles
646
+ // — each file lands as its own one-file change so the per-file
647
+ // commit-history invariant `GitService.commit` enforces still holds.
648
+ // What we DO batch is the push: instead of N round-trips to origin
649
+ // (the dominant cost — a 30-file folder previously took 30+s of
650
+ // sequential pushes), we run each release with `{ skipPush: true }`
651
+ // and call `pushBranch` once at the end. Per-file `fs-tree-changed`
652
+ // emits are also coalesced into a single end-of-batch event so the
653
+ // explorer doesn't refresh N times.
654
+ const workspaceDir = await workspaceService.getWorkspacePath(id);
655
+ const absolute = path.resolve(workspaceDir, filePath);
656
+ // Boundary check before any `fs.*` call. The single-file path
657
+ // bottoms out in `workspaceService.deleteFile` (which asserts the
658
+ // boundary itself), but this fast-path resolves the absolute
659
+ // ourselves and then `fs.stat` / `fs.rm` / `enumerateFilesUnder`
660
+ // it. Without this guard, a `../escape` `filePath` would let the
661
+ // dir-delete branch operate on directories outside the workspace.
662
+ const workspaceRoot = path.resolve(workspaceDir);
663
+ if (absolute !== workspaceRoot && !absolute.startsWith(workspaceRoot + path.sep)) {
664
+ const err = new Error('Path traversal detected');
665
+ err.status = 403;
666
+ throw err;
667
+ }
668
+ let stat = null;
669
+ try {
670
+ stat = await fs.stat(absolute);
671
+ }
672
+ catch {
673
+ // Not on disk — let workspaceService.deleteFile return its own 404.
674
+ }
675
+ if (stat?.isDirectory()) {
676
+ const filesInDir = await enumerateFilesUnder(absolute, workspaceDir);
677
+ const branch = branchForWorkspaceId(id);
678
+ for (const relFile of filesInDir) {
679
+ await withLock(id, user, relFile, async () => {
680
+ await workspaceService.deleteFile(id, relFile);
681
+ }, { skipFsTreeEvent: true });
682
+ }
683
+ // No explicit push here — each per-file release enqueues a
684
+ // pending-commits row, and the background worker drains them
685
+ // (commit + push) on its own schedule. The N round-trips that
686
+ // the old skipPush+pushBranch pattern collapsed into one happen
687
+ // serially in the worker; user perception is unchanged because
688
+ // the disk-side delete is what other sessions see via
689
+ // `fs-tree-changed`.
690
+ // Sweep the now-empty directory subtree off disk. Git doesn't track
691
+ // empty folders, so there's nothing to commit; this is just disk
692
+ // hygiene so the file tree stops showing the empty containers.
693
+ //
694
+ // This MUST recurse: a folder that held *subfolders* still has those
695
+ // (now-empty) subdirectory shells on disk after the per-file deletes,
696
+ // so a single non-recursive `rmdir(absolute)` would see a non-empty
697
+ // dir and bail — leaving the folder visible in the tree and looking
698
+ // undeletable (BEVA-132). `removeEmptyDirs` walks bottom-up and only
699
+ // removes dirs that are *actually empty* at the moment it visits them,
700
+ // so a concurrent writer's new file (and its parent chain) is
701
+ // preserved — the same safety property the old non-recursive check had.
702
+ try {
703
+ await removeEmptyDirs(absolute);
704
+ }
705
+ catch (rmErr) {
706
+ // Directory already gone (raced delete), or a concurrent writer
707
+ // repopulated it. Either way, skip removal — the per-file deletes
708
+ // are what's load-bearing.
709
+ console.warn(`[workspace.routes] dir cleanup skipped for "${filePath}":`, rmErr instanceof Error ? rmErr.message : rmErr);
710
+ }
711
+ // Single tree-refresh signal for the whole batch (we suppressed
712
+ // the per-file ones via `skipFsTreeEvent`).
713
+ eventBus.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
714
+ res.json({ status: 'deleted', count: filesInDir.length });
715
+ return;
716
+ }
717
+ await withLock(id, user, filePath, () => workspaceService.deleteFile(id, filePath));
718
+ res.json({ status: 'deleted' });
719
+ }
720
+ catch (err) {
721
+ sendError(res, err);
722
+ }
723
+ });
724
+ router.patch('/workspace/:id/file', async (req, res) => {
725
+ const id = authenticated(req, res);
726
+ if (id === null)
727
+ return;
728
+ const { oldPath, newPath } = req.body;
729
+ if (!oldPath || !newPath) {
730
+ res.status(400).json({ error: 'oldPath and newPath are required in body' });
731
+ return;
732
+ }
733
+ const user = await requireUser(req, res);
734
+ if (!user)
735
+ return;
736
+ try {
737
+ // Move = rename on disk + commit on both sides. We lock-and-release
738
+ // the destination first (commits the new file's appearance), then
739
+ // lock-and-release the source path (commits its deletion). Two
740
+ // commits rather than one — git's rename detection only kicks in
741
+ // when both delete and create are staged together, and the current
742
+ // `commitFile` is single-path. Lossy w.r.t. rename history, but
743
+ // correct: the file ends up where it should and history shows both
744
+ // ends of the move.
745
+ // Acquire BOTH locks before the move so no other writer can grab
746
+ // the source path under us during the rename window. We hold them
747
+ // simultaneously by nesting `withLock` calls. The lock order is
748
+ // lexical (the same deterministic order used in
749
+ // `LockingFilesystem.moveFile`) so two concurrent moves on the
750
+ // same pair of paths can never deadlock by acquiring in opposite
751
+ // orders. The actual move runs inside the innermost callback;
752
+ // the unwind commits each side's release in reverse order, which
753
+ // produces two single-file commits (one create, one delete) —
754
+ // not a single merge-style rename commit, but git's log/blame
755
+ // rename detection still groups them visually after the fact.
756
+ const [firstLock, secondLock] = oldPath < newPath ? [oldPath, newPath] : [newPath, oldPath];
757
+ await withLock(id, user, firstLock, () => withLock(id, user, secondLock, () => workspaceService.moveEntry(id, oldPath, newPath)));
758
+ res.json({ status: 'moved' });
759
+ }
760
+ catch (err) {
761
+ sendError(res, err);
762
+ }
763
+ });
764
+ router.put('/workspace/:id/file', async (req, res) => {
765
+ const id = authenticated(req, res);
766
+ if (id === null)
767
+ return;
768
+ const filePath = req.query.path;
769
+ if (!filePath) {
770
+ res.status(400).json({ error: 'path query parameter is required' });
771
+ return;
772
+ }
773
+ const { content } = req.body;
774
+ if (content === undefined) {
775
+ res.status(400).json({ error: 'content is required in body' });
776
+ return;
777
+ }
778
+ const user = await requireUser(req, res);
779
+ if (!user)
780
+ return;
781
+ try {
782
+ // Refuse a hand-edit that would leave roles.yaml unparseable BEFORE any
783
+ // byte hits disk — a broken roles.yaml is an app-wide admin lockout
784
+ // (loadModel hard-throws). The dedicated Roles & Members surface has its
785
+ // own validate gate; this covers the raw-text editor path.
786
+ if (isRolesYamlPath(filePath, kbDirName))
787
+ assertRolesYamlParsable(content);
788
+ // Creator read grant: a brand-new file at a spot whose access chain
789
+ // doesn't grant the creator `read` would vanish from their own explorer
790
+ // (read is default-deny). Plan BEFORE the write: a new subtree gets its
791
+ // access.md seeded first; a loose .md carries the grant in its own
792
+ // frontmatter as part of this same single write.
793
+ const plan = await creatorAccess.planForCreate(id, user, filePath, 'file');
794
+ if (plan?.kind === 'seed-access-md')
795
+ await seedCreatorAccessMd(id, user, plan);
796
+ const toWrite = plan?.kind === 'frontmatter' ? plan.apply(content) : content;
797
+ await withLock(id, user, filePath, () => workspaceService.writeFile(id, filePath, toWrite));
798
+ res.json({ status: 'written' });
799
+ }
800
+ catch (err) {
801
+ sendError(res, err);
802
+ }
803
+ });
804
+ router.post('/workspace/:id/directory', async (req, res) => {
805
+ const id = authenticated(req, res);
806
+ if (id === null)
807
+ return;
808
+ const { path: dirPath, defer } = req.body;
809
+ if (!dirPath) {
810
+ res.status(400).json({ error: 'path is required in body' });
811
+ return;
812
+ }
813
+ const user = await requireUser(req, res);
814
+ if (!user)
815
+ return;
816
+ try {
817
+ // Creator read grant: a new folder in an unreadable spot gets its own
818
+ // (or its topmost new ancestor's) access.md seeded FIRST, naming the
819
+ // creator under `read:` — otherwise the default-deny tree filter hides
820
+ // the folder from its own creator the moment it appears. Seeding first
821
+ // also makes the dir non-empty, so the .gitkeep below is skipped and
822
+ // the folder's first committed file IS its access.md.
823
+ const plan = await creatorAccess.planForCreate(id, user, dirPath, 'dir');
824
+ if (plan?.kind === 'seed-access-md')
825
+ await seedCreatorAccessMd(id, user, plan);
826
+ // `createDirectory` makes the dir and, if it ends up empty, drops a
827
+ // `.gitkeep` so git can track the otherwise-untracked folder. The
828
+ // lock-and-release on `<dir>/.gitkeep` commits whatever landed; if
829
+ // the dir was already populated and no .gitkeep was created, the
830
+ // release sees nothing dirty for that path and is a no-op.
831
+ //
832
+ // `defer: true` (folder-upload empty-dir pass) still skips the per-call
833
+ // `fs-tree-changed` so the bulk caller can emit one combined refresh
834
+ // after the burst. Per-file pushes are gone everywhere — the
835
+ // pending-commits worker drains commits out of band — so the only
836
+ // remaining option is the tree-event suppression.
837
+ const gitkeepPath = dirPath.endsWith('/') ? `${dirPath}.gitkeep` : `${dirPath}/.gitkeep`;
838
+ await withLock(id, user, gitkeepPath, async () => {
839
+ await workspaceService.createDirectory(id, dirPath);
840
+ }, defer === true ? { skipFsTreeEvent: true } : undefined);
841
+ res.json({ status: 'created' });
842
+ }
843
+ catch (err) {
844
+ sendError(res, err);
845
+ }
846
+ });
847
+ router.post('/workspace/:id/unzip', async (req, res) => {
848
+ const id = authenticated(req, res);
849
+ if (id === null)
850
+ return;
851
+ const body = (req.body ?? {});
852
+ const zipPath = body.path;
853
+ const destination = body.destination;
854
+ if (typeof zipPath !== 'string' || zipPath.length === 0) {
855
+ res.status(400).json({ error: 'path is required in body and must be a string' });
856
+ return;
857
+ }
858
+ if (destination !== undefined && (typeof destination !== 'string' || destination.length === 0)) {
859
+ res.status(400).json({ error: 'destination, if provided, must be a non-empty string' });
860
+ return;
861
+ }
862
+ const user = await requireUser(req, res);
863
+ if (!user)
864
+ return;
865
+ try {
866
+ // Extract first (all files land on disk), then sweep each extracted
867
+ // file through a lock+release so it commits + pushes as its own
868
+ // one-file change. Per-file commits mean the validator runs N times
869
+ // on large archives — fine for the typical "few files" drop, slow
870
+ // but correct for a 100-file zip. If a single file's release fails
871
+ // (e.g. validator 422), the loop stops there so the user sees the
872
+ // first concrete problem rather than a list of N similar failures.
873
+ const result = await workspaceService.unzipFile(id, zipPath, destination);
874
+ for (const relFile of result.extracted) {
875
+ await withLock(id, user, relFile, async () => {
876
+ // The file is already on disk from the unzip; the release commits +
877
+ // pushes it. If the extraction landed a markdown node the creator
878
+ // can't read (default-deny chains), splice their read grant into
879
+ // its frontmatter first so the committed change already carries it.
880
+ // Best-effort: a failed grant write must not throw — withLock's
881
+ // failure path releases WITHOUT committing and would discard the
882
+ // extracted file from disk, turning a cosmetic grant failure into
883
+ // data loss.
884
+ try {
885
+ const granted = await creatorAccess.grantInExtractedFile(id, user, relFile);
886
+ if (granted !== null)
887
+ await workspaceService.writeFile(id, relFile, granted);
888
+ }
889
+ catch (err) {
890
+ console.warn(`[workspace.routes] creator grant on extracted "${relFile}" failed:`, err instanceof Error ? err.message : err);
891
+ }
892
+ });
893
+ }
894
+ res.json(result);
895
+ }
896
+ catch (err) {
897
+ sendError(res, err);
898
+ }
899
+ });
900
+ router.post('/workspace/:id/upload', async (req, res) => {
901
+ const id = authenticated(req, res);
902
+ if (id === null)
903
+ return;
904
+ const filePath = req.query.path;
905
+ if (!filePath) {
906
+ res.status(400).json({ error: 'path query parameter is required' });
907
+ return;
908
+ }
909
+ // `?defer=true` is the bulk-upload caller's signal to suppress the
910
+ // per-file `fs-tree-changed` SSE — the caller emits one combined
911
+ // refresh at end-of-burst via `POST /flush`. Pushing isn't deferred
912
+ // anywhere anymore; every successful release enqueues a pending-commit
913
+ // row and the worker drains commits + pushes out of band.
914
+ const defer = req.query.defer === 'true';
915
+ const declaredSize = parseInt(req.headers['content-length'] ?? '', 10);
916
+ if (declaredSize > MAX_UPLOAD_BYTES) {
917
+ res.status(413).json({ error: `File exceeds ${MAX_UPLOAD_BYTES} byte limit` });
918
+ return;
919
+ }
920
+ const user = await requireUser(req, res);
921
+ if (!user)
922
+ return;
923
+ try {
924
+ const chunks = [];
925
+ let totalBytes = 0;
926
+ for await (const chunk of req) {
927
+ const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
928
+ totalBytes += buf.length;
929
+ if (totalBytes > MAX_UPLOAD_BYTES) {
930
+ res.status(413).json({ error: `File exceeds ${MAX_UPLOAD_BYTES} byte limit` });
931
+ return;
932
+ }
933
+ chunks.push(buf);
934
+ }
935
+ const data = Buffer.concat(chunks);
936
+ // Creator read grant, mirroring PUT /file: seed a new subtree's
937
+ // access.md first, or fold the grant into an uploaded markdown file's
938
+ // frontmatter. Binary uploads into a pre-existing unreadable folder
939
+ // can't carry a per-file grant (no frontmatter) and proceed ungranted.
940
+ const plan = await creatorAccess.planForCreate(id, user, filePath, 'file');
941
+ if (plan?.kind === 'seed-access-md')
942
+ await seedCreatorAccessMd(id, user, plan);
943
+ const toWrite = plan?.kind === 'frontmatter'
944
+ ? Buffer.from(plan.apply(data.toString('utf8')), 'utf8')
945
+ : data;
946
+ await withLock(id, user, filePath, () => workspaceService.writeFileBinary(id, filePath, toWrite), defer ? { skipFsTreeEvent: true } : undefined);
947
+ res.json({ status: 'uploaded' });
948
+ }
949
+ catch (err) {
950
+ sendError(res, err);
951
+ }
952
+ });
953
+ // `POST /workspace/:id/flush` — end-of-batch checkpoint for folder
954
+ // uploads (and any future batch caller using `defer: true`). Under the
955
+ // pending-commits queue, the "push everything we just committed" job
956
+ // happens out of band in the worker, so this route's only remaining
957
+ // responsibility is to emit the single combined `fs-tree-changed`
958
+ // event the bulk caller suppressed per file.
959
+ router.post('/workspace/:id/flush', async (req, res) => {
960
+ const id = authenticated(req, res);
961
+ if (id === null)
962
+ return;
963
+ const user = await requireUser(req, res);
964
+ if (!user)
965
+ return;
966
+ try {
967
+ const branch = branchForWorkspaceId(id);
968
+ eventBus.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
969
+ res.json({ status: 'flushed' });
970
+ }
971
+ catch (err) {
972
+ sendError(res, err);
973
+ }
974
+ });
975
+ return router;
976
+ }
977
+ /**
978
+ * Enumerate every file (not directory) under `absoluteDir`, returned as
979
+ * workspace-relative POSIX paths. Used by the DELETE handler to decompose
980
+ * a recursive directory delete into per-file lock+release cycles so each
981
+ * deletion lands as its own one-file change.
982
+ *
983
+ * Skips `.git` to avoid trying to commit the internal git index when a
984
+ * caller targets it accidentally. Returns paths in stable lexical order
985
+ * for predictable commit sequencing.
986
+ */
987
+ async function enumerateFilesUnder(absoluteDir, workspaceDir) {
988
+ const out = [];
989
+ async function walk(dir) {
990
+ let entries;
991
+ try {
992
+ entries = await fs.readdir(dir, { withFileTypes: true });
993
+ }
994
+ catch {
995
+ return;
996
+ }
997
+ entries.sort((a, b) => a.name.localeCompare(b.name));
998
+ for (const entry of entries) {
999
+ if (entry.name === '.git' && entry.isDirectory())
1000
+ continue;
1001
+ const child = path.join(dir, entry.name);
1002
+ if (entry.isDirectory()) {
1003
+ await walk(child);
1004
+ }
1005
+ else {
1006
+ out.push(path.relative(workspaceDir, child).replace(/\\/g, '/'));
1007
+ }
1008
+ }
1009
+ }
1010
+ await walk(absoluteDir);
1011
+ return out;
1012
+ }
1013
+ /**
1014
+ * Recursively remove empty directories under `absoluteDir`, bottom-up, then
1015
+ * remove `absoluteDir` itself if it ends up empty. A directory is removed only
1016
+ * if it contains nothing at the moment it's visited, so any file a concurrent
1017
+ * writer dropped in mid-delete — and every parent directory on its path —
1018
+ * survives. `.git` is left alone. Used after a recursive folder delete to
1019
+ * sweep the leftover empty-folder shells off disk so the file tree (which
1020
+ * lists on-disk directories, not just tracked files) stops showing the
1021
+ * deleted container.
1022
+ */
1023
+ async function removeEmptyDirs(absoluteDir) {
1024
+ let entries;
1025
+ try {
1026
+ entries = await fs.readdir(absoluteDir, { withFileTypes: true });
1027
+ }
1028
+ catch {
1029
+ // Already gone (raced delete) — nothing to do.
1030
+ return;
1031
+ }
1032
+ for (const entry of entries) {
1033
+ if (entry.name === '.git' && entry.isDirectory())
1034
+ continue;
1035
+ if (entry.isDirectory()) {
1036
+ await removeEmptyDirs(path.join(absoluteDir, entry.name));
1037
+ }
1038
+ }
1039
+ // Re-read after pruning children: a subdir we just emptied now lets this
1040
+ // dir become removable too. Any surviving file (or `.git`) keeps it.
1041
+ const remaining = await fs.readdir(absoluteDir);
1042
+ if (remaining.length === 0) {
1043
+ await fs.rmdir(absoluteDir);
1044
+ }
1045
+ }
1046
+ //# sourceMappingURL=workspace.routes.js.map