@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,894 @@
1
+ import express from 'express';
2
+ import type { AuthUser } from '@bevel-software/platform-shared';
3
+ import { isProtectedBranch, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
4
+ import type {
5
+ IAccessControl,
6
+ GrantPrincipal,
7
+ GrantSources,
8
+ } from './access-control.interface.js';
9
+ import type { WorkspaceService } from '../workspace/workspace.service.js';
10
+ import { branchForWorkspaceId, workspaceIdForBranch } from '../workspace/workspace.service.js';
11
+ import type { AuthService } from '../auth/auth.service.js';
12
+ import type { WorkflowService } from '../workflow/workflow.service.js';
13
+ import type { WorkflowEventBus } from '../workflow/event-bus.js';
14
+ import { WorkflowDomainError } from '../workflow/workflow.errors.js';
15
+ import { AccessDeniedError } from './access-errors.js';
16
+ import {
17
+ AccessMutationService,
18
+ AccessMutationError,
19
+ accessMdPathForFolder,
20
+ type TargetKind,
21
+ } from './access-mutation.service.js';
22
+ import { canonicalRoleName, EVERYONE_CANONICAL, type Verb } from './access-control.service.js';
23
+ import { RolesAdminService, RolesAdminError } from './roles-admin.service.js';
24
+ import type { Principal } from './access-splice.js';
25
+ import type { Database } from '../database/connection.js';
26
+ import { users } from '../database/schema.js';
27
+ import '../auth/auth.middleware.js';
28
+
29
+ /** Verbs the share UI may grant. Verbs are independent — `download` is grantable on its own. */
30
+ const GRANTABLE_VERBS = new Set<string>(['read', 'write', 'owner', 'download']);
31
+
32
+ function toHttpError(
33
+ err: unknown,
34
+ ): { status: number; body: Record<string, unknown> } {
35
+ if (err instanceof WorkflowDomainError) {
36
+ return { status: err.status, body: { error: err.message, ...(err.payload ?? {}) } };
37
+ }
38
+ const message = err instanceof Error ? err.message : 'Unknown error';
39
+ return { status: 500, body: { error: message } };
40
+ }
41
+
42
+ /**
43
+ * Coerce a request-body field that must be a non-empty string. Rejects arrays,
44
+ * objects, numbers, null — anything `String()` would silently stringify into a
45
+ * bogus role name / email (`[object Object]`, `"a,b"`) — with a 400 instead of
46
+ * persisting garbage. Returns the raw string (untrimmed; the service trims).
47
+ */
48
+ function requireNonEmptyString(value: unknown, field: string): string {
49
+ if (typeof value !== 'string' || value.trim() === '') {
50
+ throw new RolesAdminError(`${field} must be a non-empty string`, 400);
51
+ }
52
+ return value;
53
+ }
54
+
55
+ export function createAccessRoutes(
56
+ accessControl: IAccessControl,
57
+ workspaceService: WorkspaceService,
58
+ authService: AuthService,
59
+ workflowService: WorkflowService,
60
+ eventBus: WorkflowEventBus,
61
+ db: Database,
62
+ kbDirName: string,
63
+ ): express.Router {
64
+ const router = express.Router({ mergeParams: true });
65
+ const mutation = new AccessMutationService(workspaceService, accessControl, kbDirName);
66
+ const rolesAdmin = new RolesAdminService(
67
+ workspaceService,
68
+ workflowService,
69
+ accessControl,
70
+ kbDirName,
71
+ DEFAULT_BRANCH,
72
+ eventBus,
73
+ );
74
+
75
+ // Roles (groups) are authoritative on the DEFAULT branch — the Roles admin
76
+ // screen only ever edits roles.yaml there. Resolve the group list from the
77
+ // default-branch workspace so the share dialog suggests (and the grant route
78
+ // accepts) the same roles the Roles screen manages, regardless of which branch
79
+ // the caller is viewing. People stay branch-local (the caller's own workspace).
80
+ const defaultBranchGroups = async (): Promise<string[]> => {
81
+ await workspaceService.getOrCreateForBranch(DEFAULT_BRANCH);
82
+ const { groups } = await accessControl.kbPrincipals(workspaceIdForBranch(DEFAULT_BRANCH));
83
+ return groups;
84
+ };
85
+
86
+ // Authentication gate only. Per PLAN §3 the workspace `:id` is a branch
87
+ // identifier — any authenticated user can read its access tree.
88
+ async function requireUser(
89
+ req: express.Request,
90
+ res: express.Response,
91
+ ): Promise<AuthUser | null> {
92
+ if (!req.userId) {
93
+ res.status(401).json({ error: 'Unauthenticated' });
94
+ return null;
95
+ }
96
+ try {
97
+ const user = await authService.getUserById(req.userId);
98
+ if (!user) {
99
+ res.status(401).json({ error: 'User not found' });
100
+ return null;
101
+ }
102
+ return user;
103
+ } catch (err) {
104
+ const { status, body } = toHttpError(err);
105
+ res.status(status).json(body);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Strip the `<kbDirName>/` prefix the frontend sends (workspace-relative) to
112
+ * the repo-relative form the access model speaks. Mirrors
113
+ * `assertCanWriteAtPath`'s strip. Distinguish by CALLER (the frontend always
114
+ * sends workspace-relative), not by string content — see the
115
+ * `access-path-mismatch` learning.
116
+ */
117
+ function toRepoRelative(p: string): string {
118
+ const prefix = `${kbDirName}/`;
119
+ if (p === kbDirName) return '';
120
+ return p.startsWith(prefix) ? p.slice(prefix.length) : p;
121
+ }
122
+
123
+ /**
124
+ * Reject a repo-relative target that could escape the KB repo once joined.
125
+ * `toRepoRelative` only strips the prefix, so `${kbDirName}/../tmp` survives as
126
+ * `../tmp` — inside the workspace but outside the KB. Refuse `..` segments,
127
+ * backslashes, absolute paths, and NULs before any lock/edit-path derivation.
128
+ */
129
+ function assertRepoRelativeTarget(repoRelTarget: string, kind: TargetKind): void {
130
+ if (kind === 'file' && !repoRelTarget) {
131
+ throw new AccessMutationError('file path is required');
132
+ }
133
+ if (
134
+ repoRelTarget.startsWith('/') ||
135
+ repoRelTarget.includes('\\') ||
136
+ repoRelTarget.includes('\0') ||
137
+ repoRelTarget.split('/').some((segment) => segment === '..')
138
+ ) {
139
+ throw new AccessMutationError('path must stay inside the KB repo');
140
+ }
141
+ }
142
+
143
+ /**
144
+ * GET /api/workspace/:id/access?path=<relativePath>&kind=<folder|file>
145
+ * Returns the resolved access view for the current user at a single path,
146
+ * including a per-principal `sources` map (where each principal's access comes
147
+ * from) so the dialog can show inherited-vs-direct. `kind` defaults to `file`
148
+ * (the resolver treats a folder vs a file's own scope differently only for the
149
+ * `sources` direct/ancestor split; the eligible/verdict fields are identical).
150
+ */
151
+ router.get('/workspace/:id/access', async (req, res) => {
152
+ const user = await requireUser(req, res);
153
+ if (!user) return;
154
+
155
+ const rawPath = req.query.path;
156
+ if (typeof rawPath !== 'string' || !rawPath) {
157
+ res.status(400).json({ error: 'path query parameter is required' });
158
+ return;
159
+ }
160
+ const kind: TargetKind = req.query.kind === 'folder' ? 'folder' : 'file';
161
+
162
+ try {
163
+ // Same sanitize/validate the POST routes apply: strip any kbDir prefix and
164
+ // refuse paths that escape the KB repo before handing to the resolver.
165
+ const repoRelTarget = toRepoRelative(rawPath);
166
+ assertRepoRelativeTarget(repoRelTarget, kind);
167
+ res.json(await resolvedView(req.params.id, repoRelTarget, user.email, kind));
168
+ } catch (err) {
169
+ const { status, body } = toHttpError(err);
170
+ res.status(status).json(body);
171
+ }
172
+ });
173
+
174
+ /**
175
+ * POST /api/workspace/:id/access/batch
176
+ * Body: `{ paths: string[] }`. Returns `{ results: { [path]: boolean } }`.
177
+ */
178
+ router.post('/workspace/:id/access/batch', async (req, res) => {
179
+ const user = await requireUser(req, res);
180
+ if (!user) return;
181
+
182
+ const paths = (req.body as { paths?: unknown }).paths;
183
+ if (!Array.isArray(paths) || paths.some((p) => typeof p !== 'string')) {
184
+ res.status(400).json({ error: 'paths must be an array of strings' });
185
+ return;
186
+ }
187
+ if (paths.length > 500) {
188
+ res.status(400).json({ error: 'paths cannot exceed 500 entries per request' });
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const result = await accessControl.canWriteBatch(
194
+ req.params.id,
195
+ user.email,
196
+ paths as string[],
197
+ );
198
+ const obj: Record<string, boolean> = {};
199
+ for (const [k, v] of result) obj[k] = v;
200
+ res.json({ results: obj });
201
+ } catch (err) {
202
+ const { status, body } = toHttpError(err);
203
+ res.status(status).json(body);
204
+ }
205
+ });
206
+
207
+ /**
208
+ * GET /api/workspace/:id/access/suggest?q=<query>
209
+ * Autocomplete for the share dialog. Returns matching groups (roles.yaml role
210
+ * names — resolved from the DEFAULT branch, the authoritative roles source)
211
+ * and people (branch-local to `:id`). Groups are small + non-sensitive so they
212
+ * always show;
213
+ * PEOPLE are withheld until `q` is ≥ 2 chars, so an empty query can't dump the
214
+ * whole directory (email-harvesting guard). People are the union of the
215
+ * KB-canonical set (roles.yaml + access.md grants) and the `users` table
216
+ * (logged-in users). Results are capped.
217
+ */
218
+ router.get('/workspace/:id/access/suggest', async (req, res) => {
219
+ const user = await requireUser(req, res);
220
+ if (!user) return;
221
+
222
+ const q = (typeof req.query.q === 'string' ? req.query.q : '').trim().toLowerCase();
223
+ const CAP = 15;
224
+ try {
225
+ const groups = await defaultBranchGroups();
226
+ const { people: kbPeople } = await accessControl.kbPrincipals(req.params.id);
227
+
228
+ const matchedGroups = groups
229
+ .filter((g) => !q || g.toLowerCase().includes(q))
230
+ .slice(0, CAP);
231
+
232
+ let people: { name: string; email: string }[] = [];
233
+ if (q.length >= 2) {
234
+ // Union the KB-canonical people with the login-only users table.
235
+ const byEmail = new Map<string, { name: string; email: string }>();
236
+ for (const p of kbPeople) byEmail.set(p.email.toLowerCase(), p);
237
+ const userRows = await db.select().from(users);
238
+ for (const u of userRows) {
239
+ const key = u.email.toLowerCase();
240
+ // Prefer a real display name over the email local-part default: only
241
+ // overwrite when the existing name still looks email-like and we have
242
+ // a real one to replace it with.
243
+ const existing = byEmail.get(key);
244
+ if (!existing || (existing.name.includes('@') && u.name)) {
245
+ byEmail.set(key, { name: u.name || u.email, email: u.email });
246
+ }
247
+ }
248
+ people = [...byEmail.values()]
249
+ .filter((p) => p.email.toLowerCase().includes(q) || p.name.toLowerCase().includes(q))
250
+ .slice(0, CAP);
251
+ }
252
+
253
+ res.json({ groups: matchedGroups, people, peopleWithheld: q.length < 2 });
254
+ } catch (err) {
255
+ const { status, body } = toHttpError(err);
256
+ res.status(status).json(body);
257
+ }
258
+ });
259
+
260
+ /**
261
+ * Parse + validate a grant/revoke request body into a canonical Principal plus
262
+ * the target kind and repo-relative path. Throws AccessMutationError on bad input.
263
+ */
264
+ function parseMutationBody(body: unknown): {
265
+ repoRelTarget: string;
266
+ kind: TargetKind;
267
+ principal: Principal;
268
+ } {
269
+ const b = (body ?? {}) as Record<string, unknown>;
270
+ const rawPath = b.path;
271
+ const kind = b.kind;
272
+ const principalRaw = b.principal as Record<string, unknown> | undefined;
273
+ if (typeof rawPath !== 'string' || !rawPath) {
274
+ throw new AccessMutationError('path is required');
275
+ }
276
+ if (kind !== 'folder' && kind !== 'file') {
277
+ throw new AccessMutationError("kind must be 'folder' or 'file'");
278
+ }
279
+ if (!principalRaw || typeof principalRaw !== 'object') {
280
+ throw new AccessMutationError('principal is required');
281
+ }
282
+ let principal: Principal;
283
+ if (principalRaw.kind === 'user') {
284
+ if (typeof principalRaw.email !== 'string' || typeof principalRaw.displayName !== 'string') {
285
+ throw new AccessMutationError('user principal needs email + displayName');
286
+ }
287
+ principal = {
288
+ kind: 'user',
289
+ email: principalRaw.email,
290
+ displayName: principalRaw.displayName,
291
+ };
292
+ } else if (principalRaw.kind === 'role') {
293
+ if (typeof principalRaw.role !== 'string') {
294
+ throw new AccessMutationError('role principal needs a role name');
295
+ }
296
+ principal = { kind: 'role', role: principalRaw.role };
297
+ } else {
298
+ throw new AccessMutationError("principal.kind must be 'user' or 'role'");
299
+ }
300
+ const targetKind = kind as TargetKind;
301
+ const repoRelTarget = toRepoRelative(rawPath);
302
+ assertRepoRelativeTarget(repoRelTarget, targetKind);
303
+ return { repoRelTarget, kind: targetKind, principal };
304
+ }
305
+
306
+ /**
307
+ * The access.md / node path the write lands on, and the path the write-gate
308
+ * keys on. For a folder both are the folder's access.md; for a file they are
309
+ * the node file itself.
310
+ */
311
+ function gateAndEditPaths(kind: TargetKind, repoRelTarget: string): {
312
+ gatePath: string;
313
+ editPath: string;
314
+ } {
315
+ const editPath = kind === 'folder' ? accessMdPathForFolder(repoRelTarget) : repoRelTarget;
316
+ return { gatePath: editPath, editPath };
317
+ }
318
+
319
+ /**
320
+ * Fail-closed authorization for a mutation. Mirrors the resolver's special
321
+ * casing but NEVER default-allows: the lock-acquire gate (assertCanWriteAtPath)
322
+ * default-ALLOWS when no config resolves at HEAD on a protected branch — a
323
+ * privilege hole at exactly the bootstrap state this feature first hits. Here
324
+ * we require an explicit, resolved write permission on the access-config path,
325
+ * and on a protected branch additionally require the caller to be writable
326
+ * per the resolver (admins via rescue, or an explicit grant). Absent/
327
+ * unparseable config → AccessConfigError surfaces (not silent allow).
328
+ */
329
+ async function assertCanMutate(
330
+ workspaceId: string,
331
+ branch: string,
332
+ userEmail: string,
333
+ gatePath: string,
334
+ ): Promise<void> {
335
+ const writable = await accessControl.canWrite(workspaceId, userEmail, gatePath);
336
+ if (!writable) {
337
+ const eligible = await accessControl.eligibleWriters(workspaceId, gatePath);
338
+ throw new AccessDeniedError({
339
+ path: gatePath,
340
+ eligibleRoles: eligible.roles,
341
+ eligibleUsers: eligible.users,
342
+ });
343
+ }
344
+ // On a protected branch, write is admin-only for access.md/roles.yaml. The
345
+ // canWrite check above already encodes that (admin-rescue), so a non-admin
346
+ // resolves to false and is rejected above. Nothing extra needed here — but
347
+ // keep the branch check explicit so the intent is legible.
348
+ void branch;
349
+ void isProtectedBranch;
350
+ }
351
+
352
+ /** The full resolved-access view returned after a successful mutation. */
353
+ async function resolvedView(
354
+ workspaceId: string,
355
+ repoRelTarget: string,
356
+ userEmail: string,
357
+ kind: TargetKind,
358
+ ) {
359
+ const [canRead, canWrite, canDownload, canOwner, eligible, readers, owners, downloaders] =
360
+ await Promise.all([
361
+ accessControl.canRead(workspaceId, userEmail, repoRelTarget),
362
+ accessControl.canWrite(workspaceId, userEmail, repoRelTarget),
363
+ accessControl.canDownload(workspaceId, userEmail, repoRelTarget),
364
+ accessControl.canOwner(workspaceId, userEmail, repoRelTarget),
365
+ accessControl.eligibleWriters(workspaceId, repoRelTarget),
366
+ accessControl.eligibleReaders(workspaceId, repoRelTarget),
367
+ accessControl.eligibleOwners(workspaceId, repoRelTarget),
368
+ accessControl.eligibleDownloaders(workspaceId, repoRelTarget),
369
+ ]);
370
+
371
+ // Per-principal, per-verb origin (direct / ancestor — MECE over editable
372
+ // files). Keyed `u:<email>` / `r:<role>` to match the dialog's row keys, so
373
+ // each row can show where its access comes from and which verbs are
374
+ // removable here. A row whose verbs resolve only via a group/everyone/rescue
375
+ // has no source (the verb is absent) and renders non-actionable. Built over
376
+ // the union of every principal in the four eligible lists.
377
+ const roleSet = new Set<string>([
378
+ ...eligible.roles,
379
+ ...readers.roles,
380
+ ...owners.roles,
381
+ ...downloaders.roles,
382
+ ]);
383
+ const userSet = new Map<string, { name: string; email: string }>();
384
+ for (const u of [...eligible.users, ...readers.users, ...owners.users, ...downloaders.users]) {
385
+ if (!userSet.has(u.email.toLowerCase())) userSet.set(u.email.toLowerCase(), u);
386
+ }
387
+ const sources: Record<string, Awaited<ReturnType<IAccessControl['grantSources']>>> = {};
388
+ await Promise.all([
389
+ ...[...roleSet].map(async (role) => {
390
+ sources[`r:${role.toLowerCase()}`] = await accessControl.grantSources(
391
+ workspaceId,
392
+ kind,
393
+ repoRelTarget,
394
+ { kind: 'role', role },
395
+ );
396
+ }),
397
+ ...[...userSet.values()].map(async (u) => {
398
+ sources[`u:${u.email.toLowerCase()}`] = await accessControl.grantSources(
399
+ workspaceId,
400
+ kind,
401
+ repoRelTarget,
402
+ { kind: 'user', email: u.email },
403
+ );
404
+ }),
405
+ ]);
406
+
407
+ return { canRead, canWrite, canDownload, canOwner, eligible, readers, owners, downloaders, sources };
408
+ }
409
+
410
+ /**
411
+ * Run a mutation under the edit-file lock: acquire → op (mutation writes
412
+ * under the lock) → release (enqueues the commit + push out of band). Mirrors
413
+ * the human-save `withLock` so protected-branch enforcement + commit-as-user
414
+ * are inherited. On op failure, release WITHOUT committing partial bytes.
415
+ */
416
+ async function withEditLock(
417
+ workspaceId: string,
418
+ branch: string,
419
+ editPath: string,
420
+ user: AuthUser,
421
+ op: () => Promise<void>,
422
+ ): Promise<void> {
423
+ const wsEditPath = `${kbDirName}/${editPath}`;
424
+ const existing = await workflowService.getLock(workspaceId, branch, wsEditPath);
425
+ if (existing && existing.holderUserId === user.id) {
426
+ // Caller already holds the lock (mid-edit) — write without touching it.
427
+ await op();
428
+ eventBus.emit({
429
+ kind: 'file-changed',
430
+ workspaceId,
431
+ branch,
432
+ path: wsEditPath,
433
+ newSha: null,
434
+ byUserId: user.id,
435
+ byUserName: user.name,
436
+ });
437
+ eventBus.emit({ kind: 'fs-tree-changed', workspaceId, branch });
438
+ return;
439
+ }
440
+ const acquired = await workflowService.acquireLock(workspaceId, branch, wsEditPath, user);
441
+ if (!acquired.acquired) {
442
+ const holder = acquired.lock.holderName || 'another user';
443
+ const err: Error & { status?: number } = new Error(
444
+ `"${editPath}" is being edited by ${holder}. Try again in a moment.`,
445
+ );
446
+ err.status = 409;
447
+ throw err;
448
+ }
449
+ try {
450
+ await op();
451
+ } catch (err) {
452
+ try {
453
+ await workflowService.releaseLockNoCommit(workspaceId, branch, wsEditPath, user);
454
+ } catch {
455
+ /* best-effort */
456
+ }
457
+ throw err;
458
+ }
459
+ await workflowService.releaseLock(workspaceId, branch, wsEditPath, user);
460
+ eventBus.emit({ kind: 'fs-tree-changed', workspaceId, branch });
461
+ }
462
+
463
+ /**
464
+ * POST /api/workspace/:id/access/grant
465
+ * Body: `{ path, kind: 'folder'|'file', verb: 'write'|'owner', principal }`.
466
+ * Grants the principal the verb on the target (folder → its access.md, file →
467
+ * its own frontmatter). Gated on write to the access config (fail-closed on
468
+ * protected branches). Returns the fresh resolved access for the target.
469
+ */
470
+ router.post('/workspace/:id/access/grant', async (req, res) => {
471
+ const user = await requireUser(req, res);
472
+ if (!user) return;
473
+ const workspaceId = req.params.id;
474
+ const branch = branchForWorkspaceId(workspaceId);
475
+ try {
476
+ const verb = (req.body as { verb?: unknown }).verb;
477
+ if (typeof verb !== 'string' || !GRANTABLE_VERBS.has(verb)) {
478
+ throw new AccessMutationError(
479
+ `verb must be one of: ${[...GRANTABLE_VERBS].join(', ')}`,
480
+ );
481
+ }
482
+ const { repoRelTarget, kind, principal } = parseMutationBody(req.body);
483
+
484
+ // For a group grant, the role must exist in roles.yaml (Slice 1 grants
485
+ // existing groups only; Create Group is Slice 2). Reject loudly otherwise.
486
+ // Validate against the DEFAULT-branch roles (same authoritative source the
487
+ // suggest autocomplete uses) so a default-branch role can't be suggested
488
+ // then rejected here on a feature branch whose roles.yaml has diverged.
489
+ if (principal.kind === 'role') {
490
+ const groups = await defaultBranchGroups();
491
+ const known = groups.some((g) => canonicalRoleName(g) === canonicalRoleName(principal.role));
492
+ if (!known) {
493
+ throw new AccessMutationError(
494
+ `No group named "${principal.role}". Pick an existing group, or ask an admin to create it.`,
495
+ 404,
496
+ { kind: 'unknown-group', role: principal.role },
497
+ );
498
+ }
499
+ // The built-in `everyone` role is grantable from the share UI for READ
500
+ // only (public read). Write/owner/download to everyone would make the
501
+ // node world-writable from a casual dialog, so those stay a direct
502
+ // access.md edit.
503
+ if (canonicalRoleName(principal.role) === EVERYONE_CANONICAL && verb !== 'read') {
504
+ throw new AccessMutationError(
505
+ '"Everyone" can only be granted read access (public). Grant other access levels to specific people or groups.',
506
+ );
507
+ }
508
+ }
509
+
510
+ const { gatePath } = gateAndEditPaths(kind, repoRelTarget);
511
+ // Ensure the branch workspace exists before locking.
512
+ await workspaceService.getOrCreateForBranch(branch);
513
+ // Fail-closed gate (BEFORE acquiring the lock).
514
+ await assertCanMutate(workspaceId, branch, user.email, gatePath);
515
+
516
+ const editPath = gatePath; // grant edits the same path it gates on
517
+ await withEditLock(workspaceId, branch, editPath, user, async () => {
518
+ await mutation.grant(workspaceId, kind, repoRelTarget, verb as Verb, principal);
519
+ });
520
+
521
+ // The write landed on disk under the lock; drop the cache so the
522
+ // re-resolve reads the just-written bytes (the async commit's own
523
+ // invalidate is too late for this synchronous response).
524
+ accessControl.invalidate(workspaceId);
525
+ console.log(
526
+ `[access.grant] ws=${workspaceId} branch=${branch} byUserId=${user.id} ` +
527
+ `verb=${verb} kind=${kind} target=${repoRelTarget} ` +
528
+ `principalKind=${principal.kind} -> ok`,
529
+ );
530
+ res.json(await resolvedView(workspaceId, repoRelTarget, user.email, kind));
531
+ } catch (err) {
532
+ if (err instanceof AccessDeniedError) {
533
+ console.warn(
534
+ `[access.grant.denied] ws=${workspaceId} byUserId=${user.id} -> 403`,
535
+ );
536
+ }
537
+ const { status, body } = toHttpError(err);
538
+ res.status(status).json(body);
539
+ }
540
+ });
541
+
542
+ /**
543
+ * POST /api/workspace/:id/access/revoke
544
+ *
545
+ * Three behaviors selected by an optional `mode`:
546
+ *
547
+ * (default) Revoke on the TARGET. Classified by where the principal's
548
+ * access comes from:
549
+ * - direct on the target → remove the entry, 200 fresh view.
550
+ * - inherited from an ancestor (target splice would no-op but the
551
+ * principal still resolves) → 409 `{ kind: 'inherited', sources }`
552
+ * so the dialog can offer remove-from-parent or deny-here. We do NOT
553
+ * silently return an unchanged 200.
554
+ * - no effective access anywhere → 200 no-op.
555
+ * `verb` (optional) narrows a direct revoke to one verb.
556
+ *
557
+ * `mode: 'remove-from-parent'` — cascade up. Body adds `ancestor` (the
558
+ * repo-relative ancestor folder, echoed opaquely from the 409 sources).
559
+ * Gates + locks on the ANCESTOR's access.md, re-validates under the lock
560
+ * that the principal is still inherited from it, then revokes there.
561
+ *
562
+ * `mode: 'deny-here'` — per-item override. Adds a `deny <principal>` at the
563
+ * target (strip-then-deny + effectiveness assert in the mutation).
564
+ *
565
+ * Revoke is unconditional — no last-owner / self-lockout guard.
566
+ */
567
+ router.post('/workspace/:id/access/revoke', async (req, res) => {
568
+ const user = await requireUser(req, res);
569
+ if (!user) return;
570
+ const workspaceId = req.params.id;
571
+ const branch = branchForWorkspaceId(workspaceId);
572
+ try {
573
+ const mode = (req.body as { mode?: unknown }).mode;
574
+ const { repoRelTarget, kind, principal } = parseMutationBody(req.body);
575
+ // Optional `verb` scopes ALL three paths to a single verb: a default revoke
576
+ // strips just that verb; remove-from-parent removes just that verb on the
577
+ // ancestor; deny-here denies just that verb at the target. Absent ⇒ the
578
+ // whole principal (every verb). Shared by every mode so the share dialog's
579
+ // per-checkbox toggle and its whole-row Remove both work.
580
+ const rawVerb = (req.body as { verb?: unknown }).verb;
581
+ let verb: Verb | undefined;
582
+ if (rawVerb !== undefined) {
583
+ if (typeof rawVerb !== 'string' || !GRANTABLE_VERBS.has(rawVerb)) {
584
+ throw new AccessMutationError(`verb must be one of: ${[...GRANTABLE_VERBS].join(', ')}`);
585
+ }
586
+ verb = rawVerb as Verb;
587
+ }
588
+ await workspaceService.getOrCreateForBranch(branch);
589
+
590
+ // ---- mode: remove-from-parent (cascade up the ancestor) --------------
591
+ if (mode === 'remove-from-parent') {
592
+ const rawAncestor = (req.body as { ancestor?: unknown }).ancestor;
593
+ if (typeof rawAncestor !== 'string' || !rawAncestor) {
594
+ throw new AccessMutationError("'ancestor' is required for remove-from-parent");
595
+ }
596
+ // The ancestor is the repo-relative FOLDER whose access.md grants the
597
+ // principal — we revoke against it as a folder target. Strip the kbDir
598
+ // prefix if the client (defensively) sent a prefixed path.
599
+ const ancestorDir = toRepoRelative(rawAncestor);
600
+ assertRepoRelativeTarget(ancestorDir, 'folder');
601
+ const { gatePath: ancestorGate } = gateAndEditPaths('folder', ancestorDir);
602
+ // Gate on the ANCESTOR's access.md, not the target.
603
+ await assertCanMutate(workspaceId, branch, user.email, ancestorGate);
604
+
605
+ await withEditLock(workspaceId, branch, ancestorGate, user, async () => {
606
+ // Re-validate UNDER the lock: the principal must STILL be inherited
607
+ // from this exact ancestor (the chain may have changed since the 409).
608
+ const sources = await accessControl.grantSources(
609
+ workspaceId,
610
+ kind,
611
+ repoRelTarget,
612
+ toGrantPrincipal(principal),
613
+ );
614
+ const ancestorMd = accessMdPathForFolder(ancestorDir);
615
+ // Scope the recheck to the verb the user acted on (if any): an
616
+ // inherited grant of a DIFFERENT verb must not keep a verb-scoped
617
+ // revoke alive on this ancestor.
618
+ const listsToCheck = verb ? [sources[verb]] : Object.values(sources);
619
+ const inheritedHere = listsToCheck.some((list) =>
620
+ (list ?? []).some((s) => s.kind === 'ancestor' && s.path === ancestorMd),
621
+ );
622
+ if (!inheritedHere) {
623
+ throw new AccessMutationError(
624
+ 'This person is no longer inherited from that folder — refresh and try again.',
625
+ 409,
626
+ { kind: 'stale-ancestor' },
627
+ );
628
+ }
629
+ // Scope the ancestor revoke to the same verb the user acted on (if any).
630
+ await mutation.revoke(workspaceId, 'folder', ancestorDir, principal, user.email, verb);
631
+ });
632
+
633
+ accessControl.invalidate(workspaceId);
634
+ console.log(
635
+ `[access.revoke.remove-from-parent] ws=${workspaceId} branch=${branch} ` +
636
+ `byUserId=${user.id} ancestor=${ancestorDir} target=${repoRelTarget} verb=${verb ?? 'all'} -> ok`,
637
+ );
638
+ res.json(await resolvedView(workspaceId, repoRelTarget, user.email, kind));
639
+ return;
640
+ }
641
+
642
+ // ---- mode: deny-here (per-item override) -----------------------------
643
+ if (mode === 'deny-here') {
644
+ const { gatePath } = gateAndEditPaths(kind, repoRelTarget);
645
+ await assertCanMutate(workspaceId, branch, user.email, gatePath);
646
+ await withEditLock(workspaceId, branch, gatePath, user, async () => {
647
+ // Scope the deny to the same verb the user acted on (if any).
648
+ await mutation.denyHere(workspaceId, kind, repoRelTarget, principal, verb);
649
+ });
650
+ accessControl.invalidate(workspaceId);
651
+ console.log(
652
+ `[access.revoke.deny-here] ws=${workspaceId} branch=${branch} ` +
653
+ `byUserId=${user.id} target=${repoRelTarget} verb=${verb ?? 'all'} -> ok`,
654
+ );
655
+ res.json(await resolvedView(workspaceId, repoRelTarget, user.email, kind));
656
+ return;
657
+ }
658
+
659
+ // ---- default: revoke on the target, classified -----------------------
660
+ const { gatePath } = gateAndEditPaths(kind, repoRelTarget);
661
+ await assertCanMutate(workspaceId, branch, user.email, gatePath);
662
+
663
+ const editPath = gatePath;
664
+ let changed = false;
665
+ await withEditLock(workspaceId, branch, editPath, user, async () => {
666
+ const r = await mutation.revoke(workspaceId, kind, repoRelTarget, principal, user.email, verb);
667
+ changed = r.changed;
668
+ });
669
+
670
+ accessControl.invalidate(workspaceId);
671
+
672
+ // The target splice changed nothing. `grantSources` is MECE (direct |
673
+ // ancestor); a no-op splice means no `direct` entry existed to remove, so
674
+ // any source left is an `ancestor`. Distinguish "inherited" (still named in
675
+ // an ancestor access.md — offer cascade/deny) from "no removable source
676
+ // here" (nothing named in any file we can edit — a genuine no-op). A
677
+ // principal who only RESOLVES via a group / everyone / admin-rescue has no
678
+ // source at all (it's not their own file entry) and so is correctly a 200
679
+ // no-op: there is nothing to remove here and no Remove was ever offered for
680
+ // them in the dialog. NEVER a silent unchanged-success when there IS a
681
+ // removable inherited entry.
682
+ if (!changed) {
683
+ const sources = await accessControl.grantSources(
684
+ workspaceId,
685
+ kind,
686
+ repoRelTarget,
687
+ toGrantPrincipal(principal),
688
+ );
689
+ // Only an inherited source for the verb being revoked (if any) should
690
+ // trigger the cascade/deny prompt — a different verb's inherited grant
691
+ // is not what this revoke is trying to remove.
692
+ const sourcesToCheck: GrantSources = verb ? { [verb]: sources[verb] } : sources;
693
+ const inherited = collectInheritedSources(sourcesToCheck);
694
+ if (inherited.length > 0) {
695
+ console.log(
696
+ `[access.revoke.inherited] ws=${workspaceId} branch=${branch} byUserId=${user.id} ` +
697
+ `target=${repoRelTarget} ancestors=${inherited.join(',')}`,
698
+ );
699
+ res.status(409).json({
700
+ error:
701
+ "This person's access comes from a parent folder. Remove it there, or restrict just this item.",
702
+ kind: 'inherited',
703
+ sources,
704
+ });
705
+ return;
706
+ }
707
+ // else: genuine no-op, fall through to the 200 fresh view.
708
+ }
709
+
710
+ // The fresh view's per-row `sources` map carries the post-revoke origins
711
+ // for EVERY principal still resolving — including one whose direct entry we
712
+ // just stripped but who remains inherited from an ancestor (their row now
713
+ // shows an `ancestor` source). The dialog reads that to chain into
714
+ // "Remove from parent?" with no extra response field needed (the old
715
+ // `stillInherited` shortcut is subsumed by the richer per-verb sources).
716
+ console.log(
717
+ `[access.revoke] ws=${workspaceId} branch=${branch} byUserId=${user.id} ` +
718
+ `kind=${kind} target=${repoRelTarget} principalKind=${principal.kind} changed=${changed} -> ok`,
719
+ );
720
+ res.json(await resolvedView(workspaceId, repoRelTarget, user.email, kind));
721
+ } catch (err) {
722
+ if (err instanceof AccessDeniedError || err instanceof AccessMutationError) {
723
+ console.warn(
724
+ `[access.revoke.denied] ws=${workspaceId} byUserId=${user.id} -> ${
725
+ err instanceof WorkflowDomainError ? err.status : 500
726
+ }`,
727
+ );
728
+ }
729
+ const { status, body } = toHttpError(err);
730
+ res.status(status).json(body);
731
+ }
732
+ });
733
+
734
+ // -------------------------------------------------------------------------
735
+ // Roles & Members (admin) — full CRUD on the DEFAULT-branch roles.yaml.
736
+ // Routes are GLOBAL (no `:id`): the handler resolves the default-branch
737
+ // workspace internally, because that is the file admin status derives from.
738
+ // Mutating routes are admin-gated by `assertRolesAdmin` (the same admin-only
739
+ // `canWrite('roles.yaml')` check the rest of the app uses); the service's
740
+ // lock acquisition re-enforces it at HEAD. Path prefix is `/access/roles`,
741
+ // NOT `/admin/roles` — the access router owns roles.yaml mutation (the
742
+ // `/admin/*` namespace belongs to the admin router + its requireAdmin
743
+ // middleware, which we deliberately do not borrow here).
744
+ // -------------------------------------------------------------------------
745
+
746
+ /** Fail-fast admin gate for the roles routes. Throws AccessDeniedError. */
747
+ async function assertRolesAdmin(userEmail: string): Promise<void> {
748
+ await workspaceService.getOrCreateForBranch(DEFAULT_BRANCH);
749
+ await assertCanMutate(workspaceIdForBranch(DEFAULT_BRANCH), DEFAULT_BRANCH, userEmail, 'roles.yaml');
750
+ }
751
+
752
+ router.get('/access/roles', async (req, res) => {
753
+ const user = await requireUser(req, res);
754
+ if (!user) return;
755
+ try {
756
+ // Roster viewing is admin-only (spec: non-admins can't see or use the
757
+ // surface). The frontend hides the page, but the API must gate too — the
758
+ // roster exposes every role + member email.
759
+ await assertRolesAdmin(user.email);
760
+ res.json({ roles: await rolesAdmin.getRoster() });
761
+ } catch (err) {
762
+ const { status, body } = toHttpError(err);
763
+ res.status(status).json(body);
764
+ }
765
+ });
766
+
767
+ router.post('/access/roles', async (req, res) => {
768
+ const user = await requireUser(req, res);
769
+ if (!user) return;
770
+ try {
771
+ await assertRolesAdmin(user.email);
772
+ const displayName = requireNonEmptyString((req.body ?? {}).displayName, 'displayName');
773
+ res.json({ roles: await rolesAdmin.createRole(user, displayName) });
774
+ } catch (err) {
775
+ const { status, body } = toHttpError(err);
776
+ res.status(status).json(body);
777
+ }
778
+ });
779
+
780
+ router.delete('/access/roles/:canonical', async (req, res) => {
781
+ const user = await requireUser(req, res);
782
+ if (!user) return;
783
+ try {
784
+ await assertRolesAdmin(user.email);
785
+ const canonical = canonicalRoleName(req.params.canonical);
786
+ res.json({ roles: await rolesAdmin.deleteRole(user, canonical) });
787
+ } catch (err) {
788
+ const { status, body } = toHttpError(err);
789
+ res.status(status).json(body);
790
+ }
791
+ });
792
+
793
+ router.patch('/access/roles/:canonical', async (req, res) => {
794
+ const user = await requireUser(req, res);
795
+ if (!user) return;
796
+ try {
797
+ await assertRolesAdmin(user.email);
798
+ const canonical = canonicalRoleName(req.params.canonical);
799
+ const newDisplayName = requireNonEmptyString((req.body ?? {}).newDisplayName, 'newDisplayName');
800
+ res.json({ roles: await rolesAdmin.renameRole(user, canonical, newDisplayName) });
801
+ } catch (err) {
802
+ const { status, body } = toHttpError(err);
803
+ res.status(status).json(body);
804
+ }
805
+ });
806
+
807
+ router.post('/access/roles/:canonical/members', async (req, res) => {
808
+ const user = await requireUser(req, res);
809
+ if (!user) return;
810
+ try {
811
+ await assertRolesAdmin(user.email);
812
+ const canonical = canonicalRoleName(req.params.canonical);
813
+ const email = requireNonEmptyString((req.body ?? {}).email, 'email');
814
+ res.json({ roles: await rolesAdmin.addMember(user, canonical, email) });
815
+ } catch (err) {
816
+ const { status, body } = toHttpError(err);
817
+ res.status(status).json(body);
818
+ }
819
+ });
820
+
821
+ /**
822
+ * GET /access/roles/health
823
+ * Auth-only (NOT admin-gated): a corrupted roles.yaml resolves nobody as
824
+ * admin, so an admin gate could never report the state this exists to detect.
825
+ * Returns `{ ok, errors }` for the default-branch roles.yaml; drives the
826
+ * "roles file corrupted" banner.
827
+ */
828
+ router.get('/access/roles/health', async (req, res) => {
829
+ const user = await requireUser(req, res);
830
+ if (!user) return;
831
+ try {
832
+ res.json(await rolesAdmin.getHealth());
833
+ } catch (err) {
834
+ const { status, body } = toHttpError(err);
835
+ res.status(status).json(body);
836
+ }
837
+ });
838
+
839
+ /**
840
+ * POST /access/roles/recover — the "Bevel Recovery" button.
841
+ * Auth-only by design: see `RolesAdminService.recover`. It self-gates on the
842
+ * file actually being corrupted (409 if it parses), backs the corrupted bytes
843
+ * up to old-roles.yaml, and restores the known-good default.
844
+ */
845
+ router.post('/access/roles/recover', async (req, res) => {
846
+ const user = await requireUser(req, res);
847
+ if (!user) return;
848
+ try {
849
+ const roles = await rolesAdmin.recover(user);
850
+ console.warn(`[access.roles.recover] roles.yaml recovered byUserId=${user.id}`);
851
+ res.json({ roles });
852
+ } catch (err) {
853
+ const { status, body } = toHttpError(err);
854
+ res.status(status).json(body);
855
+ }
856
+ });
857
+
858
+ router.delete('/access/roles/:canonical/members/:email', async (req, res) => {
859
+ const user = await requireUser(req, res);
860
+ if (!user) return;
861
+ try {
862
+ await assertRolesAdmin(user.email);
863
+ // Express already URL-decodes req.params — do NOT decode again (a second
864
+ // decodeURIComponent corrupts any email containing a literal `%`).
865
+ const canonical = canonicalRoleName(req.params.canonical);
866
+ const email = req.params.email;
867
+ const confirm = req.query.confirm === 'true';
868
+ res.json({ roles: await rolesAdmin.removeMember(user, canonical, email, confirm) });
869
+ } catch (err) {
870
+ const { status, body } = toHttpError(err);
871
+ res.status(status).json(body);
872
+ }
873
+ });
874
+
875
+ return router;
876
+ }
877
+
878
+ /** Map a write-side `Principal` to the resolver's `GrantPrincipal`. */
879
+ function toGrantPrincipal(principal: Principal): GrantPrincipal {
880
+ return principal.kind === 'user'
881
+ ? { kind: 'user', email: principal.email }
882
+ : { kind: 'role', role: principal.role };
883
+ }
884
+
885
+ /** The distinct ancestor access.md paths a principal is named in for any verb. */
886
+ function collectInheritedSources(sources: GrantSources): string[] {
887
+ const paths = new Set<string>();
888
+ for (const list of Object.values(sources)) {
889
+ for (const s of list ?? []) {
890
+ if (s.kind === 'ancestor') paths.add(s.path);
891
+ }
892
+ }
893
+ return [...paths];
894
+ }