@bevel-software/platform-core-backend 0.7.5 → 0.9.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 (282) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +8 -1
  4. package/dist/core/core-ports.d.ts.map +1 -1
  5. package/dist/core/core-ports.js.map +1 -1
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +75 -18
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +12 -5
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +69 -36
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +8 -7
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +1 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +6 -6
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access-declarations.d.ts +5 -5
  20. package/dist/modules/access/access-declarations.js +3 -3
  21. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  22. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-mutation.service.js +3 -3
  24. package/dist/modules/access/access-mutation.service.js.map +1 -1
  25. package/dist/modules/access/access-splice.js +4 -4
  26. package/dist/modules/access/access-splice.js.map +1 -1
  27. package/dist/modules/access/access.routes.d.ts +3 -1
  28. package/dist/modules/access/access.routes.d.ts.map +1 -1
  29. package/dist/modules/access/access.routes.js +22 -20
  30. package/dist/modules/access/access.routes.js.map +1 -1
  31. package/dist/modules/access/creator-access.d.ts +2 -2
  32. package/dist/modules/access/creator-access.js +5 -5
  33. package/dist/modules/access/creator-access.js.map +1 -1
  34. package/dist/modules/access/render-roles-yaml.d.ts +22 -0
  35. package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
  36. package/dist/modules/access/render-roles-yaml.js +56 -0
  37. package/dist/modules/access/render-roles-yaml.js.map +1 -0
  38. package/dist/modules/access/roles-admin.service.d.ts +33 -5
  39. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  40. package/dist/modules/access/roles-admin.service.js +44 -19
  41. package/dist/modules/access/roles-admin.service.js.map +1 -1
  42. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  43. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  44. package/dist/modules/code-mode/code-mode-names.js +5 -27
  45. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  46. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  47. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  48. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  49. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  50. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  51. package/dist/modules/mcp/mcp.service.js +33 -395
  52. package/dist/modules/mcp/mcp.service.js.map +1 -1
  53. package/dist/modules/plugins/index.d.ts +7 -0
  54. package/dist/modules/plugins/index.d.ts.map +1 -0
  55. package/dist/modules/plugins/index.js +6 -0
  56. package/dist/modules/plugins/index.js.map +1 -0
  57. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  58. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  59. package/dist/modules/plugins/join-proposals.js +67 -0
  60. package/dist/modules/plugins/join-proposals.js.map +1 -0
  61. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  62. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  63. package/dist/modules/plugins/join-requests.service.js +135 -0
  64. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  65. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  66. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  68. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  69. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  70. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.contract.js +36 -0
  72. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  73. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  74. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  75. package/dist/modules/plugins/plugins.routes.js +379 -0
  76. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  77. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  78. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  79. package/dist/modules/plugins/plugins.service.js +172 -0
  80. package/dist/modules/plugins/plugins.service.js.map +1 -0
  81. package/dist/modules/settings/setup.routes.d.ts +10 -1
  82. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  83. package/dist/modules/settings/setup.routes.js +111 -6
  84. package/dist/modules/settings/setup.routes.js.map +1 -1
  85. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  86. package/dist/modules/skills/pending-skills.service.js +7 -7
  87. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  88. package/dist/modules/skills/skills.contract.d.ts +4 -4
  89. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  90. package/dist/modules/skills/skills.contract.js +1 -1
  91. package/dist/modules/skills/skills.service.js +5 -5
  92. package/dist/modules/skills/skills.service.js.map +1 -1
  93. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  94. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  95. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  96. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  97. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  98. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  99. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  100. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  101. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  102. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  103. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  104. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  105. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  106. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  107. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  108. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  109. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  110. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  111. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  112. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  113. package/dist/modules/workflow/git/git.service.js +2 -2
  114. package/dist/modules/workflow/git/git.service.js.map +1 -1
  115. package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
  116. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  117. package/dist/modules/workspace/kb-seed.service.js +43 -10
  118. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  119. package/dist/modules/workspace/plugins-migration.d.ts +50 -0
  120. package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
  121. package/dist/modules/workspace/plugins-migration.js +379 -0
  122. package/dist/modules/workspace/plugins-migration.js.map +1 -0
  123. package/dist/modules/workspace/startup/kb-git.d.ts +23 -0
  124. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
  125. package/dist/modules/workspace/startup/kb-git.js +86 -0
  126. package/dist/modules/workspace/startup/kb-git.js.map +1 -0
  127. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
  128. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
  129. package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
  130. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
  131. package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
  132. package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
  133. package/dist/modules/workspace/startup/on-server-start.js +21 -0
  134. package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
  135. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
  136. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
  137. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
  138. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
  139. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
  140. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
  141. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
  142. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
  143. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
  144. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
  145. package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
  146. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
  147. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
  148. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
  149. package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
  150. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -0
  151. package/dist/modules/workspace/workspace.routes.js +3 -3
  152. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  153. package/dist/modules/workspace/workspace.service.d.ts +0 -9
  154. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  155. package/dist/modules/workspace/workspace.service.js +2 -30
  156. package/dist/modules/workspace/workspace.service.js.map +1 -1
  157. package/dist/shared/kb-layout.test.js +3 -3
  158. package/dist/shared/kb-layout.test.js.map +1 -1
  159. package/dist/shared/utcp-namespace.d.ts +6 -27
  160. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  161. package/dist/shared/utcp-namespace.js +6 -63
  162. package/dist/shared/utcp-namespace.js.map +1 -1
  163. package/dist/shared/variable-refs.d.ts +42 -0
  164. package/dist/shared/variable-refs.d.ts.map +1 -0
  165. package/dist/shared/variable-refs.js +60 -0
  166. package/dist/shared/variable-refs.js.map +1 -0
  167. package/kb-template/.bevelignore +1 -1
  168. package/kb-template/AGENTS.md +88 -35
  169. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  170. package/kb-template/access.md +36 -36
  171. package/kb-template/gitignore.template +16 -0
  172. package/migrations/meta/0000_snapshot.json +1479 -1479
  173. package/package.json +5 -4
  174. package/src/assets.ts +25 -25
  175. package/src/core/core-ports.ts +113 -106
  176. package/src/core/create-core-server.ts +85 -20
  177. package/src/core/create-core-services.ts +83 -42
  178. package/src/index.ts +69 -69
  179. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  180. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  181. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  182. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  183. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  184. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  185. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  186. package/src/modules/access/__tests__/roles-admin.service.test.ts +36 -2
  187. package/src/modules/access/access-control.interface.ts +8 -7
  188. package/src/modules/access/access-control.service.ts +7 -7
  189. package/src/modules/access/access-declarations.ts +5 -5
  190. package/src/modules/access/access-mutation.service.ts +3 -3
  191. package/src/modules/access/access-splice.ts +4 -4
  192. package/src/modules/access/access.routes.ts +23 -20
  193. package/src/modules/access/creator-access.ts +5 -5
  194. package/src/modules/access/render-roles-yaml.ts +65 -0
  195. package/src/modules/access/roles-admin.service.ts +45 -16
  196. package/src/modules/admin/admin-access.routes.ts +29 -29
  197. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  198. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  199. package/src/modules/auth/rate-limit.ts +45 -45
  200. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  201. package/src/modules/code-mode/code-mode-names.ts +10 -36
  202. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  203. package/src/modules/database/connection.ts +15 -15
  204. package/src/modules/database/schema.ts +11 -11
  205. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  206. package/src/modules/mcp/mcp.service.ts +57 -435
  207. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  208. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  209. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  210. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  211. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  212. package/src/modules/plugins/index.ts +17 -0
  213. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  214. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  215. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  216. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  217. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  218. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  219. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  220. package/src/modules/settings/__tests__/setup.routes.test.ts +124 -3
  221. package/src/modules/settings/setup.routes.ts +112 -5
  222. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  223. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  224. package/src/modules/skills/pending-skills.service.ts +7 -7
  225. package/src/modules/skills/skills.contract.ts +4 -4
  226. package/src/modules/skills/skills.service.ts +5 -5
  227. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  228. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  229. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  230. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  231. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  232. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  233. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  234. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  235. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  236. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  237. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  238. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  239. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  240. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  241. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  242. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  243. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  244. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  245. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  246. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  247. package/src/modules/workflow/git/git.service.ts +2 -2
  248. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  249. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  250. package/src/modules/workflow/workflow-hooks.ts +101 -101
  251. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  252. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  253. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  254. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  255. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  256. package/src/modules/workspace/__tests__/workspace.service.test.ts +27 -0
  257. package/src/modules/workspace/bevel-ignore.ts +66 -66
  258. package/src/modules/workspace/session-sink.ts +25 -25
  259. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
  260. package/src/modules/workspace/startup/kb-git.ts +94 -0
  261. package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
  262. package/src/modules/workspace/startup/on-server-start.ts +97 -0
  263. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
  264. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
  265. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
  266. package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
  267. package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
  268. package/src/modules/workspace/workspace.routes.ts +3 -3
  269. package/src/modules/workspace/workspace.service.ts +2 -35
  270. package/src/modules/workspace/workspace.tools.ts +922 -922
  271. package/src/shared/__tests__/join-request.test.ts +13 -13
  272. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  273. package/src/shared/kb-layout.test.ts +3 -3
  274. package/src/shared/utcp-namespace.ts +10 -68
  275. package/src/shared/variable-refs.ts +64 -0
  276. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  277. package/src/modules/groups/index.ts +0 -17
  278. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
  279. package/src/modules/workspace/kb-seed.interface.ts +0 -36
  280. package/src/modules/workspace/kb-seed.service.ts +0 -555
  281. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  282. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -1,236 +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 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
- });
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
+ });