@bevel-software/platform-core-backend 0.26.0 → 0.27.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 (613) hide show
  1. package/agent-guide/access-control.md +17 -0
  2. package/agent-guide/conventions.md +8 -0
  3. package/agent-guide/html-views.md +83 -0
  4. package/agent-guide/new-knowledge-base.md +24 -0
  5. package/agent-guide/tool-manuals.md +10 -0
  6. package/dist/assets.d.ts +19 -0
  7. package/dist/assets.d.ts.map +1 -1
  8. package/dist/assets.js +26 -2
  9. package/dist/assets.js.map +1 -1
  10. package/dist/core/catalog-revision.d.ts +19 -1
  11. package/dist/core/catalog-revision.d.ts.map +1 -1
  12. package/dist/core/catalog-revision.js +14 -3
  13. package/dist/core/catalog-revision.js.map +1 -1
  14. package/dist/core/create-core-server.d.ts +5 -0
  15. package/dist/core/create-core-server.d.ts.map +1 -1
  16. package/dist/core/create-core-server.js +54 -3
  17. package/dist/core/create-core-server.js.map +1 -1
  18. package/dist/core/create-core-services.d.ts +26 -0
  19. package/dist/core/create-core-services.d.ts.map +1 -1
  20. package/dist/core/create-core-services.js +100 -8
  21. package/dist/core/create-core-services.js.map +1 -1
  22. package/dist/core/lifecycle.d.ts +16 -1
  23. package/dist/core/lifecycle.d.ts.map +1 -1
  24. package/dist/core/lifecycle.js +18 -1
  25. package/dist/core/lifecycle.js.map +1 -1
  26. package/dist/core-config.d.ts +21 -0
  27. package/dist/core-config.d.ts.map +1 -1
  28. package/dist/core-config.js +22 -1
  29. package/dist/core-config.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/modules/access/access-control.interface.d.ts +8 -0
  35. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  36. package/dist/modules/access/access-control.service.d.ts +1 -0
  37. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.js +11 -0
  39. package/dist/modules/access/access-control.service.js.map +1 -1
  40. package/dist/modules/access/directory-sync-bot.js +1 -1
  41. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  42. package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
  43. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  44. package/dist/modules/agent-guide/agent-guide.js +8 -0
  45. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  46. package/dist/modules/agent-guide/index.d.ts +1 -1
  47. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  48. package/dist/modules/agent-guide/index.js +1 -1
  49. package/dist/modules/agent-guide/index.js.map +1 -1
  50. package/dist/modules/agent-instructions/compose.d.ts +5 -0
  51. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  52. package/dist/modules/agent-instructions/compose.js +19 -2
  53. package/dist/modules/agent-instructions/compose.js.map +1 -1
  54. package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
  55. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  56. package/dist/modules/audit/agent-audit.service.d.ts +21 -2
  57. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  58. package/dist/modules/audit/agent-audit.service.js +43 -2
  59. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  60. package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
  61. package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
  62. package/dist/modules/audit/agent-connection.routes.js +38 -0
  63. package/dist/modules/audit/agent-connection.routes.js.map +1 -0
  64. package/dist/modules/audit/audit.contract.d.ts +24 -0
  65. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  66. package/dist/modules/audit/audit.contract.js.map +1 -1
  67. package/dist/modules/audit/index.d.ts +1 -0
  68. package/dist/modules/audit/index.d.ts.map +1 -1
  69. package/dist/modules/audit/index.js +1 -0
  70. package/dist/modules/audit/index.js.map +1 -1
  71. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  72. package/dist/modules/auth/account.routes.js +10 -3
  73. package/dist/modules/auth/account.routes.js.map +1 -1
  74. package/dist/modules/auth/auth.middleware.d.ts +7 -0
  75. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  76. package/dist/modules/auth/auth.middleware.js +1 -1
  77. package/dist/modules/auth/auth.middleware.js.map +1 -1
  78. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  79. package/dist/modules/code-mode/code-mode.tool.js +11 -3
  80. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  81. package/dist/modules/database/core-schema.d.ts +96 -0
  82. package/dist/modules/database/core-schema.d.ts.map +1 -1
  83. package/dist/modules/database/core-schema.js +32 -3
  84. package/dist/modules/database/core-schema.js.map +1 -1
  85. package/dist/modules/embed/account-link.service.d.ts +42 -0
  86. package/dist/modules/embed/account-link.service.d.ts.map +1 -0
  87. package/dist/modules/embed/account-link.service.js +50 -0
  88. package/dist/modules/embed/account-link.service.js.map +1 -0
  89. package/dist/modules/embed/embed-link.d.ts +58 -0
  90. package/dist/modules/embed/embed-link.d.ts.map +1 -0
  91. package/dist/modules/embed/embed-link.js +160 -0
  92. package/dist/modules/embed/embed-link.js.map +1 -0
  93. package/dist/modules/embed/embed.errors.d.ts +30 -0
  94. package/dist/modules/embed/embed.errors.d.ts.map +1 -0
  95. package/dist/modules/embed/embed.errors.js +42 -0
  96. package/dist/modules/embed/embed.errors.js.map +1 -0
  97. package/dist/modules/embed/embed.interface.d.ts +204 -0
  98. package/dist/modules/embed/embed.interface.d.ts.map +1 -0
  99. package/dist/modules/embed/embed.interface.js +2 -0
  100. package/dist/modules/embed/embed.interface.js.map +1 -0
  101. package/dist/modules/embed/embed.routes.d.ts +59 -0
  102. package/dist/modules/embed/embed.routes.d.ts.map +1 -0
  103. package/dist/modules/embed/embed.routes.js +343 -0
  104. package/dist/modules/embed/embed.routes.js.map +1 -0
  105. package/dist/modules/embed/embed.service.d.ts +169 -0
  106. package/dist/modules/embed/embed.service.d.ts.map +1 -0
  107. package/dist/modules/embed/embed.service.js +627 -0
  108. package/dist/modules/embed/embed.service.js.map +1 -0
  109. package/dist/modules/embed/embed.tools.d.ts +51 -0
  110. package/dist/modules/embed/embed.tools.d.ts.map +1 -0
  111. package/dist/modules/embed/embed.tools.js +170 -0
  112. package/dist/modules/embed/embed.tools.js.map +1 -0
  113. package/dist/modules/embed/mcp-app.d.ts +64 -0
  114. package/dist/modules/embed/mcp-app.d.ts.map +1 -0
  115. package/dist/modules/embed/mcp-app.js +147 -0
  116. package/dist/modules/embed/mcp-app.js.map +1 -0
  117. package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
  118. package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
  119. package/dist/modules/embed/mcp-app.routes.js +36 -0
  120. package/dist/modules/embed/mcp-app.routes.js.map +1 -0
  121. package/dist/modules/github-app/github-app.connection.d.ts +15 -0
  122. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
  123. package/dist/modules/github-app/github-app.connection.js +34 -3
  124. package/dist/modules/github-app/github-app.connection.js.map +1 -1
  125. package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
  126. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  127. package/dist/modules/kb-fs/locking-filesystem.js +300 -0
  128. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  129. package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
  130. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  131. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  132. package/dist/modules/kb-sync/kb-sync.service.js +6 -0
  133. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  134. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  135. package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
  136. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  137. package/dist/modules/mcp/mcp.service.d.ts +28 -1
  138. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  139. package/dist/modules/mcp/mcp.service.js +64 -8
  140. package/dist/modules/mcp/mcp.service.js.map +1 -1
  141. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
  142. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  143. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
  144. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  145. package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
  146. package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
  147. package/dist/modules/onboarding/onboarding.contract.js +7 -0
  148. package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
  149. package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
  150. package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
  151. package/dist/modules/onboarding/onboarding.routes.js +81 -0
  152. package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
  153. package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
  154. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
  155. package/dist/modules/onboarding/starter-pack.service.js +492 -0
  156. package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
  157. package/dist/modules/onboarding/starter-packs.d.ts +78 -0
  158. package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
  159. package/dist/modules/onboarding/starter-packs.js +156 -0
  160. package/dist/modules/onboarding/starter-packs.js.map +1 -0
  161. package/dist/modules/plugins/index.d.ts +1 -1
  162. package/dist/modules/plugins/index.d.ts.map +1 -1
  163. package/dist/modules/plugins/index.js +1 -1
  164. package/dist/modules/plugins/index.js.map +1 -1
  165. package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
  166. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  167. package/dist/modules/plugins/plugin-provision.service.js +18 -1
  168. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  169. package/dist/modules/plugins/plugins.routes.d.ts +0 -36
  170. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  171. package/dist/modules/plugins/plugins.routes.js +31 -0
  172. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  173. package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
  174. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  175. package/dist/modules/settings/deployment-settings.service.js +134 -1
  176. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  177. package/dist/modules/settings/repository-source.d.ts +1 -0
  178. package/dist/modules/settings/repository-source.d.ts.map +1 -1
  179. package/dist/modules/settings/repository-source.js +3 -3
  180. package/dist/modules/settings/repository-source.js.map +1 -1
  181. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  182. package/dist/modules/skills/skills.tools.js +50 -32
  183. package/dist/modules/skills/skills.tools.js.map +1 -1
  184. package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
  185. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
  186. package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
  187. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
  188. package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
  189. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  190. package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
  191. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  192. package/dist/modules/tool-auth/external-api-key.service.js +59 -2
  193. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  194. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  195. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
  196. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  197. package/dist/modules/tool-helpers/index.d.ts +28 -1
  198. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  199. package/dist/modules/tool-helpers/index.js +28 -1
  200. package/dist/modules/tool-helpers/index.js.map +1 -1
  201. package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
  202. package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
  203. package/dist/modules/tool-helpers/route-argument-check.js +132 -0
  204. package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
  205. package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
  206. package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
  207. package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
  208. package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
  209. package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
  210. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
  211. package/dist/modules/tool-helpers/tool-def.js +70 -6
  212. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  213. package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
  214. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  215. package/dist/modules/tool-helpers/tool-handler.js +157 -8
  216. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  217. package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
  218. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  219. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  220. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  221. package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
  222. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  223. package/dist/modules/tool-registry/description-length.d.ts +6 -2
  224. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  225. package/dist/modules/tool-registry/description-length.js +27 -5
  226. package/dist/modules/tool-registry/description-length.js.map +1 -1
  227. package/dist/modules/tool-registry/guide-first.d.ts +4 -0
  228. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
  229. package/dist/modules/tool-registry/guide-first.js +9 -1
  230. package/dist/modules/tool-registry/guide-first.js.map +1 -1
  231. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
  232. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  233. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
  234. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  235. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
  236. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  237. package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
  238. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  239. package/dist/modules/workflow/file-lock.service.d.ts +29 -5
  240. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  241. package/dist/modules/workflow/file-lock.service.js +62 -5
  242. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  243. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  244. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  245. package/dist/modules/workflow/git/git.service.js +172 -36
  246. package/dist/modules/workflow/git/git.service.js.map +1 -1
  247. package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
  248. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  249. package/dist/modules/workflow/git/node-git-runner.js +78 -6
  250. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  251. package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
  252. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  253. package/dist/modules/workflow/pending-commits.service.js +25 -1
  254. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  255. package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
  256. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  257. package/dist/modules/workflow/pending-commits.worker.js +23 -6
  258. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  259. package/dist/modules/workflow/recovery-bot.js +1 -1
  260. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  261. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
  263. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  264. package/dist/modules/workflow/sanitize-error.d.ts +16 -0
  265. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  266. package/dist/modules/workflow/sanitize-error.js +45 -0
  267. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  268. package/dist/modules/workflow/workflow.service.d.ts +183 -15
  269. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  270. package/dist/modules/workflow/workflow.service.js +805 -106
  271. package/dist/modules/workflow/workflow.service.js.map +1 -1
  272. package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
  273. package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
  274. package/dist/modules/workspace/agent-download.builder.js +238 -0
  275. package/dist/modules/workspace/agent-download.builder.js.map +1 -0
  276. package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
  277. package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
  278. package/dist/modules/workspace/agent-download.routes.js +156 -0
  279. package/dist/modules/workspace/agent-download.routes.js.map +1 -0
  280. package/dist/modules/workspace/agent-download.store.d.ts +225 -0
  281. package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
  282. package/dist/modules/workspace/agent-download.store.js +423 -0
  283. package/dist/modules/workspace/agent-download.store.js.map +1 -0
  284. package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
  285. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
  286. package/dist/modules/workspace/agent-upload.store.js +16 -6
  287. package/dist/modules/workspace/agent-upload.store.js.map +1 -1
  288. package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
  289. package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
  290. package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
  291. package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
  292. package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
  293. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  294. package/dist/modules/workspace/file-readers/text-reader.js +5 -8
  295. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  296. package/dist/modules/workspace/first-run.d.ts +111 -0
  297. package/dist/modules/workspace/first-run.d.ts.map +1 -0
  298. package/dist/modules/workspace/first-run.js +181 -0
  299. package/dist/modules/workspace/first-run.js.map +1 -0
  300. package/dist/modules/workspace/move-links.d.ts +92 -0
  301. package/dist/modules/workspace/move-links.d.ts.map +1 -0
  302. package/dist/modules/workspace/move-links.js +173 -0
  303. package/dist/modules/workspace/move-links.js.map +1 -0
  304. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  305. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -13
  306. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  307. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  308. package/dist/modules/workspace/workspace.routes.js +63 -22
  309. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  310. package/dist/modules/workspace/workspace.service.d.ts +75 -2
  311. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  312. package/dist/modules/workspace/workspace.service.js +197 -24
  313. package/dist/modules/workspace/workspace.service.js.map +1 -1
  314. package/dist/modules/workspace/workspace.tools.d.ts +59 -1
  315. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  316. package/dist/modules/workspace/workspace.tools.js +443 -65
  317. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  318. package/dist/modules/write-access/write-access.d.ts.map +1 -1
  319. package/dist/modules/write-access/write-access.js +9 -0
  320. package/dist/modules/write-access/write-access.js.map +1 -1
  321. package/dist/shared/domain-errors.d.ts +73 -1
  322. package/dist/shared/domain-errors.d.ts.map +1 -1
  323. package/dist/shared/domain-errors.js +86 -12
  324. package/dist/shared/domain-errors.js.map +1 -1
  325. package/dist/shared/git.contract.d.ts +10 -1
  326. package/dist/shared/git.contract.d.ts.map +1 -1
  327. package/dist/shared/git.contract.js.map +1 -1
  328. package/dist/shared/utf8-text.d.ts +15 -0
  329. package/dist/shared/utf8-text.d.ts.map +1 -0
  330. package/dist/shared/utf8-text.js +20 -0
  331. package/dist/shared/utf8-text.js.map +1 -0
  332. package/dist/tenancy/static-tenant-source.d.ts +2 -1
  333. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  334. package/dist/tenancy/static-tenant-source.js +9 -2
  335. package/dist/tenancy/static-tenant-source.js.map +1 -1
  336. package/mcp-app/page.html +349 -0
  337. package/migrations/0017_atlassian_account_links.sql +37 -0
  338. package/migrations/meta/0017_snapshot.json +2396 -0
  339. package/migrations/meta/_journal.json +8 -1
  340. package/package.json +5 -3
  341. package/src/__tests__/packaged-assets-ship.test.ts +16 -4
  342. package/src/assets.ts +28 -2
  343. package/src/core/__tests__/catalog-revision.test.ts +40 -0
  344. package/src/core/__tests__/gated-boot.test.ts +1 -0
  345. package/src/core/__tests__/lifecycle.test.ts +14 -0
  346. package/src/core/catalog-revision.ts +15 -2
  347. package/src/core/create-core-server.ts +67 -3
  348. package/src/core/create-core-services.ts +140 -6
  349. package/src/core/lifecycle.ts +28 -2
  350. package/src/core-config.ts +24 -1
  351. package/src/index.ts +2 -2
  352. package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
  353. package/src/modules/access/access-control.interface.ts +13 -0
  354. package/src/modules/access/access-control.service.ts +16 -0
  355. package/src/modules/access/directory-sync-bot.ts +1 -1
  356. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
  357. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
  358. package/src/modules/agent-guide/agent-guide.ts +9 -0
  359. package/src/modules/agent-guide/index.ts +1 -0
  360. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
  361. package/src/modules/agent-instructions/compose.ts +17 -2
  362. package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
  363. package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
  364. package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
  365. package/src/modules/audit/agent-audit.service.ts +58 -3
  366. package/src/modules/audit/agent-connection.routes.ts +56 -0
  367. package/src/modules/audit/audit.contract.ts +26 -0
  368. package/src/modules/audit/index.ts +1 -0
  369. package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
  370. package/src/modules/auth/account.routes.ts +10 -3
  371. package/src/modules/auth/auth.middleware.ts +1 -1
  372. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
  373. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  374. package/src/modules/database/core-schema.ts +33 -3
  375. package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
  376. package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
  377. package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
  378. package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
  379. package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
  380. package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
  381. package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
  382. package/src/modules/embed/account-link.service.ts +76 -0
  383. package/src/modules/embed/embed-link.ts +180 -0
  384. package/src/modules/embed/embed.errors.ts +43 -0
  385. package/src/modules/embed/embed.interface.ts +203 -0
  386. package/src/modules/embed/embed.routes.ts +363 -0
  387. package/src/modules/embed/embed.service.ts +705 -0
  388. package/src/modules/embed/embed.tools.ts +206 -0
  389. package/src/modules/embed/mcp-app.routes.ts +41 -0
  390. package/src/modules/embed/mcp-app.ts +173 -0
  391. package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
  392. package/src/modules/github-app/github-app.connection.ts +38 -4
  393. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
  394. package/src/modules/kb-fs/locking-filesystem.ts +287 -0
  395. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
  396. package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
  397. package/src/modules/kb-sync/kb-sync.service.ts +6 -0
  398. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
  399. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
  400. package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
  401. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  402. package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
  403. package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
  404. package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
  405. package/src/modules/mcp/mcp.service.ts +81 -12
  406. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
  407. package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
  408. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
  409. package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
  410. package/src/modules/onboarding/onboarding.contract.ts +85 -0
  411. package/src/modules/onboarding/onboarding.routes.ts +85 -0
  412. package/src/modules/onboarding/starter-pack.service.ts +580 -0
  413. package/src/modules/onboarding/starter-packs.ts +182 -0
  414. package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
  415. package/src/modules/plugins/index.ts +1 -0
  416. package/src/modules/plugins/plugin-provision.service.ts +22 -1
  417. package/src/modules/plugins/plugins.routes.ts +29 -0
  418. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
  419. package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
  420. package/src/modules/settings/deployment-settings.service.ts +131 -1
  421. package/src/modules/settings/repository-source.ts +4 -4
  422. package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
  423. package/src/modules/skills/skills.tools.ts +55 -35
  424. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
  425. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
  426. package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
  427. package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
  428. package/src/modules/tool-auth/external-api-key.service.ts +61 -4
  429. package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
  430. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
  431. package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
  432. package/src/modules/tool-helpers/index.ts +34 -1
  433. package/src/modules/tool-helpers/route-argument-check.ts +149 -0
  434. package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
  435. package/src/modules/tool-helpers/tool-def.ts +132 -7
  436. package/src/modules/tool-helpers/tool-handler.ts +196 -8
  437. package/src/modules/tool-helpers/tool.contract.ts +9 -0
  438. package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
  439. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
  440. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
  441. package/src/modules/tool-registry/description-length.ts +34 -7
  442. package/src/modules/tool-registry/guide-first.ts +8 -1
  443. package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
  444. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
  445. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
  446. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
  447. package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
  448. package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
  449. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
  450. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
  451. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
  452. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
  453. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
  454. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
  455. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
  457. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
  458. package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
  459. package/src/modules/workflow/file-lock.service.ts +67 -5
  460. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
  461. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
  462. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
  463. package/src/modules/workflow/git/git.service.ts +176 -38
  464. package/src/modules/workflow/git/node-git-runner.ts +82 -7
  465. package/src/modules/workflow/pending-commits.service.ts +29 -1
  466. package/src/modules/workflow/pending-commits.worker.ts +24 -6
  467. package/src/modules/workflow/recovery-bot.ts +1 -1
  468. package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
  469. package/src/modules/workflow/sanitize-error.ts +50 -0
  470. package/src/modules/workflow/workflow.service.ts +879 -112
  471. package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
  472. package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
  473. package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
  474. package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
  475. package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
  476. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
  477. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
  478. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
  479. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
  480. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
  481. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
  482. package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
  483. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
  484. package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
  485. package/src/modules/workspace/agent-download.builder.ts +308 -0
  486. package/src/modules/workspace/agent-download.routes.ts +183 -0
  487. package/src/modules/workspace/agent-download.store.ts +533 -0
  488. package/src/modules/workspace/agent-upload.store.ts +23 -7
  489. package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
  490. package/src/modules/workspace/file-readers/text-reader.ts +5 -8
  491. package/src/modules/workspace/first-run.ts +222 -0
  492. package/src/modules/workspace/move-links.ts +237 -0
  493. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
  494. package/src/modules/workspace/workspace.routes.ts +64 -22
  495. package/src/modules/workspace/workspace.service.ts +202 -25
  496. package/src/modules/workspace/workspace.tools.ts +492 -63
  497. package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
  498. package/src/modules/write-access/write-access.ts +9 -0
  499. package/src/shared/domain-errors.ts +108 -11
  500. package/src/shared/git.contract.ts +8 -1
  501. package/src/shared/utf8-text.ts +21 -0
  502. package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
  503. package/src/tenancy/static-tenant-source.ts +10 -2
  504. package/starter-packs/README.md +103 -0
  505. package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
  506. package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
  507. package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
  508. package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
  509. package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
  510. package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
  511. package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
  512. package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
  513. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
  514. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
  515. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
  516. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
  517. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
  518. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
  519. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
  520. package/starter-packs/engineering/pack.yaml +11 -0
  521. package/starter-packs/general/KnowledgeBase/About us.md +9 -0
  522. package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
  523. package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
  524. package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
  525. package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
  526. package/starter-packs/general/Plugins/general-starter/access.md +19 -0
  527. package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
  528. package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
  529. package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
  530. package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
  531. package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
  532. package/starter-packs/general/pack.yaml +10 -0
  533. package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
  534. package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
  535. package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
  536. package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
  537. package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
  538. package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
  539. package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
  540. package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
  541. package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
  542. package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
  543. package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
  544. package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
  545. package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
  546. package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
  547. package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
  548. package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
  549. package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
  550. package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
  551. package/starter-packs/operations/pack.yaml +12 -0
  552. package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
  553. package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
  554. package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
  555. package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
  556. package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
  557. package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
  558. package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
  559. package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
  560. package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
  561. package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
  562. package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
  563. package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
  564. package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
  565. package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
  566. package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
  567. package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
  568. package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
  569. package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
  570. package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
  571. package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
  572. package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
  573. package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
  574. package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
  575. package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
  576. package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
  577. package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
  578. package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
  579. package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
  580. package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
  581. package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
  582. package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
  583. package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
  584. package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
  585. package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
  586. package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
  587. package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
  588. package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
  589. package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
  590. package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
  591. package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
  592. package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
  593. package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
  594. package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
  595. package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
  596. package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
  597. package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
  598. package/starter-packs/sales/pack.yaml +13 -0
  599. package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
  600. package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
  601. package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
  602. package/starter-packs/support/KnowledgeBase/Products.md +9 -0
  603. package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
  604. package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
  605. package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
  606. package/starter-packs/support/Plugins/support-starter/access.md +19 -0
  607. package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
  608. package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
  609. package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
  610. package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
  611. package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
  612. package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
  613. package/starter-packs/support/pack.yaml +12 -0
@@ -0,0 +1,805 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { createHmac } from 'node:crypto';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { createFileReaderRegistry } from '../../workspace/file-readers/file-reader.registry.js';
5
+ import type { DocExtractOutcome, DocExtractService } from '../../workspace/file-readers/doc-extract.service.js';
6
+ import { makeRolesYamlWriteValidator, RolesYamlInvalidError } from '../../access-model/roles-yaml-guard.js';
7
+ import { ACCOUNT_DEACTIVATED_MESSAGE } from '../../auth/account-admission.js';
8
+ import { testKbContext, TEST_BRANCH_MODEL } from '../../../__tests__/kb-context.js';
9
+ import { EmbedService, type EmbedConfig } from '../embed.service.js';
10
+ import {
11
+ EmbedAccessError,
12
+ EmbedLockedError,
13
+ EmbedNodeNotFoundError,
14
+ EmbedTokenError,
15
+ } from '../embed.errors.js';
16
+ import { EmbedRefParseError } from '../embed-link.js';
17
+ import { resolveBeside } from '../embed.service.js';
18
+
19
+ const KB = 'knowledge-base';
20
+ const BRANCH = TEST_BRANCH_MODEL.defaultBranch;
21
+ const REPO = 'Data/Thing.md';
22
+ const WS = `${KB}/${REPO}`;
23
+ const PAGE = '# Thing\n\nWhat it is.\n';
24
+
25
+ const USER = { id: 'u-1', email: 'alice@bevel.software', name: 'Alice' };
26
+ const SIGNING_KEY = createHmac('sha256', 'test-jwt-secret').update('bevel-embed-token-v1').digest('hex');
27
+
28
+ function config(overrides: Partial<EmbedConfig> = {}): EmbedConfig {
29
+ return {
30
+ jwtSecret: 'test-jwt-secret',
31
+ embedSharedSecret: 'shh',
32
+ publicFrontendUrl: 'https://hexis.example',
33
+ kbDirName: KB,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ interface Opts {
39
+ canRead?: boolean;
40
+ canWrite?: boolean;
41
+ /** `undefined` = linked to USER; `null` = not linked to anybody. */
42
+ linkedUserId?: string | null;
43
+ acquired?: boolean;
44
+ holderName?: string;
45
+ files?: Record<string, string | Buffer>;
46
+ emailDomainAllowed?: boolean;
47
+ /** Whether the token's account is still on; `false` = an admin switched it off. */
48
+ active?: boolean;
49
+ openChangeRequest?: ReturnType<typeof vi.fn>;
50
+ createBranch?: ReturnType<typeof vi.fn>;
51
+ /** Whether origin already carries the viewer's suggestions branch. */
52
+ remoteBranchExists?: boolean;
53
+ config?: Partial<EmbedConfig>;
54
+ resolveNodeId?: ((id: string) => Promise<string | null>) | null;
55
+ }
56
+
57
+ /**
58
+ * The extraction service the reader registry takes, typed against its
59
+ * contract: no test here reads a document-format file, and one that did
60
+ * would get this typed refusal, not a silent empty text.
61
+ */
62
+ function noDocExtract(): DocExtractService {
63
+ const extract = async (): Promise<DocExtractOutcome> => ({
64
+ ok: false,
65
+ message: 'this suite reads no document-format file',
66
+ });
67
+ return { extract } satisfies Pick<DocExtractService, 'extract'> as DocExtractService;
68
+ }
69
+
70
+ function build(opts: Opts = {}) {
71
+ const files: Record<string, string | Buffer> = opts.files ?? { [WS]: PAGE };
72
+ const workspaceService = {
73
+ getOrCreateForBranch: vi.fn(async (branch: string) => ({ id: encodeURIComponent(branch), kbDirName: KB })),
74
+ readFileBinary: vi.fn(async (_id: string, wsPath: string) => {
75
+ const found = files[wsPath];
76
+ if (found === undefined) {
77
+ const err = new Error(`ENOENT: ${wsPath}`) as NodeJS.ErrnoException;
78
+ err.code = 'ENOENT';
79
+ throw err;
80
+ }
81
+ return Buffer.isBuffer(found) ? found : Buffer.from(found, 'utf8');
82
+ }),
83
+ // The mint's existence check: a stat, never a read.
84
+ isFile: vi.fn(async (_id: string, wsPath: string) => files[wsPath] !== undefined),
85
+ writeFile: vi.fn(async () => undefined),
86
+ // The service's per-path turn, as the real one runs `op`: one at a time
87
+ // per path, callers in order. `turns` records what ran inside which turn.
88
+ withPathTurn: vi.fn(async (_id: string, wsPath: string, op: () => Promise<unknown>) => {
89
+ const before = turns.get(wsPath) ?? Promise.resolve();
90
+ const mine = before.catch(() => undefined).then(op);
91
+ turns.set(wsPath, mine);
92
+ return mine;
93
+ }),
94
+ };
95
+ const turns = new Map<string, Promise<unknown>>();
96
+ const accessControl = {
97
+ canRead: vi.fn(async () => opts.canRead ?? true),
98
+ canWrite: vi.fn(async () => opts.canWrite ?? true),
99
+ };
100
+ const authService = {
101
+ getUserById: vi.fn(async () => USER),
102
+ isEmailDomainAllowed: vi.fn(() => opts.emailDomainAllowed ?? true),
103
+ isActive: vi.fn(async () => opts.active ?? true),
104
+ };
105
+ const workflowService = {
106
+ // No live lock: a save takes it. The contention rules themselves are
107
+ // exercised against the real lock in embed.service.lock.test.ts.
108
+ getLock: vi.fn(async () => null),
109
+ // The suggestions branch is created here, under the workflow's own
110
+ // branch-lifecycle lock, never straight through git.
111
+ createBranch: opts.createBranch ?? vi.fn(async () => ({})),
112
+ acquireLock: vi.fn(async () => ({
113
+ acquired: opts.acquired ?? true,
114
+ lock: { holderName: opts.holderName ?? 'Bob' },
115
+ })),
116
+ heartbeatLock: vi.fn(async () => undefined),
117
+ releaseLock: vi.fn(async () => undefined),
118
+ releaseLockNoCommit: vi.fn(async () => undefined),
119
+ commitChanges: vi.fn(async () => ({ sha: 'deadbee' })),
120
+ openChangeRequest: opts.openChangeRequest ?? vi.fn(async () => ({ number: 42 })),
121
+ };
122
+ const gitService = {
123
+ // Origin has no suggestions branch yet unless a test says otherwise.
124
+ remoteBranchExists: vi.fn(async () => opts.remoteBranchExists ?? false),
125
+ };
126
+ const accountLinks = {
127
+ getUserId: vi.fn(async () => (opts.linkedUserId === undefined ? USER.id : opts.linkedUserId)),
128
+ link: vi.fn(async () => undefined),
129
+ listForUser: vi.fn(async () => [{ atlassianAccountId: 'acc-1', createdAt: new Date(0) }]),
130
+ unlink: vi.fn(async () => true),
131
+ };
132
+ const service = new EmbedService(
133
+ config(opts.config),
134
+ testKbContext({ kbDirName: KB }),
135
+ workspaceService as never,
136
+ accessControl as never,
137
+ authService as never,
138
+ workflowService as never,
139
+ gitService as never,
140
+ accountLinks as never,
141
+ createFileReaderRegistry(noDocExtract()),
142
+ // The real gate: the property under test is that an embed write cannot
143
+ // put on disk what the app's editor would refuse.
144
+ makeRolesYamlWriteValidator(KB),
145
+ opts.resolveNodeId ?? null,
146
+ );
147
+ return { service, workspaceService, accessControl, authService, workflowService, gitService, accountLinks };
148
+ }
149
+
150
+ /** The token a mint produced, decoded — so a test can assert on the claims. */
151
+ function claimsOf(token: string): Record<string, unknown> {
152
+ return jwt.verify(token, SIGNING_KEY) as Record<string, unknown>;
153
+ }
154
+
155
+ describe('EmbedService: minting', () => {
156
+ it('mints for a Hexis user — the MCP path — and names the embed page', async () => {
157
+ const { service } = build();
158
+ const { token, embedUrl } = await service.mintForUser({ userId: USER.id, reference: REPO });
159
+ expect(embedUrl).toBe(`https://hexis.example/embed?token=${encodeURIComponent(token)}`);
160
+ expect(claimsOf(token)).toMatchObject({
161
+ scope: 'embed',
162
+ kind: 'user',
163
+ sub: USER.id,
164
+ repoRelative: REPO,
165
+ });
166
+ });
167
+
168
+ /**
169
+ * The token rides in the tool result and so in the chat transcript;
170
+ * whoever holds it acts on that one file as its user until it expires.
171
+ * One hour (Razvan, 2026-10-09), down from the two the embed used before.
172
+ */
173
+ it('mints a token that lives one hour', async () => {
174
+ const { service } = build();
175
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
176
+ const { iat, exp } = claimsOf(token) as { iat: number; exp: number };
177
+ expect(exp - iat).toBe(60 * 60);
178
+ });
179
+
180
+ it('mints for an outside account — the connector path, unchanged', async () => {
181
+ const { service } = build();
182
+ const { token } = await service.mintToken({
183
+ accountId: 'acc-1',
184
+ email: 'alice@bevel.software',
185
+ reference: `content: ${KB}/${REPO}#what`,
186
+ });
187
+ expect(claimsOf(token)).toMatchObject({ kind: 'atlassian', sub: 'acc-1', repoRelative: REPO, slug: 'what' });
188
+ });
189
+
190
+ it('keeps the token PSEUDONYMOUS — no address, no name', async () => {
191
+ const { service } = build();
192
+ const { token } = await service.mintToken({ accountId: 'acc-1', email: USER.email, reference: REPO });
193
+ expect(JSON.stringify(claimsOf(token))).not.toContain(USER.email);
194
+ expect(JSON.stringify(claimsOf(token))).not.toContain(USER.name);
195
+ });
196
+
197
+ it('refuses a reference whose file does not exist, so a dead link fails at mint time', async () => {
198
+ const { service } = build();
199
+ await expect(service.mintForUser({ userId: USER.id, reference: 'Data/Gone.md' })).rejects.toThrow(
200
+ EmbedNodeNotFoundError,
201
+ );
202
+ });
203
+
204
+ it('refuses an account whose email domain may not reach the knowledge base', async () => {
205
+ const { service } = build({ emailDomainAllowed: false });
206
+ await expect(
207
+ service.mintToken({ accountId: 'acc-1', email: 'x@elsewhere.test', reference: REPO }),
208
+ ).rejects.toThrow(EmbedAccessError);
209
+ });
210
+
211
+ it('refuses a node-id reference when no deployment resolver is registered', async () => {
212
+ const { service } = build();
213
+ await expect(service.mintForUser({ userId: USER.id, reference: 'hx-a-node' })).rejects.toThrow(
214
+ EmbedRefParseError,
215
+ );
216
+ });
217
+
218
+ /**
219
+ * `readme` is the copy-link's id shape AND a legal root-level file. The
220
+ * file that is really there wins — `open_page` has just read it by that
221
+ * very path, and core has no id resolver to fall back on.
222
+ */
223
+ it('reads an extensionless root file as the path it is, not as a node id', async () => {
224
+ const { service } = build({ files: { [`${KB}/readme`]: 'hello' } });
225
+ const { token } = await service.mintForUser({ userId: USER.id, reference: 'readme' });
226
+ expect(claimsOf(token)).toMatchObject({ repoRelative: 'readme' });
227
+ });
228
+
229
+ it('refuses a separator spelled %2F rather than decoding it into a real one', async () => {
230
+ const { service } = build({ files: { [WS]: PAGE } });
231
+ await expect(service.mintForUser({ userId: USER.id, reference: 'Data%2FThing.md' })).rejects.toThrow(
232
+ EmbedRefParseError,
233
+ );
234
+ await expect(
235
+ service.mintForUser({ userId: USER.id, reference: `https://hexis.example/workspace/main/${KB}/Data%2FThing.md` }),
236
+ ).rejects.toThrow(EmbedRefParseError);
237
+ });
238
+
239
+ it('resolves a node-id reference through a deployment resolver when one is', async () => {
240
+ const { service } = build({ resolveNodeId: async () => REPO });
241
+ const { token } = await service.mintForUser({ userId: USER.id, reference: 'hx-a-node' });
242
+ expect(claimsOf(token)).toMatchObject({ repoRelative: REPO });
243
+ });
244
+
245
+ it('checks the shared secret in a way that rejects a wrong one and a wrong length', () => {
246
+ const { service } = build();
247
+ expect(service.sharedSecretConfigured()).toBe(true);
248
+ expect(service.verifySharedSecret('shh')).toBe(true);
249
+ expect(service.verifySharedSecret('shhh')).toBe(false);
250
+ expect(service.verifySharedSecret('sh')).toBe(false);
251
+ expect(service.verifySharedSecret(undefined)).toBe(false);
252
+ });
253
+
254
+ it('reports the shared-secret mint as unconfigured when no secret is set', () => {
255
+ const { service } = build({ config: { embedSharedSecret: '' } });
256
+ expect(service.sharedSecretConfigured()).toBe(false);
257
+ expect(service.verifySharedSecret('')).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe('EmbedService: loading', () => {
262
+ it('answers the file, the default branch and the app address', async () => {
263
+ const { service } = build();
264
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
265
+ const view = await service.loadFile(token);
266
+ expect(view).toMatchObject({
267
+ nodeName: 'Thing',
268
+ repoRelative: REPO,
269
+ workspacePath: WS,
270
+ kbDirName: KB,
271
+ branch: BRANCH,
272
+ content: PAGE,
273
+ contentIsText: true,
274
+ linked: true,
275
+ canRead: true,
276
+ canWrite: true,
277
+ });
278
+ expect(view.appUrl).toBe(
279
+ `https://hexis.example/workspace/${encodeURIComponent(BRANCH)}/${KB}/Data/Thing.md`,
280
+ );
281
+ });
282
+
283
+ it('carries the heading the agent named, so the view opens there', async () => {
284
+ const { service } = build();
285
+ const { token } = await service.mintForUser({ userId: USER.id, reference: `${REPO}#what-it-is` });
286
+ const view = await service.loadFile(token);
287
+ expect(view.heading).toBe('what-it-is');
288
+ // The WHOLE page, still: the heading says where to start reading, and a
289
+ // save writes the file the way the app's file page writes it.
290
+ expect(view.content).toBe(PAGE);
291
+ expect(view.appUrl).toContain('#what-it-is');
292
+ });
293
+
294
+ /**
295
+ * The embed renders EVERY type the app renders, so the load has to say
296
+ * which ones carry text and which ones are bytes a renderer fetches. The
297
+ * answer comes from the same reader registry `read_file` dispatches on, so
298
+ * it cannot disagree with what a read of the file returns.
299
+ */
300
+ it.each([
301
+ ['markdown', 'Data/Thing.md', true],
302
+ ['an HTML page', 'Pages/Report.html', true],
303
+ ['a CSV', 'Data/rows.csv', true],
304
+ ['an image', 'Shots/diagram.png', false],
305
+ ['a PDF', 'Docs/Report.pdf', false],
306
+ ['a Word document', 'Docs/Spec.docx', false],
307
+ ])('says whether %s is text or bytes', async (_label, repo, isText) => {
308
+ const { service } = build({ files: { [`${KB}/${repo}`]: 'bytes' } });
309
+ const { token } = await service.mintForUser({ userId: USER.id, reference: repo });
310
+ const view = await service.loadFile(token);
311
+ expect(view.contentIsText).toBe(isText);
312
+ // A byte renderer is handed NO content — it reads `/api/embed/raw` under
313
+ // this same token, which keeps a document out of a JSON payload.
314
+ if (!isText) expect(view.content).toBe('');
315
+ });
316
+
317
+ it('shows NO content to an identity that does not resolve, but still says so', async () => {
318
+ const { service } = build({ linkedUserId: null });
319
+ const { token } = await service.mintToken({ accountId: 'acc-1', reference: REPO });
320
+ const view = await service.loadFile(token);
321
+ expect(view).toMatchObject({ linked: false, canRead: false, canWrite: false, content: '' });
322
+ expect(view.linkUrl).toContain('/embed/link?token=');
323
+ });
324
+
325
+ it('shows NO content to a viewer who may not read the file', async () => {
326
+ const { service } = build({ canRead: false });
327
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
328
+ const view = await service.loadFile(token);
329
+ expect(view).toMatchObject({ linked: true, canRead: false, content: '' });
330
+ });
331
+
332
+ it('never reports write access without read access', async () => {
333
+ const { service, accessControl } = build({ canRead: false, canWrite: true });
334
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
335
+ expect((await service.loadFile(token)).canWrite).toBe(false);
336
+ // And the write question is not even asked once read has said no.
337
+ expect(accessControl.canWrite).not.toHaveBeenCalled();
338
+ });
339
+
340
+ it.each([
341
+ ['a garbage token', 'not-a-token'],
342
+ ['a token signed with another key', jwt.sign({ scope: 'embed', kind: 'user', sub: 'u', repoRelative: REPO }, 'other')],
343
+ ['a token with the wrong scope', jwt.sign({ scope: 'session', kind: 'user', sub: 'u', repoRelative: REPO }, SIGNING_KEY)],
344
+ ['a token with no subject', jwt.sign({ scope: 'embed', repoRelative: REPO }, SIGNING_KEY)],
345
+ ])('refuses %s', async (_label, token) => {
346
+ const { service } = build();
347
+ await expect(service.loadFile(token)).rejects.toThrow(EmbedTokenError);
348
+ });
349
+
350
+ it('refuses an EXPIRED token', async () => {
351
+ const { service } = build();
352
+ const expired = jwt.sign(
353
+ { scope: 'embed', kind: 'user', sub: USER.id, repoRelative: REPO },
354
+ SIGNING_KEY,
355
+ { expiresIn: -10 },
356
+ );
357
+ await expect(service.loadFile(expired)).rejects.toThrow(EmbedTokenError);
358
+ });
359
+
360
+ /**
361
+ * An embed token is derived from the JWT secret with a fixed label, so it
362
+ * cannot be replayed as a session JWT nor the reverse. The derivation is
363
+ * part of the token format: a session token must be refused here.
364
+ */
365
+ it('refuses a token signed with the JWT secret itself', async () => {
366
+ const { service } = build();
367
+ const session = jwt.sign(
368
+ { scope: 'embed', kind: 'user', sub: USER.id, repoRelative: REPO },
369
+ 'test-jwt-secret',
370
+ );
371
+ await expect(service.loadFile(session)).rejects.toThrow(EmbedTokenError);
372
+ });
373
+
374
+ /**
375
+ * A token minted by the release BEFORE the subject was generalised keyed
376
+ * the identity on `accountId` alone. Those live two hours, so an upgrade
377
+ * would otherwise expire every open Atlassian panel on the spot.
378
+ */
379
+ it('still reads a token from the release before the subject was generalised', async () => {
380
+ const { service, accountLinks } = build();
381
+ const legacy = jwt.sign({ scope: 'embed', accountId: 'acc-legacy', repoRelative: REPO }, SIGNING_KEY);
382
+ const view = await service.loadFile(legacy);
383
+ expect(view.linked).toBe(true);
384
+ expect(accountLinks.getUserId).toHaveBeenCalledWith('acc-legacy');
385
+ });
386
+ });
387
+
388
+ describe('EmbedService: bytes', () => {
389
+ it('serves the embedded file itself', async () => {
390
+ const { service } = build({ files: { [`${KB}/Shots/x.png`]: Buffer.from([1, 2, 3]) } });
391
+ const { token } = await service.mintForUser({ userId: USER.id, reference: 'Shots/x.png' });
392
+ const { bytes, path } = await service.readBytes(token);
393
+ expect([...bytes]).toEqual([1, 2, 3]);
394
+ expect(path).toBe('Shots/x.png');
395
+ });
396
+
397
+ it('serves a picture beside the page, resolved against the page and re-gated', async () => {
398
+ const { service, accessControl } = build({
399
+ files: { [WS]: PAGE, [`${KB}/Data/assets/shot.png`]: Buffer.from([9]) },
400
+ });
401
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
402
+ const { path } = await service.readBytes(token, './assets/shot.png');
403
+ expect(path).toBe('Data/assets/shot.png');
404
+ // The token scopes the view to one page, never to one page's PERMISSIONS:
405
+ // the file actually served is checked on its own.
406
+ expect(accessControl.canRead).toHaveBeenCalledWith(expect.anything(), USER.email, 'Data/assets/shot.png');
407
+ });
408
+
409
+ /** The form the embed view sends: a renderer names files by workspace path. */
410
+ it('serves a picture named from the repository root', async () => {
411
+ const { service } = build({
412
+ files: { [WS]: PAGE, [`${KB}/Data/assets/shot.png`]: Buffer.from([9]) },
413
+ });
414
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
415
+ const { bytes, path } = await service.readBytes(token, '/Data/assets/shot.png');
416
+ expect(path).toBe('Data/assets/shot.png');
417
+ expect([...bytes]).toEqual([9]);
418
+ });
419
+
420
+ it('refuses another page beside the embedded one: the token shows one page', async () => {
421
+ const { service, workspaceService } = build({
422
+ files: { [WS]: PAGE, [`${KB}/Data/Other.md`]: '# Other\n', [`${KB}/Other.md`]: '# Root\n' },
423
+ });
424
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
425
+ await expect(service.readBytes(token, './Other.md')).rejects.toThrow(EmbedAccessError);
426
+ await expect(service.readBytes(token, '/Other.md')).rejects.toThrow(EmbedAccessError);
427
+ // The page itself, under any spelling, is what the token is for.
428
+ const { path } = await service.readBytes(token, `/${REPO}`);
429
+ expect(path).toBe(REPO);
430
+ // An SVG is text, and the one text format a page draws as a picture.
431
+ const { service: withSvg } = build({ files: { [WS]: PAGE, [`${KB}/Data/d.svg`]: '<svg/>' } });
432
+ const svg = await withSvg.mintForUser({ userId: USER.id, reference: REPO });
433
+ expect((await withSvg.readBytes(svg.token, './d.svg')).path).toBe('Data/d.svg');
434
+ expect(workspaceService.readFileBinary).not.toHaveBeenCalledWith(expect.anything(), `${KB}/Other.md`);
435
+ expect(workspaceService.readFileBinary).not.toHaveBeenCalledWith(expect.anything(), `${KB}/Data/Other.md`);
436
+ });
437
+
438
+ it('refuses the outside-the-knowledge-base form the view sends', async () => {
439
+ const { service } = build();
440
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
441
+ await expect(service.readBytes(token, '/..')).rejects.toThrow(EmbedAccessError);
442
+ });
443
+
444
+ it('refuses a path that climbs out of the repository', async () => {
445
+ const { service } = build();
446
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
447
+ await expect(service.readBytes(token, '../../../etc/passwd')).rejects.toThrow(EmbedAccessError);
448
+ });
449
+
450
+ it('refuses a path the viewer may not read', async () => {
451
+ const { service } = build({ canRead: false });
452
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
453
+ await expect(service.readBytes(token)).rejects.toThrow(EmbedAccessError);
454
+ });
455
+ });
456
+
457
+ describe('EmbedService: saving as a writer', () => {
458
+ it('writes to the default branch and releases the lock, which commits', async () => {
459
+ const { service, workspaceService, workflowService } = build();
460
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
461
+ await service.acquireLock(token);
462
+ await service.save(token, 'new text');
463
+ expect(workflowService.acquireLock).toHaveBeenCalledWith(
464
+ encodeURIComponent(BRANCH),
465
+ BRANCH,
466
+ WS,
467
+ expect.objectContaining({ id: USER.id }),
468
+ );
469
+ expect(workspaceService.writeFile).toHaveBeenCalledWith(encodeURIComponent(BRANCH), WS, 'new text');
470
+ expect(workflowService.releaseLock).toHaveBeenCalledWith(
471
+ encodeURIComponent(BRANCH),
472
+ BRANCH,
473
+ WS,
474
+ expect.objectContaining({ id: USER.id }),
475
+ );
476
+ });
477
+
478
+ it('names the holder when somebody else has the lock', async () => {
479
+ const { service } = build({ acquired: false, holderName: 'Bob' });
480
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
481
+ expect(await service.acquireLock(token)).toEqual({ acquired: false, holderName: 'Bob' });
482
+ });
483
+
484
+ /**
485
+ * A frame hidden past the lock's TTL can come back to find the file taken.
486
+ * Its Save must not land: written first and refused at the release, the
487
+ * text would sit on disk under the OTHER editor's lock, for their next
488
+ * commit to publish.
489
+ */
490
+ it('writes nothing when the viewer no longer holds the lock', async () => {
491
+ const { service, workspaceService, workflowService } = build({ acquired: false, holderName: 'Bob' });
492
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
493
+ await expect(service.save(token, 'x')).rejects.toThrow(EmbedLockedError);
494
+ await expect(service.save(token, 'x')).rejects.toThrow('Bob');
495
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
496
+ expect(workflowService.releaseLock).not.toHaveBeenCalled();
497
+ });
498
+
499
+ /**
500
+ * The lock is let go on the SERVER at that moment, not left to the view's
501
+ * cancel or the TTL: a frame that is gone, or a host that never delivers
502
+ * the refusal, would otherwise keep the file shut to the writers who still
503
+ * have access.
504
+ */
505
+ it('refuses to keep a lock alive for a viewer who lost write access, and releases it', async () => {
506
+ const { service, accessControl, workflowService } = build();
507
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
508
+ accessControl.canWrite.mockResolvedValue(false);
509
+ await expect(service.heartbeat(token)).rejects.toThrow(EmbedAccessError);
510
+ expect(workflowService.heartbeatLock).not.toHaveBeenCalled();
511
+ expect(workflowService.releaseLockNoCommit).toHaveBeenCalledWith(
512
+ expect.any(String),
513
+ BRANCH,
514
+ `${KB}/${REPO}`,
515
+ expect.objectContaining({ id: USER.id }),
516
+ );
517
+ expect(workflowService.releaseLock).not.toHaveBeenCalled();
518
+ });
519
+
520
+ it('releases the lock WITHOUT committing when the write fails, and rethrows', async () => {
521
+ const { service, workspaceService, workflowService } = build();
522
+ workspaceService.writeFile.mockRejectedValueOnce(new Error('disk full'));
523
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
524
+ await expect(service.save(token, 'x')).rejects.toThrow('disk full');
525
+ expect(workflowService.releaseLockNoCommit).toHaveBeenCalled();
526
+ expect(workflowService.releaseLock).not.toHaveBeenCalled();
527
+ });
528
+
529
+ it('refuses Edit and Save to a viewer without write access', async () => {
530
+ const { service, workspaceService } = build({ canWrite: false });
531
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
532
+ await expect(service.acquireLock(token)).rejects.toThrow(EmbedAccessError);
533
+ await expect(service.save(token, 'x')).rejects.toThrow(EmbedAccessError);
534
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
535
+ });
536
+
537
+ it('refuses to save text over bytes that are not text, and says so in the load', async () => {
538
+ // A PNG under a markdown name: the fallback reader is text-editable, but
539
+ // what it answers for these bytes is a refusal, not the file's text.
540
+ const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0]);
541
+ const { service, workspaceService } = build({ files: { [WS]: png } });
542
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
543
+ const view = await service.loadFile(token);
544
+ expect(view.contentIsText).toBe(false);
545
+ expect(view.canWrite).toBe(false);
546
+ await expect(service.save(token, '# replaced')).rejects.toThrow(EmbedAccessError);
547
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
548
+ });
549
+
550
+ it('refuses to edit as an identity that does not resolve', async () => {
551
+ const { service } = build({ linkedUserId: null });
552
+ const { token } = await service.mintToken({ accountId: 'acc-1', reference: REPO });
553
+ await expect(service.acquireLock(token)).rejects.toThrow(EmbedAccessError);
554
+ });
555
+ });
556
+
557
+ describe('EmbedService: proposing as a non-writer', () => {
558
+ it('commits to the viewer OWN suggestions branch and opens a change request', async () => {
559
+ const { service, workspaceService, workflowService } = build({ canWrite: false });
560
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
561
+ const result = await service.propose(token, 'proposed text');
562
+
563
+ const branch = 'suggestions/alice-u-1/knowledge';
564
+ expect(workflowService.createBranch).toHaveBeenCalledWith(encodeURIComponent(BRANCH), branch, BRANCH);
565
+ expect(workspaceService.writeFile).toHaveBeenCalledWith(encodeURIComponent(branch), WS, 'proposed text');
566
+ // The write and its commit run inside the workspace service's own
567
+ // per-path turn — the queue every other write surface takes — so two
568
+ // proposals for one file from one person cannot interleave.
569
+ expect(workspaceService.withPathTurn).toHaveBeenCalledWith(encodeURIComponent(branch), WS, expect.any(Function));
570
+ const turn = workspaceService.withPathTurn.mock.invocationCallOrder[0]!;
571
+ expect(workspaceService.writeFile.mock.invocationCallOrder[0]).toBeGreaterThan(turn);
572
+ expect(workflowService.commitChanges.mock.invocationCallOrder[0]).toBeGreaterThan(turn);
573
+ /**
574
+ * COMMITTED, and committed BEFORE the request is opened.
575
+ *
576
+ * `writeFile` only puts bytes on disk. Without this the branch head never
577
+ * moved, and the request opened against a tree identical to its base:
578
+ * `changedFiles: 0`, `headSha === baseSha`, and a reviewer told "this pull
579
+ * request has no file changes to approve" about a proposal sitting
580
+ * uncommitted on disk. Found on a real boot, not by a stub.
581
+ */
582
+ expect(workflowService.commitChanges).toHaveBeenCalledWith(
583
+ encodeURIComponent(branch),
584
+ expect.objectContaining({ id: USER.id }),
585
+ expect.stringContaining(REPO),
586
+ // Scoped to this one path: the suggestions branch carries everything
587
+ // this person has proposed, and a bare commit would sweep another
588
+ // in-flight write of theirs in under this message.
589
+ [WS],
590
+ );
591
+ expect(workflowService.commitChanges.mock.invocationCallOrder[0]).toBeLessThan(
592
+ workflowService.openChangeRequest.mock.invocationCallOrder[0],
593
+ );
594
+ expect(workflowService.openChangeRequest).toHaveBeenCalledWith(
595
+ encodeURIComponent(branch),
596
+ expect.objectContaining({ id: USER.id }),
597
+ { sourceBranch: branch, targetBranch: BRANCH, title: 'Changes from Alice. Knowledge' },
598
+ );
599
+ expect(result).toEqual({ branch, number: 42, url: 'https://hexis.example/change-requests/42' });
600
+ });
601
+
602
+ it('touches NOTHING on the default branch — no lock, no write there', async () => {
603
+ const { service, workspaceService, workflowService } = build({ canWrite: false });
604
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
605
+ await service.propose(token, 'proposed text');
606
+ expect(workflowService.acquireLock).not.toHaveBeenCalled();
607
+ expect(workspaceService.writeFile).not.toHaveBeenCalledWith(
608
+ encodeURIComponent(BRANCH),
609
+ expect.anything(),
610
+ expect.anything(),
611
+ );
612
+ });
613
+
614
+ it('reuses an existing suggestions branch rather than failing on it', async () => {
615
+ const createBranch = vi.fn(async () => {
616
+ throw new Error('a branch named "suggestions/alice-u-1/knowledge" already exists');
617
+ });
618
+ const { service, workflowService } = build({ canWrite: false, createBranch });
619
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
620
+ await expect(service.propose(token, 'x')).resolves.toMatchObject({ number: 42 });
621
+ expect(workflowService.openChangeRequest).toHaveBeenCalled();
622
+ });
623
+
624
+ it('a second proposal finds the branch on origin and does not try to create it again', async () => {
625
+ // Created and pushed by the first proposal, the branch is on origin; a
626
+ // second `createBranch` would fail as a non-fast-forward push, not as
627
+ // "already exists" — so origin is asked first.
628
+ const createBranch = vi.fn(async () => {
629
+ throw new Error('git push failed: ! [rejected] suggestions/alice-u-1/knowledge (non-fast-forward)');
630
+ });
631
+ const { service, workflowService } = build({ canWrite: false, createBranch, remoteBranchExists: true });
632
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
633
+ await expect(service.propose(token, 'again')).resolves.toMatchObject({ number: 42 });
634
+ expect(createBranch).not.toHaveBeenCalled();
635
+ expect(workflowService.openChangeRequest).toHaveBeenCalled();
636
+ });
637
+
638
+ /**
639
+ * The person's one open Knowledge request already covers this branch — which
640
+ * is the state `propose` is trying to reach, not a failure. The refusal
641
+ * names it, so the view points at it.
642
+ */
643
+ it('adopts the request that already covers the branch', async () => {
644
+ const duplicate = Object.assign(new Error('An open change request already exists (#7).'), {
645
+ status: 409,
646
+ payload: { kind: 'duplicate-change-request', existingNumber: 7 },
647
+ existingNumber: 7,
648
+ });
649
+ const openChangeRequest = vi.fn(async () => {
650
+ throw duplicate;
651
+ });
652
+ const { service } = build({ canWrite: false, openChangeRequest });
653
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
654
+ await expect(service.propose(token, 'x')).resolves.toMatchObject({
655
+ number: 7,
656
+ url: 'https://hexis.example/change-requests/7',
657
+ });
658
+ });
659
+
660
+ it('refuses to propose on a file the viewer may not read', async () => {
661
+ const { service, workspaceService } = build({ canRead: false });
662
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
663
+ await expect(service.propose(token, 'x')).rejects.toThrow(EmbedAccessError);
664
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
665
+ });
666
+
667
+ it('refuses to propose text over bytes that are not text, as a save is refused', async () => {
668
+ // The same PNG-under-a-markdown-name the save test uses: a proposal is a
669
+ // commit of text, and must not commit text over bytes a Save refuses.
670
+ const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0]);
671
+ const { service, workspaceService, workflowService } = build({
672
+ canWrite: false,
673
+ files: { [WS]: png },
674
+ });
675
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
676
+ await expect(service.propose(token, '# replaced')).rejects.toThrow(EmbedAccessError);
677
+ expect(workflowService.createBranch).not.toHaveBeenCalled();
678
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
679
+ expect(workflowService.commitChanges).not.toHaveBeenCalled();
680
+ expect(workflowService.openChangeRequest).not.toHaveBeenCalled();
681
+ });
682
+ });
683
+
684
+ /**
685
+ * The embed writes through `workspaceService.writeFile`, which puts bytes on
686
+ * disk with no gate of its own — the app's editor and the agent tools write
687
+ * through a filesystem that refuses a `roles.yaml` that would not parse. The
688
+ * embed asks that same gate first, so a chat cannot commit what the app
689
+ * would refuse.
690
+ */
691
+ describe('EmbedService: a switched-off account', () => {
692
+ it('reads, saves and proposes nothing on a token minted for it, and is told why', async () => {
693
+ const { service, workspaceService, workflowService } = build({ active: false });
694
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
695
+ for (const call of [
696
+ () => service.loadFile(token),
697
+ () => service.readBytes(token),
698
+ () => service.acquireLock(token),
699
+ () => service.save(token, 'x'),
700
+ () => service.propose(token, 'x'),
701
+ ]) {
702
+ // One invocation, both checks on its one rejection.
703
+ const err: unknown = await call().then(() => null, (e: unknown) => e);
704
+ expect(err).toBeInstanceOf(EmbedAccessError);
705
+ expect((err as Error).message).toBe(ACCOUNT_DEACTIVATED_MESSAGE);
706
+ }
707
+ expect(workspaceService.readFileBinary).not.toHaveBeenCalled();
708
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
709
+ expect(workflowService.acquireLock).not.toHaveBeenCalled();
710
+ expect(workflowService.commitChanges).not.toHaveBeenCalled();
711
+ });
712
+ });
713
+
714
+ describe('EmbedService: the roles.yaml write gate', () => {
715
+ const ROLES = `${KB}/roles.yaml`;
716
+ const VALID = 'roles:\n Admin:\n - a@x.eu\n';
717
+
718
+ it('refuses to save a roles.yaml that would not parse, writing nothing and taking no lock', async () => {
719
+ const { service, workspaceService, workflowService } = build({ files: { [ROLES]: VALID } });
720
+ const { token } = await service.mintForUser({ userId: USER.id, reference: 'roles.yaml' });
721
+ await expect(service.save(token, 'roles: [oops')).rejects.toThrow(RolesYamlInvalidError);
722
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
723
+ expect(workflowService.acquireLock).not.toHaveBeenCalled();
724
+ expect(workflowService.releaseLock).not.toHaveBeenCalled();
725
+ });
726
+
727
+ it('refuses to propose a roles.yaml that would not parse, before any branch is made', async () => {
728
+ const { service, workspaceService, workflowService } = build({
729
+ canWrite: false,
730
+ files: { [ROLES]: VALID },
731
+ });
732
+ const { token } = await service.mintForUser({ userId: USER.id, reference: 'roles.yaml' });
733
+ await expect(service.propose(token, 'roles: [oops')).rejects.toThrow(RolesYamlInvalidError);
734
+ expect(workflowService.createBranch).not.toHaveBeenCalled();
735
+ expect(workspaceService.writeFile).not.toHaveBeenCalled();
736
+ expect(workflowService.commitChanges).not.toHaveBeenCalled();
737
+ });
738
+
739
+ it('saves a roles.yaml that parses, and any other file untouched by the gate', async () => {
740
+ const { service, workspaceService } = build({ files: { [ROLES]: VALID, [WS]: PAGE } });
741
+ const roles = await service.mintForUser({ userId: USER.id, reference: 'roles.yaml' });
742
+ await service.save(roles.token, `${VALID} Editor:\n - e@x.eu\n`);
743
+ expect(workspaceService.writeFile).toHaveBeenCalledWith(encodeURIComponent(BRANCH), ROLES, expect.any(String));
744
+ const page = await service.mintForUser({ userId: USER.id, reference: REPO });
745
+ await service.save(page.token, 'roles: [oops');
746
+ expect(workspaceService.writeFile).toHaveBeenCalledWith(encodeURIComponent(BRANCH), WS, 'roles: [oops');
747
+ });
748
+ });
749
+
750
+ describe('EmbedService: account links', () => {
751
+ it('links the token account to the signed-in user', async () => {
752
+ const { service, accountLinks } = build();
753
+ const { token } = await service.mintToken({ accountId: 'acc-9', reference: REPO });
754
+ await service.linkAccount(token, 'u-2');
755
+ expect(accountLinks.link).toHaveBeenCalledWith('acc-9', 'u-2');
756
+ });
757
+
758
+ it('refuses to re-point a token that already names a Hexis user', async () => {
759
+ const { service, accountLinks } = build();
760
+ const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
761
+ await expect(service.linkAccount(token, 'u-2')).rejects.toThrow(EmbedAccessError);
762
+ expect(accountLinks.link).not.toHaveBeenCalled();
763
+ });
764
+
765
+ it('lists and unlinks a user own links', async () => {
766
+ const { service, accountLinks } = build();
767
+ expect(await service.listLinkedAccounts('u-1')).toEqual([
768
+ { atlassianAccountId: 'acc-1', createdAt: 0 },
769
+ ]);
770
+ expect(await service.unlinkAccount('u-1', 'acc-1')).toBe(true);
771
+ expect(accountLinks.unlink).toHaveBeenCalledWith('u-1', 'acc-1');
772
+ });
773
+ });
774
+
775
+ describe('resolveBeside', () => {
776
+ it.each([
777
+ ['a sibling', 'Data/Thing.md', 'shot.png', 'Data/shot.png'],
778
+ ['a child folder', 'Data/Thing.md', './assets/shot.png', 'Data/assets/shot.png'],
779
+ ['one level up', 'Data/Deep/Thing.md', '../shot.png', 'Data/shot.png'],
780
+ ['a repo-absolute path', 'Data/Thing.md', '/Other/shot.png', 'Other/shot.png'],
781
+ ['a percent-encoded name', 'Data/Thing.md', 'a%20shot.png', 'Data/a shot.png'],
782
+ ])('resolves %s', (_label, from, path, expected) => {
783
+ expect(resolveBeside(from, path)).toBe(expected);
784
+ });
785
+
786
+ it.each([
787
+ ['traversal past the root', 'Data/Thing.md', '../../../etc/passwd'],
788
+ ['a control character', 'Data/Thing.md', 'a\nb.png'],
789
+ ['a malformed escape', 'Data/Thing.md', '%zz.png'],
790
+ ['a path that resolves to nothing', 'Thing.md', './'],
791
+ ])('refuses %s', (_label, from, path) => {
792
+ expect(resolveBeside(from, path)).toBeNull();
793
+ });
794
+
795
+ /**
796
+ * `./` beside a file in a folder resolves to the FOLDER, which is not a
797
+ * file. Nothing downstream is fooled — the read fails with the same
798
+ * not-found a missing file gives — and it stays inside the repository,
799
+ * which is the property this function is responsible for.
800
+ */
801
+ it('stays inside the repository even when a path names a folder', () => {
802
+ expect(resolveBeside('Data/Deep/Thing.md', './')).toBe('Data/Deep');
803
+ expect(resolveBeside('Data/Deep/Thing.md', '../')).toBe('Data');
804
+ });
805
+ });