@bevel-software/platform-core-backend 0.4.0 → 0.5.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 (305) hide show
  1. package/dist/core/core-ports.d.ts +15 -0
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +40 -6
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +17 -1
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +153 -29
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/core-config.d.ts +70 -21
  12. package/dist/core-config.d.ts.map +1 -1
  13. package/dist/core-config.js +134 -58
  14. package/dist/core-config.js.map +1 -1
  15. package/dist/modules/access/access-control.interface.d.ts +6 -0
  16. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.d.ts +74 -5
  18. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  19. package/dist/modules/access/access-control.service.js +116 -15
  20. package/dist/modules/access/access-control.service.js.map +1 -1
  21. package/dist/modules/access/access-declarations.d.ts +93 -0
  22. package/dist/modules/access/access-declarations.d.ts.map +1 -0
  23. package/dist/modules/access/access-declarations.js +168 -0
  24. package/dist/modules/access/access-declarations.js.map +1 -0
  25. package/dist/modules/access/access-mutation.service.js +4 -4
  26. package/dist/modules/access/access-mutation.service.js.map +1 -1
  27. package/dist/modules/access/access-splice.d.ts +16 -1
  28. package/dist/modules/access/access-splice.d.ts.map +1 -1
  29. package/dist/modules/access/access-splice.js +40 -21
  30. package/dist/modules/access/access-splice.js.map +1 -1
  31. package/dist/modules/access/access.routes.d.ts.map +1 -1
  32. package/dist/modules/access/access.routes.js +51 -0
  33. package/dist/modules/access/access.routes.js.map +1 -1
  34. package/dist/modules/access/creator-access.d.ts +8 -0
  35. package/dist/modules/access/creator-access.d.ts.map +1 -1
  36. package/dist/modules/access/creator-access.js +21 -2
  37. package/dist/modules/access/creator-access.js.map +1 -1
  38. package/dist/modules/admin/admin-access.service.d.ts +1 -1
  39. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  40. package/dist/modules/admin/admin-access.service.js +3 -2
  41. package/dist/modules/admin/admin-access.service.js.map +1 -1
  42. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  43. package/dist/modules/auth/auth.routes.js +50 -1
  44. package/dist/modules/auth/auth.routes.js.map +1 -1
  45. package/dist/modules/auth/auth.service.d.ts +39 -10
  46. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  47. package/dist/modules/auth/auth.service.js +38 -16
  48. package/dist/modules/auth/auth.service.js.map +1 -1
  49. package/dist/modules/database/core-schema.d.ts +124 -0
  50. package/dist/modules/database/core-schema.d.ts.map +1 -1
  51. package/dist/modules/database/core-schema.js +45 -0
  52. package/dist/modules/database/core-schema.js.map +1 -1
  53. package/dist/modules/groups/group-provision.service.d.ts +130 -0
  54. package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
  55. package/dist/modules/groups/group-provision.service.js +288 -0
  56. package/dist/modules/groups/group-provision.service.js.map +1 -0
  57. package/dist/modules/groups/groups.contract.d.ts +106 -0
  58. package/dist/modules/groups/groups.contract.d.ts.map +1 -0
  59. package/dist/modules/groups/groups.contract.js +36 -0
  60. package/dist/modules/groups/groups.contract.js.map +1 -0
  61. package/dist/modules/groups/groups.routes.d.ts +42 -0
  62. package/dist/modules/groups/groups.routes.d.ts.map +1 -0
  63. package/dist/modules/groups/groups.routes.js +379 -0
  64. package/dist/modules/groups/groups.routes.js.map +1 -0
  65. package/dist/modules/groups/groups.service.d.ts +60 -0
  66. package/dist/modules/groups/groups.service.d.ts.map +1 -0
  67. package/dist/modules/groups/groups.service.js +172 -0
  68. package/dist/modules/groups/groups.service.js.map +1 -0
  69. package/dist/modules/groups/index.d.ts +7 -0
  70. package/dist/modules/groups/index.d.ts.map +1 -0
  71. package/dist/modules/groups/index.js +6 -0
  72. package/dist/modules/groups/index.js.map +1 -0
  73. package/dist/modules/groups/join-proposals.d.ts +53 -0
  74. package/dist/modules/groups/join-proposals.d.ts.map +1 -0
  75. package/dist/modules/groups/join-proposals.js +67 -0
  76. package/dist/modules/groups/join-proposals.js.map +1 -0
  77. package/dist/modules/groups/join-requests.service.d.ts +81 -0
  78. package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
  79. package/dist/modules/groups/join-requests.service.js +135 -0
  80. package/dist/modules/groups/join-requests.service.js.map +1 -0
  81. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
  82. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  83. package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
  84. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  85. package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
  86. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
  87. package/dist/modules/settings/deployment-settings.service.js +368 -0
  88. package/dist/modules/settings/deployment-settings.service.js.map +1 -0
  89. package/dist/modules/settings/setup.routes.d.ts +49 -0
  90. package/dist/modules/settings/setup.routes.d.ts.map +1 -0
  91. package/dist/modules/settings/setup.routes.js +292 -0
  92. package/dist/modules/settings/setup.routes.js.map +1 -0
  93. package/dist/modules/skills/index.d.ts +2 -1
  94. package/dist/modules/skills/index.d.ts.map +1 -1
  95. package/dist/modules/skills/index.js +1 -0
  96. package/dist/modules/skills/index.js.map +1 -1
  97. package/dist/modules/skills/pending-skills.service.d.ts +40 -0
  98. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
  99. package/dist/modules/skills/pending-skills.service.js +147 -0
  100. package/dist/modules/skills/pending-skills.service.js.map +1 -0
  101. package/dist/modules/skills/skills.contract.d.ts +32 -3
  102. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  103. package/dist/modules/skills/skills.contract.js +1 -1
  104. package/dist/modules/skills/skills.routes.d.ts +3 -2
  105. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  106. package/dist/modules/skills/skills.routes.js +18 -1
  107. package/dist/modules/skills/skills.routes.js.map +1 -1
  108. package/dist/modules/skills/skills.service.d.ts +22 -0
  109. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  110. package/dist/modules/skills/skills.service.js +24 -12
  111. package/dist/modules/skills/skills.service.js.map +1 -1
  112. package/dist/modules/tool-manuals/index.d.ts +1 -1
  113. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  114. package/dist/modules/tool-manuals/index.js.map +1 -1
  115. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
  116. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  117. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  118. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
  119. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  120. package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
  121. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  122. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
  123. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  124. package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
  125. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  126. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  127. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
  128. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  129. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  130. package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
  131. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  132. package/dist/modules/workflow/event-bus.d.ts +11 -0
  133. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  134. package/dist/modules/workflow/event-bus.js +22 -0
  135. package/dist/modules/workflow/event-bus.js.map +1 -1
  136. package/dist/modules/workflow/file-lock.service.d.ts +9 -0
  137. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  138. package/dist/modules/workflow/file-lock.service.js +16 -0
  139. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  140. package/dist/modules/workflow/git/branch-name.d.ts +1 -1
  141. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
  142. package/dist/modules/workflow/git/branch-name.js +1 -1
  143. package/dist/modules/workflow/git/branch-name.js.map +1 -1
  144. package/dist/modules/workflow/git/git.service.d.ts +31 -2
  145. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  146. package/dist/modules/workflow/git/git.service.js +157 -54
  147. package/dist/modules/workflow/git/git.service.js.map +1 -1
  148. package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
  149. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  150. package/dist/modules/workflow/git/pull-request.service.js +19 -9
  151. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  152. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  153. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  154. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  155. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  156. package/dist/modules/workflow/workflow.routes.js +95 -14
  157. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  158. package/dist/modules/workflow/workflow.service.d.ts +97 -5
  159. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  160. package/dist/modules/workflow/workflow.service.js +393 -114
  161. package/dist/modules/workflow/workflow.service.js.map +1 -1
  162. package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
  163. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
  164. package/dist/modules/workspace/bevel-ignore.js +5 -1
  165. package/dist/modules/workspace/bevel-ignore.js.map +1 -1
  166. package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
  167. package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
  168. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  169. package/dist/modules/workspace/kb-seed.service.js +228 -51
  170. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  171. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  172. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  173. package/dist/modules/workspace/workspace.routes.js +51 -18
  174. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  175. package/dist/modules/workspace/workspace.service.d.ts +16 -4
  176. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  177. package/dist/modules/workspace/workspace.service.js +36 -9
  178. package/dist/modules/workspace/workspace.service.js.map +1 -1
  179. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  180. package/dist/modules/workspace/workspace.tools.js +8 -3
  181. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  182. package/dist/shared/kb-layout.test.js +2 -2
  183. package/dist/test-setup.d.ts +2 -0
  184. package/dist/test-setup.d.ts.map +1 -0
  185. package/dist/test-setup.js +18 -0
  186. package/dist/test-setup.js.map +1 -0
  187. package/kb-template/.bevelignore +6 -2
  188. package/kb-template/AGENTS.md +272 -0
  189. package/kb-template/KnowledgeBase/How to get started.md +65 -0
  190. package/kb-template/access.md +36 -36
  191. package/migrations/0002_user_onboarding_done.sql +1 -0
  192. package/migrations/0003_deployment_settings.sql +9 -0
  193. package/migrations/meta/0001_snapshot.json +1486 -0
  194. package/migrations/meta/0002_snapshot.json +1493 -0
  195. package/migrations/meta/0003_snapshot.json +1552 -0
  196. package/migrations/meta/_journal.json +15 -1
  197. package/package.json +3 -3
  198. package/src/__tests__/branch-model.test.ts +79 -0
  199. package/src/__tests__/core-config.admin.test.ts +114 -0
  200. package/src/__tests__/database-url.test.ts +70 -0
  201. package/src/core/core-ports.ts +15 -0
  202. package/src/core/create-core-server.ts +55 -6
  203. package/src/core/create-core-services.ts +201 -36
  204. package/src/core-config.ts +139 -59
  205. package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
  206. package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
  207. package/src/modules/access/__tests__/access-splice.test.ts +21 -0
  208. package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
  209. package/src/modules/access/access-control.interface.ts +11 -0
  210. package/src/modules/access/access-control.service.ts +113 -15
  211. package/src/modules/access/access-declarations.ts +251 -0
  212. package/src/modules/access/access-mutation.service.ts +4 -4
  213. package/src/modules/access/access-splice.ts +64 -21
  214. package/src/modules/access/access.routes.ts +61 -0
  215. package/src/modules/access/creator-access.ts +22 -2
  216. package/src/modules/admin/admin-access.service.ts +4 -3
  217. package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
  218. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
  219. package/src/modules/auth/auth.routes.ts +50 -1
  220. package/src/modules/auth/auth.service.ts +55 -17
  221. package/src/modules/database/core-schema.ts +46 -0
  222. package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
  223. package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
  224. package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
  225. package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
  226. package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
  227. package/src/modules/groups/group-provision.service.ts +328 -0
  228. package/src/modules/groups/groups.contract.ts +107 -0
  229. package/src/modules/groups/groups.routes.ts +433 -0
  230. package/src/modules/groups/groups.service.ts +193 -0
  231. package/src/modules/groups/index.ts +17 -0
  232. package/src/modules/groups/join-proposals.ts +114 -0
  233. package/src/modules/groups/join-requests.service.ts +185 -0
  234. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
  235. package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
  236. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
  237. package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
  238. package/src/modules/settings/deployment-settings.service.ts +419 -0
  239. package/src/modules/settings/setup.routes.ts +317 -0
  240. package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
  241. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  242. package/src/modules/skills/index.ts +3 -0
  243. package/src/modules/skills/pending-skills.service.ts +168 -0
  244. package/src/modules/skills/skills.contract.ts +34 -3
  245. package/src/modules/skills/skills.routes.ts +23 -2
  246. package/src/modules/skills/skills.service.ts +24 -16
  247. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
  248. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  249. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
  250. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
  251. package/src/modules/tool-manuals/index.ts +2 -0
  252. package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
  253. package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
  254. package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
  255. package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
  256. package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
  257. package/src/modules/workflow/__tests__/pending-commits.enqueue-if-absent.test.ts +82 -72
  258. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +295 -5
  259. package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
  260. package/src/modules/workflow/event-bus.ts +25 -0
  261. package/src/modules/workflow/file-lock.service.ts +17 -0
  262. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
  263. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
  264. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
  265. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
  266. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
  267. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
  268. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +311 -306
  269. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
  270. package/src/modules/workflow/git/branch-name.ts +5 -1
  271. package/src/modules/workflow/git/git.service.ts +172 -64
  272. package/src/modules/workflow/git/pull-request.service.ts +22 -11
  273. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
  274. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  275. package/src/modules/workflow/workflow.routes.ts +91 -13
  276. package/src/modules/workflow/workflow.service.ts +365 -19
  277. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
  278. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
  279. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
  280. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
  281. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
  283. package/src/modules/workspace/bevel-ignore.ts +66 -62
  284. package/src/modules/workspace/kb-seed.interface.ts +1 -1
  285. package/src/modules/workspace/kb-seed.service.ts +247 -53
  286. package/src/modules/workspace/workspace.routes.ts +54 -18
  287. package/src/modules/workspace/workspace.service.ts +45 -7
  288. package/src/modules/workspace/workspace.tools.ts +922 -917
  289. package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
  290. package/src/shared/__tests__/join-request.test.ts +96 -0
  291. package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
  292. package/src/shared/kb-layout.test.ts +2 -2
  293. package/src/test-setup.ts +18 -0
  294. package/kb-template/Agents/README.md +0 -38
  295. package/kb-template/CLAUDE.md +0 -419
  296. package/kb-template/Data/.gitkeep +0 -0
  297. package/kb-template/Data/README.md +0 -39
  298. package/kb-template/KnowledgeBase/README.md +0 -37
  299. package/kb-template/Pipelines/.gitkeep +0 -0
  300. package/kb-template/Pipelines/README.md +0 -38
  301. package/kb-template/Skills/.gitkeep +0 -0
  302. package/kb-template/Skills/README.md +0 -32
  303. package/kb-template/Tools/.gitkeep +0 -0
  304. package/kb-template/Tools/README.md +0 -42
  305. /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
@@ -1,233 +1,236 @@
1
- import type { Server } from 'node:http';
2
- import type { AddressInfo } from 'node:net';
3
- import express from 'express';
4
- import { describe, it, expect, afterEach, vi } from 'vitest';
5
- import type { IWorkflowService } from '@bevel-software/platform-shared';
6
- import type { IAccessControl } from '../../access/access-control.interface.js';
7
- import type { ICreatorAccess } from '../../access/creator-access.js';
8
- import type { WorkflowEventBus } from '../../workflow/event-bus.js';
9
- import type { AuthService } from '../../auth/auth.service.js';
10
- import { createWorkspaceRoutes } from '../workspace.routes.js';
11
- import type { WorkspaceService } from '../workspace.service.js';
12
-
13
- /**
14
- * Contract test for the creator read-grant hooks on the creation routes:
15
- * PUT /file (frontmatter transform + subtree seed), POST /directory (subtree
16
- * seed before the .gitkeep cycle), POST /unzip (in-place grant per extracted
17
- * file). The planner itself is unit-tested in creator-access.test.ts — here
18
- * it's mocked to return canned plans and the assertion is on what the route
19
- * writes, locks, and invalidates.
20
- */
21
-
22
- const USER_ID = 'user-1';
23
- const USER = { id: USER_ID, email: 'alice@example.com', name: 'Alice' };
24
- const WS = 'feat-branch';
25
- const KB = 'knowledge-base';
26
-
27
- interface Harness {
28
- server: Server;
29
- baseUrl: string;
30
- writes: Array<{ path: string; content: string }>;
31
- lockedPaths: string[];
32
- writeFileMock: ReturnType<typeof vi.fn>;
33
- creatorAccess: {
34
- planForCreate: ReturnType<typeof vi.fn>;
35
- grantInExtractedFile: ReturnType<typeof vi.fn>;
36
- noteAccessFileWritten: ReturnType<typeof vi.fn>;
37
- };
38
- }
39
-
40
- async function makeHarness(opts: { extracted?: string[] } = {}): Promise<Harness> {
41
- const writes: Array<{ path: string; content: string }> = [];
42
- const lockedPaths: string[] = [];
43
-
44
- const writeFileMock = vi.fn(async (_id: string, p: string, content: string) => {
45
- writes.push({ path: p, content });
46
- });
47
- const workspaceService = {
48
- writeFile: writeFileMock,
49
- writeFileBinary: vi.fn(async (_id: string, p: string, data: Uint8Array) => {
50
- writes.push({ path: p, content: Buffer.from(data).toString('utf8') });
51
- }),
52
- createDirectory: vi.fn(async () => undefined),
53
- unzipFile: vi.fn(async () => ({ extracted: opts.extracted ?? [] })),
54
- } as unknown as WorkspaceService;
55
-
56
- const workflowService = {
57
- getLock: vi.fn(async () => null),
58
- acquireLock: vi.fn(async (_w: string, _b: string, p: string) => {
59
- lockedPaths.push(p);
60
- return { acquired: true, lock: { holderUserId: USER_ID, holderName: 'Alice' } };
61
- }),
62
- releaseLock: vi.fn(async () => null),
63
- releaseLockNoCommit: vi.fn(async () => undefined),
64
- } as unknown as IWorkflowService;
65
-
66
- const creatorAccess = {
67
- planForCreate: vi.fn(async () => null),
68
- grantInExtractedFile: vi.fn(async () => null),
69
- noteAccessFileWritten: vi.fn(),
70
- };
71
-
72
- const authService = { getUserById: vi.fn(async () => USER) } as unknown as AuthService;
73
- const eventBus = { emit: vi.fn() } as unknown as WorkflowEventBus;
74
-
75
- const app = express();
76
- app.use(express.json());
77
- app.use('/api', (req, _res, next) => {
78
- (req as unknown as { userId: string }).userId = USER_ID;
79
- next();
80
- });
81
- app.use(
82
- '/api',
83
- createWorkspaceRoutes(
84
- workspaceService,
85
- authService,
86
- workflowService,
87
- eventBus,
88
- {} as unknown as IAccessControl,
89
- KB,
90
- creatorAccess as unknown as ICreatorAccess,
91
- ),
92
- );
93
- const server = await new Promise<Server>((resolve) => {
94
- const s = app.listen(0, () => resolve(s));
95
- });
96
- const addr = server.address() as AddressInfo;
97
- return {
98
- server,
99
- baseUrl: `http://127.0.0.1:${addr.port}`,
100
- writes,
101
- lockedPaths,
102
- writeFileMock,
103
- creatorAccess,
104
- };
105
- }
106
-
107
- function close(s: Server): Promise<void> {
108
- return new Promise((resolve, reject) => s.close((e) => (e ? reject(e) : resolve())));
109
- }
110
-
111
- describe('creator read-grant hooks on the creation routes', () => {
112
- let h: Harness | null = null;
113
- afterEach(async () => {
114
- if (h) await close(h.server);
115
- h = null;
116
- });
117
-
118
- it('PUT /file with a frontmatter plan writes the transformed content in ONE write', async () => {
119
- h = await makeHarness();
120
- h.creatorAccess.planForCreate.mockResolvedValue({
121
- kind: 'frontmatter',
122
- apply: (c: string) => `---\nread: Alice <alice@example.com>\n---\n${c}`,
123
- });
124
- const res = await fetch(
125
- `${h.baseUrl}/api/workspace/${WS}/file?path=${encodeURIComponent(`${KB}/KnowledgeBase/new.md`)}`,
126
- { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content: '# New\n' }) },
127
- );
128
- expect(res.status).toBe(200);
129
- expect(h.creatorAccess.planForCreate).toHaveBeenCalledWith(
130
- WS, USER, `${KB}/KnowledgeBase/new.md`, 'file',
131
- );
132
- expect(h.writes).toEqual([
133
- { path: `${KB}/KnowledgeBase/new.md`, content: '---\nread: Alice <alice@example.com>\n---\n# New\n' },
134
- ]);
135
- });
136
-
137
- it('PUT /file with a seed plan writes the access.md under its own lock BEFORE the file', async () => {
138
- h = await makeHarness();
139
- const seedPath = `${KB}/KnowledgeBase/Mine/access.md`;
140
- h.creatorAccess.planForCreate.mockResolvedValue({
141
- kind: 'seed-access-md',
142
- wsRelPath: seedPath,
143
- apply: (current: string) => current + '---\nread:\n - Alice <alice@example.com>\n---\n',
144
- });
145
- const filePath = `${KB}/KnowledgeBase/Mine/doc.md`;
146
- const res = await fetch(
147
- `${h.baseUrl}/api/workspace/${WS}/file?path=${encodeURIComponent(filePath)}`,
148
- { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content: 'body' }) },
149
- );
150
- expect(res.status).toBe(200);
151
- expect(h.writes.map((w) => w.path)).toEqual([seedPath, filePath]);
152
- // The file content is untouched — the grant lives in the seeded access.md.
153
- expect(h.writes[1].content).toBe('body');
154
- expect(h.lockedPaths).toEqual([seedPath, filePath]);
155
- expect(h.creatorAccess.noteAccessFileWritten).toHaveBeenCalledWith(WS);
156
- });
157
-
158
- it('a failing seed never fails the creation itself', async () => {
159
- h = await makeHarness();
160
- h.creatorAccess.planForCreate.mockResolvedValue({
161
- kind: 'seed-access-md',
162
- wsRelPath: `${KB}/KnowledgeBase/Mine/access.md`,
163
- apply: () => 'seed',
164
- });
165
- // Only the seed write blows up; the file write succeeds.
166
- h.writeFileMock.mockImplementation(async (_id: string, p: string, content: string) => {
167
- if (p.endsWith('/access.md')) throw new Error('boom');
168
- h!.writes.push({ path: p, content });
169
- });
170
- const res = await fetch(
171
- `${h.baseUrl}/api/workspace/${WS}/file?path=${encodeURIComponent(`${KB}/KnowledgeBase/Mine/doc.md`)}`,
172
- { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content: 'body' }) },
173
- );
174
- expect(res.status).toBe(200);
175
- expect(h.writes).toEqual([{ path: `${KB}/KnowledgeBase/Mine/doc.md`, content: 'body' }]);
176
- });
177
-
178
- it('POST /directory seeds the new folder access.md before the .gitkeep cycle', async () => {
179
- h = await makeHarness();
180
- const seedPath = `${KB}/KnowledgeBase/Projects/access.md`;
181
- h.creatorAccess.planForCreate.mockResolvedValue({
182
- kind: 'seed-access-md',
183
- wsRelPath: seedPath,
184
- apply: () => '---\nread:\n - Alice <alice@example.com>\n---\n',
185
- });
186
- const res = await fetch(`${h.baseUrl}/api/workspace/${WS}/directory`, {
187
- method: 'POST',
188
- headers: { 'content-type': 'application/json' },
189
- body: JSON.stringify({ path: `${KB}/KnowledgeBase/Projects` }),
190
- });
191
- expect(res.status).toBe(200);
192
- expect(h.creatorAccess.planForCreate).toHaveBeenCalledWith(
193
- WS, USER, `${KB}/KnowledgeBase/Projects`, 'dir',
194
- );
195
- // Seed written (and locked) before the .gitkeep lock cycle.
196
- expect(h.writes.map((w) => w.path)).toEqual([seedPath]);
197
- expect(h.lockedPaths[0]).toBe(seedPath);
198
- expect(h.lockedPaths[1]).toBe(`${KB}/KnowledgeBase/Projects/.gitkeep`);
199
- });
200
-
201
- it('POST /unzip splices the creator grant into extracted files that need one', async () => {
202
- const extracted = [`${KB}/KnowledgeBase/a.md`, `${KB}/KnowledgeBase/b.md`];
203
- h = await makeHarness({ extracted });
204
- h.creatorAccess.grantInExtractedFile.mockImplementation(
205
- async (_w: string, _u: unknown, p: string) =>
206
- p.endsWith('a.md') ? 'GRANTED CONTENT' : null,
207
- );
208
- const res = await fetch(`${h.baseUrl}/api/workspace/${WS}/unzip`, {
209
- method: 'POST',
210
- headers: { 'content-type': 'application/json' },
211
- body: JSON.stringify({ path: `${KB}/KnowledgeBase/drop.zip` }),
212
- });
213
- expect(res.status).toBe(200);
214
- // a.md got rewritten with the granted content; b.md untouched.
215
- expect(h.writes).toEqual([{ path: `${KB}/KnowledgeBase/a.md`, content: 'GRANTED CONTENT' }]);
216
- });
217
-
218
- it('POST /upload transforms an uploaded .md through the frontmatter plan', async () => {
219
- h = await makeHarness();
220
- h.creatorAccess.planForCreate.mockResolvedValue({
221
- kind: 'frontmatter',
222
- apply: (c: string) => `---\nread: Alice <alice@example.com>\n---\n${c}`,
223
- });
224
- const res = await fetch(
225
- `${h.baseUrl}/api/workspace/${WS}/upload?path=${encodeURIComponent(`${KB}/KnowledgeBase/up.md`)}`,
226
- { method: 'POST', headers: { 'content-type': 'application/octet-stream' }, body: '# Uploaded\n' },
227
- );
228
- expect(res.status).toBe(200);
229
- expect(h.writes).toEqual([
230
- { path: `${KB}/KnowledgeBase/up.md`, content: '---\nread: Alice <alice@example.com>\n---\n# Uploaded\n' },
231
- ]);
232
- });
233
- });
1
+ import type { Server } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import express from 'express';
4
+ import { describe, it, expect, afterEach, vi } from 'vitest';
5
+ import type { IWorkflowService } from '@bevel-software/platform-shared';
6
+ import type { IAccessControl } from '../../access/access-control.interface.js';
7
+ import type { ICreatorAccess } from '../../access/creator-access.js';
8
+ import type { WorkflowEventBus } from '../../workflow/event-bus.js';
9
+ import type { AuthService } from '../../auth/auth.service.js';
10
+ import type { IAdminAccessService } from '../../admin/admin.interface.js';
11
+ import { createWorkspaceRoutes } from '../workspace.routes.js';
12
+ import type { WorkspaceService } from '../workspace.service.js';
13
+
14
+ /**
15
+ * Contract test for the creator read-grant hooks on the creation routes:
16
+ * PUT /file (frontmatter transform + subtree seed), POST /directory (subtree
17
+ * seed before the .gitkeep cycle), POST /unzip (in-place grant per extracted
18
+ * file). The planner itself is unit-tested in creator-access.test.ts — here
19
+ * it's mocked to return canned plans and the assertion is on what the route
20
+ * writes, locks, and invalidates.
21
+ */
22
+
23
+ const USER_ID = 'user-1';
24
+ const USER = { id: USER_ID, email: 'alice@example.com', name: 'Alice' };
25
+ const WS = 'feat-branch';
26
+ const KB = 'knowledge-base';
27
+
28
+ interface Harness {
29
+ server: Server;
30
+ baseUrl: string;
31
+ writes: Array<{ path: string; content: string }>;
32
+ lockedPaths: string[];
33
+ writeFileMock: ReturnType<typeof vi.fn>;
34
+ creatorAccess: {
35
+ planForCreate: ReturnType<typeof vi.fn>;
36
+ grantInExtractedFile: ReturnType<typeof vi.fn>;
37
+ noteAccessFileWritten: ReturnType<typeof vi.fn>;
38
+ };
39
+ }
40
+
41
+ async function makeHarness(opts: { extracted?: string[] } = {}): Promise<Harness> {
42
+ const writes: Array<{ path: string; content: string }> = [];
43
+ const lockedPaths: string[] = [];
44
+
45
+ const writeFileMock = vi.fn(async (_id: string, p: string, content: string) => {
46
+ writes.push({ path: p, content });
47
+ });
48
+ const workspaceService = {
49
+ writeFile: writeFileMock,
50
+ writeFileBinary: vi.fn(async (_id: string, p: string, data: Uint8Array) => {
51
+ writes.push({ path: p, content: Buffer.from(data).toString('utf8') });
52
+ }),
53
+ createDirectory: vi.fn(async () => undefined),
54
+ unzipFile: vi.fn(async () => ({ extracted: opts.extracted ?? [] })),
55
+ } as unknown as WorkspaceService;
56
+
57
+ const workflowService = {
58
+ getLock: vi.fn(async () => null),
59
+ acquireLock: vi.fn(async (_w: string, _b: string, p: string) => {
60
+ lockedPaths.push(p);
61
+ return { acquired: true, lock: { holderUserId: USER_ID, holderName: 'Alice' } };
62
+ }),
63
+ releaseLock: vi.fn(async () => null),
64
+ releaseLockNoCommit: vi.fn(async () => undefined),
65
+ } as unknown as IWorkflowService;
66
+
67
+ const creatorAccess = {
68
+ planForCreate: vi.fn(async () => null),
69
+ grantInExtractedFile: vi.fn(async () => null),
70
+ noteAccessFileWritten: vi.fn(),
71
+ };
72
+
73
+ const authService = { getUserById: vi.fn(async () => USER) } as unknown as AuthService;
74
+ const eventBus = { emit: vi.fn() } as unknown as WorkflowEventBus;
75
+
76
+ const app = express();
77
+ app.use(express.json());
78
+ app.use('/api', (req, _res, next) => {
79
+ (req as unknown as { userId: string }).userId = USER_ID;
80
+ next();
81
+ });
82
+ app.use(
83
+ '/api',
84
+ createWorkspaceRoutes(
85
+ workspaceService,
86
+ authService,
87
+ workflowService,
88
+ eventBus,
89
+ {} as unknown as IAccessControl,
90
+ KB,
91
+ creatorAccess as unknown as ICreatorAccess,
92
+ // Not exercised here — only `.bevelignore`'s tree visibility consults it.
93
+ { isAdmin: async () => false } as unknown as IAdminAccessService,
94
+ ),
95
+ );
96
+ const server = await new Promise<Server>((resolve) => {
97
+ const s = app.listen(0, () => resolve(s));
98
+ });
99
+ const addr = server.address() as AddressInfo;
100
+ return {
101
+ server,
102
+ baseUrl: `http://127.0.0.1:${addr.port}`,
103
+ writes,
104
+ lockedPaths,
105
+ writeFileMock,
106
+ creatorAccess,
107
+ };
108
+ }
109
+
110
+ function close(s: Server): Promise<void> {
111
+ return new Promise((resolve, reject) => s.close((e) => (e ? reject(e) : resolve())));
112
+ }
113
+
114
+ describe('creator read-grant hooks on the creation routes', () => {
115
+ let h: Harness | null = null;
116
+ afterEach(async () => {
117
+ if (h) await close(h.server);
118
+ h = null;
119
+ });
120
+
121
+ it('PUT /file with a frontmatter plan writes the transformed content in ONE write', async () => {
122
+ h = await makeHarness();
123
+ h.creatorAccess.planForCreate.mockResolvedValue({
124
+ kind: 'frontmatter',
125
+ apply: (c: string) => `---\nread: Alice <alice@example.com>\n---\n${c}`,
126
+ });
127
+ const res = await fetch(
128
+ `${h.baseUrl}/api/workspace/${WS}/file?path=${encodeURIComponent(`${KB}/KnowledgeBase/new.md`)}`,
129
+ { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content: '# New\n' }) },
130
+ );
131
+ expect(res.status).toBe(200);
132
+ expect(h.creatorAccess.planForCreate).toHaveBeenCalledWith(
133
+ WS, USER, `${KB}/KnowledgeBase/new.md`, 'file',
134
+ );
135
+ expect(h.writes).toEqual([
136
+ { path: `${KB}/KnowledgeBase/new.md`, content: '---\nread: Alice <alice@example.com>\n---\n# New\n' },
137
+ ]);
138
+ });
139
+
140
+ it('PUT /file with a seed plan writes the access.md under its own lock BEFORE the file', async () => {
141
+ h = await makeHarness();
142
+ const seedPath = `${KB}/KnowledgeBase/Mine/access.md`;
143
+ h.creatorAccess.planForCreate.mockResolvedValue({
144
+ kind: 'seed-access-md',
145
+ wsRelPath: seedPath,
146
+ apply: (current: string) => current + '---\nread:\n - Alice <alice@example.com>\n---\n',
147
+ });
148
+ const filePath = `${KB}/KnowledgeBase/Mine/doc.md`;
149
+ const res = await fetch(
150
+ `${h.baseUrl}/api/workspace/${WS}/file?path=${encodeURIComponent(filePath)}`,
151
+ { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content: 'body' }) },
152
+ );
153
+ expect(res.status).toBe(200);
154
+ expect(h.writes.map((w) => w.path)).toEqual([seedPath, filePath]);
155
+ // The file content is untouched — the grant lives in the seeded access.md.
156
+ expect(h.writes[1].content).toBe('body');
157
+ expect(h.lockedPaths).toEqual([seedPath, filePath]);
158
+ expect(h.creatorAccess.noteAccessFileWritten).toHaveBeenCalledWith(WS);
159
+ });
160
+
161
+ it('a failing seed never fails the creation itself', async () => {
162
+ h = await makeHarness();
163
+ h.creatorAccess.planForCreate.mockResolvedValue({
164
+ kind: 'seed-access-md',
165
+ wsRelPath: `${KB}/KnowledgeBase/Mine/access.md`,
166
+ apply: () => 'seed',
167
+ });
168
+ // Only the seed write blows up; the file write succeeds.
169
+ h.writeFileMock.mockImplementation(async (_id: string, p: string, content: string) => {
170
+ if (p.endsWith('/access.md')) throw new Error('boom');
171
+ h!.writes.push({ path: p, content });
172
+ });
173
+ const res = await fetch(
174
+ `${h.baseUrl}/api/workspace/${WS}/file?path=${encodeURIComponent(`${KB}/KnowledgeBase/Mine/doc.md`)}`,
175
+ { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content: 'body' }) },
176
+ );
177
+ expect(res.status).toBe(200);
178
+ expect(h.writes).toEqual([{ path: `${KB}/KnowledgeBase/Mine/doc.md`, content: 'body' }]);
179
+ });
180
+
181
+ it('POST /directory seeds the new folder access.md before the .gitkeep cycle', async () => {
182
+ h = await makeHarness();
183
+ const seedPath = `${KB}/KnowledgeBase/Projects/access.md`;
184
+ h.creatorAccess.planForCreate.mockResolvedValue({
185
+ kind: 'seed-access-md',
186
+ wsRelPath: seedPath,
187
+ apply: () => '---\nread:\n - Alice <alice@example.com>\n---\n',
188
+ });
189
+ const res = await fetch(`${h.baseUrl}/api/workspace/${WS}/directory`, {
190
+ method: 'POST',
191
+ headers: { 'content-type': 'application/json' },
192
+ body: JSON.stringify({ path: `${KB}/KnowledgeBase/Projects` }),
193
+ });
194
+ expect(res.status).toBe(200);
195
+ expect(h.creatorAccess.planForCreate).toHaveBeenCalledWith(
196
+ WS, USER, `${KB}/KnowledgeBase/Projects`, 'dir',
197
+ );
198
+ // Seed written (and locked) before the .gitkeep lock cycle.
199
+ expect(h.writes.map((w) => w.path)).toEqual([seedPath]);
200
+ expect(h.lockedPaths[0]).toBe(seedPath);
201
+ expect(h.lockedPaths[1]).toBe(`${KB}/KnowledgeBase/Projects/.gitkeep`);
202
+ });
203
+
204
+ it('POST /unzip splices the creator grant into extracted files that need one', async () => {
205
+ const extracted = [`${KB}/KnowledgeBase/a.md`, `${KB}/KnowledgeBase/b.md`];
206
+ h = await makeHarness({ extracted });
207
+ h.creatorAccess.grantInExtractedFile.mockImplementation(
208
+ async (_w: string, _u: unknown, p: string) =>
209
+ p.endsWith('a.md') ? 'GRANTED CONTENT' : null,
210
+ );
211
+ const res = await fetch(`${h.baseUrl}/api/workspace/${WS}/unzip`, {
212
+ method: 'POST',
213
+ headers: { 'content-type': 'application/json' },
214
+ body: JSON.stringify({ path: `${KB}/KnowledgeBase/drop.zip` }),
215
+ });
216
+ expect(res.status).toBe(200);
217
+ // a.md got rewritten with the granted content; b.md untouched.
218
+ expect(h.writes).toEqual([{ path: `${KB}/KnowledgeBase/a.md`, content: 'GRANTED CONTENT' }]);
219
+ });
220
+
221
+ it('POST /upload transforms an uploaded .md through the frontmatter plan', async () => {
222
+ h = await makeHarness();
223
+ h.creatorAccess.planForCreate.mockResolvedValue({
224
+ kind: 'frontmatter',
225
+ apply: (c: string) => `---\nread: Alice <alice@example.com>\n---\n${c}`,
226
+ });
227
+ const res = await fetch(
228
+ `${h.baseUrl}/api/workspace/${WS}/upload?path=${encodeURIComponent(`${KB}/KnowledgeBase/up.md`)}`,
229
+ { method: 'POST', headers: { 'content-type': 'application/octet-stream' }, body: '# Uploaded\n' },
230
+ );
231
+ expect(res.status).toBe(200);
232
+ expect(h.writes).toEqual([
233
+ { path: `${KB}/KnowledgeBase/up.md`, content: '---\nread: Alice <alice@example.com>\n---\n# Uploaded\n' },
234
+ ]);
235
+ });
236
+ });