@bevel-software/platform-core-backend 0.7.4 → 0.8.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 (246) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +51 -7
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +5 -3
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +38 -21
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/modules/access/access-control.interface.d.ts +8 -7
  12. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  13. package/dist/modules/access/access-control.service.d.ts +1 -1
  14. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.js +6 -6
  16. package/dist/modules/access/access-control.service.js.map +1 -1
  17. package/dist/modules/access/access-declarations.d.ts +5 -5
  18. package/dist/modules/access/access-declarations.js +3 -3
  19. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  20. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  21. package/dist/modules/access/access-mutation.service.js +3 -3
  22. package/dist/modules/access/access-mutation.service.js.map +1 -1
  23. package/dist/modules/access/access-splice.js +4 -4
  24. package/dist/modules/access/access-splice.js.map +1 -1
  25. package/dist/modules/access/access.routes.js +19 -19
  26. package/dist/modules/access/access.routes.js.map +1 -1
  27. package/dist/modules/access/creator-access.d.ts +2 -2
  28. package/dist/modules/access/creator-access.js +5 -5
  29. package/dist/modules/access/creator-access.js.map +1 -1
  30. package/dist/modules/access/roles-admin.service.d.ts +18 -4
  31. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  32. package/dist/modules/access/roles-admin.service.js +14 -4
  33. package/dist/modules/access/roles-admin.service.js.map +1 -1
  34. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  35. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  36. package/dist/modules/code-mode/code-mode-names.js +5 -27
  37. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  38. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  39. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  40. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  41. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  42. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  43. package/dist/modules/mcp/mcp.service.js +33 -395
  44. package/dist/modules/mcp/mcp.service.js.map +1 -1
  45. package/dist/modules/plugins/index.d.ts +7 -0
  46. package/dist/modules/plugins/index.d.ts.map +1 -0
  47. package/dist/modules/plugins/index.js +6 -0
  48. package/dist/modules/plugins/index.js.map +1 -0
  49. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  50. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  51. package/dist/modules/plugins/join-proposals.js +67 -0
  52. package/dist/modules/plugins/join-proposals.js.map +1 -0
  53. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  54. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  55. package/dist/modules/plugins/join-requests.service.js +135 -0
  56. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  57. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  58. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  59. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  60. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  61. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  62. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  63. package/dist/modules/plugins/plugins.contract.js +36 -0
  64. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  65. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  66. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugins.routes.js +379 -0
  68. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  69. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  70. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.service.js +172 -0
  72. package/dist/modules/plugins/plugins.service.js.map +1 -0
  73. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  74. package/dist/modules/skills/pending-skills.service.js +7 -7
  75. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  76. package/dist/modules/skills/skills.contract.d.ts +4 -4
  77. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  78. package/dist/modules/skills/skills.contract.js +1 -1
  79. package/dist/modules/skills/skills.service.js +5 -5
  80. package/dist/modules/skills/skills.service.js.map +1 -1
  81. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  82. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  83. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  84. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  85. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  86. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  87. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  88. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  89. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  90. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  91. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  93. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  94. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  95. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  96. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  97. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  98. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  99. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  100. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  101. package/dist/modules/workflow/git/git.service.js +2 -2
  102. package/dist/modules/workflow/git/git.service.js.map +1 -1
  103. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  104. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  105. package/dist/modules/workflow/pending-commits.service.js +37 -9
  106. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  107. package/dist/modules/workflow/pending-commits.worker.d.ts +17 -4
  108. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  109. package/dist/modules/workflow/pending-commits.worker.js +63 -9
  110. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  111. package/dist/modules/workflow/workflow.service.d.ts +1 -0
  112. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  113. package/dist/modules/workflow/workflow.service.js +6 -1
  114. package/dist/modules/workflow/workflow.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/workspace.routes.js +3 -3
  124. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  125. package/dist/modules/workspace/workspace.service.d.ts +26 -0
  126. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  127. package/dist/modules/workspace/workspace.service.js +83 -12
  128. package/dist/modules/workspace/workspace.service.js.map +1 -1
  129. package/dist/shared/kb-layout.test.js +3 -3
  130. package/dist/shared/kb-layout.test.js.map +1 -1
  131. package/dist/shared/utcp-namespace.d.ts +6 -27
  132. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  133. package/dist/shared/utcp-namespace.js +6 -63
  134. package/dist/shared/utcp-namespace.js.map +1 -1
  135. package/dist/shared/variable-refs.d.ts +42 -0
  136. package/dist/shared/variable-refs.d.ts.map +1 -0
  137. package/dist/shared/variable-refs.js +60 -0
  138. package/dist/shared/variable-refs.js.map +1 -0
  139. package/kb-template/.bevelignore +1 -1
  140. package/kb-template/AGENTS.md +88 -35
  141. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  142. package/kb-template/access.md +36 -36
  143. package/migrations/meta/0000_snapshot.json +1479 -1479
  144. package/package.json +5 -4
  145. package/src/assets.ts +25 -25
  146. package/src/core/core-ports.ts +106 -106
  147. package/src/core/create-core-server.ts +55 -9
  148. package/src/core/create-core-services.ts +48 -24
  149. package/src/index.ts +69 -69
  150. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  151. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  152. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  153. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  154. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  155. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  156. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  157. package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
  158. package/src/modules/access/access-control.interface.ts +8 -7
  159. package/src/modules/access/access-control.service.ts +7 -7
  160. package/src/modules/access/access-declarations.ts +5 -5
  161. package/src/modules/access/access-mutation.service.ts +3 -3
  162. package/src/modules/access/access-splice.ts +4 -4
  163. package/src/modules/access/access.routes.ts +20 -20
  164. package/src/modules/access/creator-access.ts +5 -5
  165. package/src/modules/access/roles-admin.service.ts +13 -2
  166. package/src/modules/admin/admin-access.routes.ts +29 -29
  167. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  168. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  169. package/src/modules/auth/rate-limit.ts +45 -45
  170. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  171. package/src/modules/code-mode/code-mode-names.ts +10 -36
  172. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  173. package/src/modules/database/connection.ts +15 -15
  174. package/src/modules/database/schema.ts +11 -11
  175. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  176. package/src/modules/mcp/mcp.service.ts +57 -435
  177. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  178. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  179. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  180. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  181. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  182. package/src/modules/plugins/index.ts +17 -0
  183. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  184. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  185. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  186. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  187. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  188. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  189. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  190. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  191. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  192. package/src/modules/skills/pending-skills.service.ts +7 -7
  193. package/src/modules/skills/skills.contract.ts +4 -4
  194. package/src/modules/skills/skills.service.ts +5 -5
  195. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  196. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  197. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  198. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  199. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  200. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  201. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  202. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  203. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  204. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  205. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  206. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  207. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  208. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  209. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +118 -0
  210. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  211. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  212. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  213. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  214. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  215. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  216. package/src/modules/workflow/git/git.service.ts +2 -2
  217. package/src/modules/workflow/pending-commits.service.ts +48 -12
  218. package/src/modules/workflow/pending-commits.worker.ts +64 -8
  219. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  220. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  221. package/src/modules/workflow/workflow-hooks.ts +101 -101
  222. package/src/modules/workflow/workflow.service.ts +13 -2
  223. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
  224. package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
  225. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  226. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  227. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  228. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  229. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  230. package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
  231. package/src/modules/workspace/bevel-ignore.ts +66 -66
  232. package/src/modules/workspace/kb-seed.service.ts +38 -9
  233. package/src/modules/workspace/plugins-migration.ts +479 -0
  234. package/src/modules/workspace/session-sink.ts +25 -25
  235. package/src/modules/workspace/workspace.routes.ts +3 -3
  236. package/src/modules/workspace/workspace.service.ts +85 -14
  237. package/src/modules/workspace/workspace.tools.ts +922 -922
  238. package/src/shared/__tests__/join-request.test.ts +13 -13
  239. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  240. package/src/shared/kb-layout.test.ts +3 -3
  241. package/src/shared/utcp-namespace.ts +10 -68
  242. package/src/shared/variable-refs.ts +64 -0
  243. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  244. package/src/modules/groups/index.ts +0 -17
  245. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  246. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -113,9 +113,9 @@ function close(s: Server): Promise<void> {
113
113
  const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
114
114
 
115
115
  const FILES = {
116
- [`${KB}/Groups/GTM/access.md`]: fm('read:\n - GTM Team'),
117
- [`${KB}/Groups/GTM/battlecards/access.md`]: fm('read:\n - deny everyone'),
118
- [`${KB}/Groups/GTM/slack.tool`]: fm('owner: Ali <ali@bevel.software>'),
116
+ [`${KB}/Plugins/GTM/access.md`]: fm('read:\n - GTM Team'),
117
+ [`${KB}/Plugins/GTM/battlecards/access.md`]: fm('read:\n - deny everyone'),
118
+ [`${KB}/Plugins/GTM/slack.tool`]: fm('owner: Ali <ali@bevel.software>'),
119
119
  };
120
120
 
121
121
  describe('GET /access/overrides', () => {
@@ -133,20 +133,20 @@ describe('GET /access/overrides', () => {
133
133
  it('returns the declarations under a folder the caller can read', async () => {
134
134
  h = await makeHarness({ files: FILES });
135
135
 
136
- const res = await get('Groups/GTM');
136
+ const res = await get('Plugins/GTM');
137
137
  expect(res.status).toBe(200);
138
138
  expect(await res.json()).toEqual({
139
139
  truncated: false,
140
140
  overrides: [
141
141
  {
142
- path: 'Groups/GTM/battlecards/access.md',
143
- governs: 'Groups/GTM/battlecards',
142
+ path: 'Plugins/GTM/battlecards/access.md',
143
+ governs: 'Plugins/GTM/battlecards',
144
144
  source: 'access-md',
145
145
  entries: [{ verb: 'read', deny: true, principal: { kind: 'everyone' } }],
146
146
  },
147
147
  {
148
- path: 'Groups/GTM/slack.tool',
149
- governs: 'Groups/GTM/slack.tool',
148
+ path: 'Plugins/GTM/slack.tool',
149
+ governs: 'Plugins/GTM/slack.tool',
150
150
  source: 'frontmatter',
151
151
  entries: [
152
152
  {
@@ -163,7 +163,7 @@ describe('GET /access/overrides', () => {
163
163
  it('403s a caller who cannot read the folder', async () => {
164
164
  h = await makeHarness({ files: FILES, canRead: () => false });
165
165
 
166
- const res = await get('Groups/GTM');
166
+ const res = await get('Plugins/GTM');
167
167
  expect(res.status).toBe(403);
168
168
  expect(await res.json()).toEqual({ error: 'You do not have access to this folder.' });
169
169
  });
@@ -171,21 +171,21 @@ describe('GET /access/overrides', () => {
171
171
  it('drops rows governing something the caller cannot read', async () => {
172
172
  h = await makeHarness({
173
173
  files: FILES,
174
- rowReadable: (p) => p !== 'Groups/GTM/battlecards',
174
+ rowReadable: (p) => p !== 'Plugins/GTM/battlecards',
175
175
  });
176
176
 
177
- const res = await get('Groups/GTM');
177
+ const res = await get('Plugins/GTM');
178
178
  const body = (await res.json()) as { overrides: { path: string }[] };
179
- expect(body.overrides.map((o) => o.path)).toEqual(['Groups/GTM/slack.tool']);
179
+ expect(body.overrides.map((o) => o.path)).toEqual(['Plugins/GTM/slack.tool']);
180
180
  });
181
181
 
182
182
  it('accepts a workspace-relative path exactly like a repo-relative one', async () => {
183
183
  h = await makeHarness({ files: FILES });
184
184
 
185
- const repoRel = (await (await get('Groups/GTM')).json()) as unknown;
186
- const wsRel = (await (await get(`${KB}/Groups/GTM`)).json()) as unknown;
185
+ const repoRel = (await (await get('Plugins/GTM')).json()) as unknown;
186
+ const wsRel = (await (await get(`${KB}/Plugins/GTM`)).json()) as unknown;
187
187
  expect(wsRel).toEqual(repoRel);
188
- expect(h.canRead).toHaveBeenCalledWith(WS, USER.email, 'Groups/GTM');
188
+ expect(h.canRead).toHaveBeenCalledWith(WS, USER.email, 'Plugins/GTM');
189
189
  });
190
190
 
191
191
  it('400s a path that escapes the KB repo', async () => {
@@ -198,7 +198,7 @@ describe('GET /access/overrides', () => {
198
198
  it('400s a target that is a file, not a folder', async () => {
199
199
  h = await makeHarness({ files: FILES });
200
200
 
201
- const res = await get('Groups/GTM/slack.tool');
201
+ const res = await get('Plugins/GTM/slack.tool');
202
202
  expect(res.status).toBe(400);
203
203
  expect(await res.json()).toEqual({ error: 'path must be a folder' });
204
204
  });
@@ -109,19 +109,19 @@ describe('AccessControlService.grantSources', () => {
109
109
  expect(src.write).toEqual([{ kind: 'ancestor', path: 'Knowledge/access.md' }]);
110
110
  });
111
111
 
112
- it('a USER who holds a verb only via a group they belong to has NO source (the group row carries it)', async () => {
112
+ it('a USER who holds a verb only via a plugin they belong to has NO source (the plugin row carries it)', async () => {
113
113
  await writeFile(repo, 'access.md', '---\nwrite:\n - Engineer\n---\n');
114
- // Ali is an Engineer (per roles.yaml) — his write flows through the GROUP, not
114
+ // Ali is an Engineer (per roles.yaml) — his write flows through the PLUGIN, not
115
115
  // a per-user file entry. He isn't named in any file, so there's nothing to
116
- // remove for HIM here; the Engineering group is the removable principal.
116
+ // remove for HIM here; the Engineering plugin is the removable principal.
117
117
  const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', ALI);
118
118
  expect(src.write).toBeUndefined();
119
119
  expect(src).toEqual({}); // no verb resolves to a per-user file entry
120
120
  });
121
121
 
122
- it('the GROUP principal itself resolves to its own file scope (direct/ancestor)', async () => {
122
+ it('the PLUGIN principal itself resolves to its own file scope (direct/ancestor)', async () => {
123
123
  // The same Engineer grant, asked about as the ROLE principal, IS a removable
124
- // file entry — the group is named directly on Knowledge/access.md.
124
+ // file entry — the plugin is named directly on Knowledge/access.md.
125
125
  await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
126
126
  await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - Engineer\n---\n');
127
127
  const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
@@ -150,20 +150,20 @@ describe('AccessControlService.grantSources', () => {
150
150
  expect(src).toEqual({});
151
151
  });
152
152
 
153
- it("a folder-target admin whose access is via the Admin GROUP has NO per-user source", async () => {
154
- // Razvan's effective write comes from the `Admin` group grant, not a per-user
153
+ it("a folder-target admin whose access is via the Admin PLUGIN has NO per-user source", async () => {
154
+ // Razvan's effective write comes from the `Admin` plugin grant, not a per-user
155
155
  // entry — so as a USER principal he has no removable source here (the Admin
156
- // group row carries it, exactly like any other group).
156
+ // plugin row carries it, exactly like any other plugin).
157
157
  await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
158
158
  const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', RAZVAN);
159
159
  expect(src.write).toBeUndefined();
160
160
  expect(src).toEqual({});
161
161
  });
162
162
 
163
- it("a user named DIRECTLY in a file keeps a source even if they're also in a group", async () => {
164
- // Razvan is in the Admin group AND named inline on Knowledge — the inline
163
+ it("a user named DIRECTLY in a file keeps a source even if they're also in a plugin", async () => {
164
+ // Razvan is in the Admin plugin AND named inline on Knowledge — the inline
165
165
  // entry is a removable file grant, so his source is direct (tier-1 email
166
- // beats the group at the same scope).
166
+ // beats the plugin at the same scope).
167
167
  await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
168
168
  await writeFile(
169
169
  repo,
@@ -177,7 +177,7 @@ describe('AccessControlService.grantSources', () => {
177
177
  it('a role principal is matched by its own scope entry, not a role indirection', async () => {
178
178
  await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
179
179
  await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - Engineer\n---\n');
180
- // The Engineer GROUP itself is granted on Knowledge — for the role principal that's a direct grant.
180
+ // The Engineer PLUGIN itself is granted on Knowledge — for the role principal that's a direct grant.
181
181
  const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
182
182
  kind: 'role',
183
183
  role: 'Engineer',
@@ -160,7 +160,7 @@ describe('RolesAdminService', () => {
160
160
  access = new AccessControlService(ws, KB);
161
161
  workflow = stubWorkflow();
162
162
  bus = stubEventBus();
163
- svc = new RolesAdminService(ws, workflow.svc, access, KB, DEFAULT_BRANCH, bus.bus);
163
+ svc = new RolesAdminService(ws, workflow.svc, access, KB, () => DEFAULT_BRANCH, bus.bus);
164
164
  });
165
165
 
166
166
  afterEach(async () => {
@@ -174,6 +174,18 @@ describe('RolesAdminService', () => {
174
174
  expect(roster.find((r) => r.canonical === 'sales')!.members).toEqual(['felix@example.com']);
175
175
  });
176
176
 
177
+ it('resolves the default branch per call, not at construction', async () => {
178
+ // The deployment shape this guards: a service built during boot, before
179
+ // `configureBranchModel()` has applied the branch model, saw an empty
180
+ // `DEFAULT_BRANCH` and froze it — every roles route then failed branch
181
+ // validation with `Invalid branch name "": empty name` until a restart.
182
+ let branch = '';
183
+ const late = new RolesAdminService(ws, workflow.svc, access, KB, () => branch, bus.bus);
184
+ branch = DEFAULT_BRANCH;
185
+ const roster = await late.getRoster();
186
+ expect(roster.map((r) => r.canonical)).toEqual(['admin', 'sales']);
187
+ });
188
+
177
189
  it('getHealth reports ok for a valid file and corrupted for a broken one', async () => {
178
190
  expect(await svc.getHealth()).toEqual({ ok: true, errors: [] });
179
191
 
@@ -20,8 +20,8 @@
20
20
  *
21
21
  * Only a principal NAMED in a file (a direct user grant `Name <email>`, or a
22
22
  * group/role token) produces sources. A USER who merely RESOLVES to access via a
23
- * group they belong to, the built-in `everyone`, or admin-rescue is NOT a
24
- * per-target file entry and produces NO source (the group itself shows as its own
23
+ * plugin they belong to, the built-in `everyone`, or admin-rescue is NOT a
24
+ * per-target file entry and produces NO source (the plugin itself shows as its own
25
25
  * row instead).
26
26
  */
27
27
  export type GrantSource =
@@ -138,8 +138,8 @@ export interface IAccessControl {
138
138
 
139
139
  /**
140
140
  * Batched `canOwner` — one config load per call, same `owner:`-lists-only
141
- * resolution (no admin rescue). Used by group enumeration to answer "which
142
- * of these folders does the caller own?" without one model load per group.
141
+ * resolution (no admin rescue). Used by plugin enumeration to answer "which
142
+ * of these folders does the caller own?" without one model load per plugin.
143
143
  */
144
144
  canOwnerBatch(
145
145
  workspaceId: string,
@@ -281,8 +281,9 @@ export interface IAccessControl {
281
281
 
282
282
  /**
283
283
  * Enumerate the grantable principals known to the KB, for the share-dialog
284
- * autocomplete. `groups` are the declared `roles.yaml` role display names
285
- * (the built-in `everyone` is excluded — it isn't grantable from the UI).
284
+ * autocomplete. `plugins` are the built-in `everyone` role plus the declared
285
+ * `roles.yaml` role display names (`everyone` is surfaced so the UI can
286
+ * grant public read; the grant route gates it to the `read` verb only).
286
287
  * `people` are every email named in `roles.yaml` (name defaults to the local
287
288
  * part) unioned with every `Name <email>` grant in any `access.md` (named).
288
289
  * The login-only `users` table is unioned in by the caller — this method
@@ -290,7 +291,7 @@ export interface IAccessControl {
290
291
  */
291
292
  kbPrincipals(
292
293
  workspaceId: string,
293
- ): Promise<{ groups: string[]; people: { name: string; email: string }[] }>;
294
+ ): Promise<{ plugins: string[]; people: { name: string; email: string }[] }>;
294
295
 
295
296
  /**
296
297
  * Reverse-lookup an email by its SHA-256 hash (per `hashEmail` semantics)
@@ -925,7 +925,7 @@ function scopeToGrantSource(
925
925
  * returning the WHOLE list of removable entries rather than just the winner.
926
926
  *
927
927
  * Only the principal's OWN named entry yields a source — a `user` by their email,
928
- * a `role` by its role token. A grant that reaches the user via a group they
928
+ * a `role` by its role token. A grant that reaches the user via a plugin they
929
929
  * belong to, the built-in `everyone`, or admin-rescue is NOT their entry, so it
930
930
  * never adds a source (the group/role shows as its own row instead). The list is:
931
931
  * - `[]` (verb omitted by the caller) when the principal effectively holds no
@@ -941,7 +941,7 @@ function scopeToGrantSource(
941
941
  * under closest-wins), and the revoke flow needs the inherited remainder that
942
942
  * survives removing the direct entry. A group/everyone grant at some scope does
943
943
  * not add a source AND does not hide a farther own-entry the principal is named
944
- * in (removing it is still meaningful if the group grant is later removed).
944
+ * in (removing it is still meaningful if the plugin grant is later removed).
945
945
  */
946
946
  function resolveGrantSourcesForVerb(
947
947
  model: AccessModel,
@@ -1453,18 +1453,18 @@ export class AccessControlService implements IAccessControl {
1453
1453
 
1454
1454
  async kbPrincipals(
1455
1455
  workspaceId: string,
1456
- ): Promise<{ groups: string[]; people: { name: string; email: string }[] }> {
1456
+ ): Promise<{ plugins: string[]; people: { name: string; email: string }[] }> {
1457
1457
  let model: AccessModel;
1458
1458
  try {
1459
1459
  model = await this.loadModel(workspaceId);
1460
1460
  } catch {
1461
- return { groups: [], people: [] };
1461
+ return { plugins: [], people: [] };
1462
1462
  }
1463
- // Groups = the built-in `everyone` role plus every declared role's display
1463
+ // Plugins = the built-in `everyone` role plus every declared role's display
1464
1464
  // name. `everyone` is surfaced so the share UI can grant public read; the
1465
1465
  // grant route gates it to the `read` verb only (write/owner/download
1466
1466
  // everyone stay a direct-access.md edit).
1467
- const groups = [
1467
+ const plugins = [
1468
1468
  EVERYONE_DISPLAY,
1469
1469
  ...[...model.roles.byCanonical.values()].map((r) => r.displayName),
1470
1470
  ];
@@ -1487,7 +1487,7 @@ export class AccessControlService implements IAccessControl {
1487
1487
  name: name || email.split('@')[0],
1488
1488
  email,
1489
1489
  }));
1490
- return { groups, people };
1490
+ return { plugins, people };
1491
1491
  }
1492
1492
 
1493
1493
  async findEmailByHash(
@@ -14,9 +14,9 @@ import {
14
14
  * WHERE access rules are DECLARED inside a folder — a read-only inventory, not
15
15
  * a second resolver.
16
16
  *
17
- * A group's `access.md` is never the whole story: resolution is closeness-first,
17
+ * A plugin's `access.md` is never the whole story: resolution is closeness-first,
18
18
  * so a rule written on a single skill or `.tool` inside the folder overrides the
19
- * folder's rule for the principals it names. An admin looking at a group's share
19
+ * folder's rule for the principals it names. An admin looking at a plugin's share
20
20
  * list and seeing "GTM Team can read" would otherwise have no way to know that
21
21
  * one battlecard underneath says `deny read: everyone`.
22
22
  *
@@ -66,9 +66,9 @@ export interface AccessDeclaration {
66
66
  }
67
67
 
68
68
  /**
69
- * How many files one scan will read. A group folder is small by construction;
69
+ * How many files one scan will read. A plugin folder is small by construction;
70
70
  * this cap exists so a pathological folder (a whole ontology moved under
71
- * `Groups/`) degrades into an honest partial answer instead of pinning the
71
+ * `Plugins/`) degrades into an honest partial answer instead of pinning the
72
72
  * event loop. Callers surface `truncated` to the user.
73
73
  */
74
74
  export const DECLARATION_SCAN_FILE_CAP = 400;
@@ -163,7 +163,7 @@ function toEntries(byVerb: Record<Verb, ParsedEntry[]>): DeclarationEntry[] {
163
163
  *
164
164
  * Failure modes are asymmetric on purpose, matching the resolver: a node whose
165
165
  * frontmatter is malformed is skipped silently (a typo in a skill's `owner:`
166
- * must not make the group's access page unreadable), while a malformed
166
+ * must not make the plugin's access page unreadable), while a malformed
167
167
  * `access.md` is REPORTED with its `parseError` (that file was written to
168
168
  * control access and currently controls nothing).
169
169
  *
@@ -49,7 +49,7 @@ import { WorkflowDomainError } from '../workflow/workflow.errors.js';
49
49
  /** Whether the dialog target is a folder (edit folder access.md) or a file (edit node frontmatter). */
50
50
  export type TargetKind = 'folder' | 'file';
51
51
 
52
- /** Bad-request-class mutation failure (invalid principal, unknown group, lockout). */
52
+ /** Bad-request-class mutation failure (invalid principal, unknown plugin, lockout). */
53
53
  export class AccessMutationError extends WorkflowDomainError {
54
54
  constructor(message: string, status = 400, payload?: Record<string, unknown>) {
55
55
  super(message, status, payload);
@@ -305,7 +305,7 @@ export class AccessMutationService {
305
305
  * access — not just "is there a removable file entry."
306
306
  *
307
307
  * For a USER, that distinction matters: effective access includes admin-rescue
308
- * on `access.md`/`roles.yaml`, group membership, and the built-in `everyone` —
308
+ * on `access.md`/`roles.yaml`, plugin membership, and the built-in `everyone` —
309
309
  * none of which `grantSources` reports (it is MECE over file-backed
310
310
  * direct/ancestor entries only). So we ask the same `canRead/canWrite/
311
311
  * canDownload/canOwner` resolver every real access decision uses; if the
@@ -351,7 +351,7 @@ export class AccessMutationService {
351
351
  * asynchronously in the route (it needs the roles model). Here we only run the
352
352
  * cheap injection/shape validation so a malformed principal never reaches the
353
353
  * splice. Role-exists-in-roles.yaml is the route's job (so it can offer
354
- * "Create Group" for an unknown group, an admin-only flow).
354
+ * "Create Plugin" for an unknown plugin, an admin-only flow).
355
355
  */
356
356
  private assertPrincipalSafe(principal: Principal): void {
357
357
  try {
@@ -122,7 +122,7 @@ export function validatePrincipal(p: Principal): Principal {
122
122
  }
123
123
  const role = p.role.trim();
124
124
  const canonical = canonicalRoleName(role);
125
- if (!canonical) throw new AccessSpliceError('group grant needs a name');
125
+ if (!canonical) throw new AccessSpliceError('plugin grant needs a name');
126
126
  if (
127
127
  CONTROL_CHARS.test(role) ||
128
128
  role.includes('<') ||
@@ -130,16 +130,16 @@ export function validatePrincipal(p: Principal): Principal {
130
130
  role.includes(':') ||
131
131
  role.includes('#')
132
132
  ) {
133
- throw new AccessSpliceError(`invalid group name: ${JSON.stringify(p.role)}`);
133
+ throw new AccessSpliceError(`invalid plugin name: ${JSON.stringify(p.role)}`);
134
134
  }
135
135
  // `deny` is never a grantee — it's the denial prefix. `everyone` IS grantable
136
136
  // (the built-in public role); the access route restricts it to the read verb.
137
137
  if (canonical === 'deny') {
138
- throw new AccessSpliceError(`'${role}' is a reserved name and cannot be granted as a group`);
138
+ throw new AccessSpliceError(`'${role}' is a reserved name and cannot be granted as a plugin`);
139
139
  }
140
140
  const round = parseAccessEntry(role);
141
141
  if (!round.ok || round.entry.kind !== 'role' || round.entry.deny) {
142
- throw new AccessSpliceError(`group grant does not round-trip: ${JSON.stringify(p)}`);
142
+ throw new AccessSpliceError(`plugin grant does not round-trip: ${JSON.stringify(p)}`);
143
143
  }
144
144
  return { kind: 'role', role };
145
145
  }
@@ -69,19 +69,19 @@ export function createAccessRoutes(
69
69
  workflowService,
70
70
  accessControl,
71
71
  kbDirName,
72
- DEFAULT_BRANCH,
72
+ () => DEFAULT_BRANCH,
73
73
  eventBus,
74
74
  );
75
75
 
76
- // Roles (groups) are authoritative on the DEFAULT branch — the Roles admin
77
- // screen only ever edits roles.yaml there. Resolve the group list from the
76
+ // Roles (plugins) are authoritative on the DEFAULT branch — the Roles admin
77
+ // screen only ever edits roles.yaml there. Resolve the plugin list from the
78
78
  // default-branch workspace so the share dialog suggests (and the grant route
79
79
  // accepts) the same roles the Roles screen manages, regardless of which branch
80
80
  // the caller is viewing. People stay branch-local (the caller's own workspace).
81
- const defaultBranchGroups = async (): Promise<string[]> => {
81
+ const defaultBranchPlugins = async (): Promise<string[]> => {
82
82
  await workspaceService.getOrCreateForBranch(DEFAULT_BRANCH);
83
- const { groups } = await accessControl.kbPrincipals(workspaceIdForBranch(DEFAULT_BRANCH));
84
- return groups;
83
+ const { plugins } = await accessControl.kbPrincipals(workspaceIdForBranch(DEFAULT_BRANCH));
84
+ return plugins;
85
85
  };
86
86
 
87
87
  // Authentication gate only. Per PLAN §3 the workspace `:id` is a branch
@@ -177,7 +177,7 @@ export function createAccessRoutes(
177
177
  *
178
178
  * Every access declaration living INSIDE a folder — the descendant
179
179
  * `access.md` files and the node frontmatter that override the folder's own
180
- * rules for the principals they name. Display-only: the group access surface
180
+ * rules for the principals they name. Display-only: the plugin access surface
181
181
  * shows it so nobody reads a folder's share list as the whole story.
182
182
  *
183
183
  * Gating is stricter than the sibling `GET /access`, which hands its eligible
@@ -267,9 +267,9 @@ export function createAccessRoutes(
267
267
 
268
268
  /**
269
269
  * GET /api/workspace/:id/access/suggest?q=<query>
270
- * Autocomplete for the share dialog. Returns matching groups (roles.yaml role
270
+ * Autocomplete for the share dialog. Returns matching plugins (roles.yaml role
271
271
  * names — resolved from the DEFAULT branch, the authoritative roles source)
272
- * and people (branch-local to `:id`). Groups are small + non-sensitive so they
272
+ * and people (branch-local to `:id`). Plugins are small + non-sensitive so they
273
273
  * always show;
274
274
  * PEOPLE are withheld until `q` is ≥ 2 chars, so an empty query can't dump the
275
275
  * whole directory (email-harvesting guard). People are the union of the
@@ -283,10 +283,10 @@ export function createAccessRoutes(
283
283
  const q = (typeof req.query.q === 'string' ? req.query.q : '').trim().toLowerCase();
284
284
  const CAP = 15;
285
285
  try {
286
- const groups = await defaultBranchGroups();
286
+ const plugins = await defaultBranchPlugins();
287
287
  const { people: kbPeople } = await accessControl.kbPrincipals(req.params.id);
288
288
 
289
- const matchedGroups = groups
289
+ const matchedPlugins = plugins
290
290
  .filter((g) => !q || g.toLowerCase().includes(q))
291
291
  .slice(0, CAP);
292
292
 
@@ -311,7 +311,7 @@ export function createAccessRoutes(
311
311
  .slice(0, CAP);
312
312
  }
313
313
 
314
- res.json({ groups: matchedGroups, people, peopleWithheld: q.length < 2 });
314
+ res.json({ plugins: matchedPlugins, people, peopleWithheld: q.length < 2 });
315
315
  } catch (err) {
316
316
  const { status, body } = toHttpError(err);
317
317
  res.status(status).json(body);
@@ -542,19 +542,19 @@ export function createAccessRoutes(
542
542
  }
543
543
  const { repoRelTarget, kind, principal } = parseMutationBody(req.body);
544
544
 
545
- // For a group grant, the role must exist in roles.yaml (Slice 1 grants
546
- // existing groups only; Create Group is Slice 2). Reject loudly otherwise.
545
+ // For a plugin grant, the role must exist in roles.yaml (Slice 1 grants
546
+ // existing plugins only; Create Plugin is Slice 2). Reject loudly otherwise.
547
547
  // Validate against the DEFAULT-branch roles (same authoritative source the
548
548
  // suggest autocomplete uses) so a default-branch role can't be suggested
549
549
  // then rejected here on a feature branch whose roles.yaml has diverged.
550
550
  if (principal.kind === 'role') {
551
- const groups = await defaultBranchGroups();
552
- const known = groups.some((g) => canonicalRoleName(g) === canonicalRoleName(principal.role));
551
+ const plugins = await defaultBranchPlugins();
552
+ const known = plugins.some((g) => canonicalRoleName(g) === canonicalRoleName(principal.role));
553
553
  if (!known) {
554
554
  throw new AccessMutationError(
555
- `No group named "${principal.role}". Pick an existing group, or ask an admin to create it.`,
555
+ `No plugin named "${principal.role}". Pick an existing plugin, or ask an admin to create it.`,
556
556
  404,
557
- { kind: 'unknown-group', role: principal.role },
557
+ { kind: 'unknown-plugin', role: principal.role },
558
558
  );
559
559
  }
560
560
  // The built-in `everyone` role is grantable from the share UI for READ
@@ -563,7 +563,7 @@ export function createAccessRoutes(
563
563
  // access.md edit.
564
564
  if (canonicalRoleName(principal.role) === EVERYONE_CANONICAL && verb !== 'read') {
565
565
  throw new AccessMutationError(
566
- '"Everyone" can only be granted read access (public). Grant other access levels to specific people or groups.',
566
+ '"Everyone" can only be granted read access (public). Grant other access levels to specific people or plugins.',
567
567
  );
568
568
  }
569
569
  }
@@ -735,7 +735,7 @@ export function createAccessRoutes(
735
735
  // any source left is an `ancestor`. Distinguish "inherited" (still named in
736
736
  // an ancestor access.md — offer cascade/deny) from "no removable source
737
737
  // here" (nothing named in any file we can edit — a genuine no-op). A
738
- // principal who only RESOLVES via a group / everyone / admin-rescue has no
738
+ // principal who only RESOLVES via a plugin / everyone / admin-rescue has no
739
739
  // source at all (it's not their own file entry) and so is correctly a 200
740
740
  // no-op: there is nothing to remove here and no Remove was ever offered for
741
741
  // them in the dialog. NEVER a silent unchanged-success when there IS a
@@ -88,8 +88,8 @@ export interface ICreatorAccess {
88
88
  }
89
89
 
90
90
  /**
91
- * The splice-safe principal for a creator — exported because group
92
- * provisioning (`GroupProvisionService`) writes the same `Name <email>`
91
+ * The splice-safe principal for a creator — exported because plugin
92
+ * provisioning (`PluginProvisionService`) writes the same `Name <email>`
93
93
  * entries into the access.md files it seeds, and two spellings of the same
94
94
  * person would read as two people.
95
95
  */
@@ -169,9 +169,9 @@ export class CreatorAccessService implements ICreatorAccess {
169
169
  warnSkipped(rel, err);
170
170
  return null;
171
171
  }
172
- // NOTE: a direct group folder (`Groups/<Name>`) is no longer special
173
- // here. Groups — and personal folders — are made by the dedicated
174
- // provisioning endpoint (`GroupProvisionService`), which writes the
172
+ // NOTE: a direct plugin folder (`Plugins/<Name>`) is no longer special
173
+ // here. Plugins — and personal folders — are made by the dedicated
174
+ // provisioning endpoint (`PluginProvisionService`), which writes the
175
175
  // full ownership template itself; this generic read-grant only covers
176
176
  // ad-hoc folder creation elsewhere in the tree.
177
177
  return {
@@ -119,8 +119,14 @@ export class RolesAdminService {
119
119
  private readonly workflowService: IWorkflowService,
120
120
  private readonly accessControl: IAccessControl,
121
121
  private readonly kbDirName: string,
122
- /** The branch whose roles.yaml is authoritative for admin status. */
123
- private readonly defaultBranch: string,
122
+ /**
123
+ * The branch whose roles.yaml is authoritative for admin status, read
124
+ * through a thunk rather than taken by value. `DEFAULT_BRANCH` is an ES
125
+ * module live binding that stays empty until `configureBranchModel()` runs
126
+ * at boot; a service constructed before that would freeze the empty string
127
+ * and every roles route would fail branch validation until a restart.
128
+ */
129
+ private readonly defaultBranchOf: () => string,
124
130
  /**
125
131
  * Optional so existing tests can construct the service without an event
126
132
  * bus. When present, every committed write announces itself so already-open
@@ -131,6 +137,11 @@ export class RolesAdminService {
131
137
  private readonly eventBus?: WorkflowEventBus,
132
138
  ) {}
133
139
 
140
+ /** Resolved per call — see the constructor note on `defaultBranchOf`. */
141
+ private get defaultBranch(): string {
142
+ return this.defaultBranchOf();
143
+ }
144
+
134
145
  private get workspaceId(): string {
135
146
  return workspaceIdForBranch(this.defaultBranch);
136
147
  }
@@ -1,29 +1,29 @@
1
- import express from 'express';
2
- import type { IAdminAccessService } from './admin.interface.js';
3
- import '../auth/auth.middleware.js'; // Express Request augmentation
4
-
5
- /**
6
- * The CORE slice of the admin surface: just the admin-status resolver the
7
- * frontend branches on (AdminMenu's "Admin only" section, the roles-recovery
8
- * banner). The full admin router (feedback inbox, user administration /
9
- * GDPR erasure) is an enterprise extension mounted via
10
- * `ServerExtensions.authed`.
11
- *
12
- * NOTE (core split): in bevel-platform this endpoint lives in the enterprise
13
- * `admin.routes.ts`; core serves it itself so a core-only deployment can
14
- * resolve admin status. Express keeps first-match-wins, so an enterprise
15
- * overlay's own `/admin/access` (mounted later) never shadows this one —
16
- * both resolve through the same {@link IAdminAccessService}.
17
- */
18
- export function createAdminAccessRoutes(adminAccess: IAdminAccessService): express.Router {
19
- const router = express.Router();
20
-
21
- // Open to any authenticated user — returns `false` for non-admins instead of
22
- // 403 so the frontend can branch on the answer without treating a normal
23
- // user as an error case.
24
- router.get('/admin/access', async (req, res) => {
25
- res.json({ isAdmin: await adminAccess.isAdmin(req.userEmail) });
26
- });
27
-
28
- return router;
29
- }
1
+ import express from 'express';
2
+ import type { IAdminAccessService } from './admin.interface.js';
3
+ import '../auth/auth.middleware.js'; // Express Request augmentation
4
+
5
+ /**
6
+ * The CORE slice of the admin surface: just the admin-status resolver the
7
+ * frontend branches on (AdminMenu's "Admin only" section, the roles-recovery
8
+ * banner). The full admin router (feedback inbox, user administration /
9
+ * GDPR erasure) is an enterprise extension mounted via
10
+ * `ServerExtensions.authed`.
11
+ *
12
+ * NOTE (core split): in bevel-platform this endpoint lives in the enterprise
13
+ * `admin.routes.ts`; core serves it itself so a core-only deployment can
14
+ * resolve admin status. Express keeps first-match-wins, so an enterprise
15
+ * overlay's own `/admin/access` (mounted later) never shadows this one —
16
+ * both resolve through the same {@link IAdminAccessService}.
17
+ */
18
+ export function createAdminAccessRoutes(adminAccess: IAdminAccessService): express.Router {
19
+ const router = express.Router();
20
+
21
+ // Open to any authenticated user — returns `false` for non-admins instead of
22
+ // 403 so the frontend can branch on the answer without treating a normal
23
+ // user as an error case.
24
+ router.get('/admin/access', async (req, res) => {
25
+ res.json({ isAdmin: await adminAccess.isAdmin(req.userEmail) });
26
+ });
27
+
28
+ return router;
29
+ }