@bevel-software/platform-core-backend 0.10.0 → 0.11.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 (188) hide show
  1. package/dist/core/create-core-server.d.ts.map +1 -1
  2. package/dist/core/create-core-server.js +12 -1
  3. package/dist/core/create-core-server.js.map +1 -1
  4. package/dist/core/create-core-services.d.ts +22 -0
  5. package/dist/core/create-core-services.d.ts.map +1 -1
  6. package/dist/core/create-core-services.js +34 -1
  7. package/dist/core/create-core-services.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/modules/access/access-control.interface.d.ts +54 -28
  13. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  14. package/dist/modules/access/access-control.service.d.ts +129 -14
  15. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  16. package/dist/modules/access/access-control.service.js +435 -66
  17. package/dist/modules/access/access-control.service.js.map +1 -1
  18. package/dist/modules/access/access-declarations.d.ts.map +1 -1
  19. package/dist/modules/access/access-declarations.js +5 -3
  20. package/dist/modules/access/access-declarations.js.map +1 -1
  21. package/dist/modules/access/access-mutation.service.d.ts +39 -6
  22. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-mutation.service.js +78 -18
  24. package/dist/modules/access/access-mutation.service.js.map +1 -1
  25. package/dist/modules/access/access-splice.d.ts +31 -4
  26. package/dist/modules/access/access-splice.d.ts.map +1 -1
  27. package/dist/modules/access/access-splice.js +40 -16
  28. package/dist/modules/access/access-splice.js.map +1 -1
  29. package/dist/modules/access/access.routes.d.ts.map +1 -1
  30. package/dist/modules/access/access.routes.js +204 -82
  31. package/dist/modules/access/access.routes.js.map +1 -1
  32. package/dist/modules/access/admin-locked-commit.d.ts +134 -0
  33. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -0
  34. package/dist/modules/access/admin-locked-commit.js +277 -0
  35. package/dist/modules/access/admin-locked-commit.js.map +1 -0
  36. package/dist/modules/access/admin-route-helpers.d.ts +32 -0
  37. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -0
  38. package/dist/modules/access/admin-route-helpers.js +44 -0
  39. package/dist/modules/access/admin-route-helpers.js.map +1 -0
  40. package/dist/modules/access/capability-registry.d.ts +41 -0
  41. package/dist/modules/access/capability-registry.d.ts.map +1 -0
  42. package/dist/modules/access/capability-registry.js +46 -0
  43. package/dist/modules/access/capability-registry.js.map +1 -0
  44. package/dist/modules/access/directory-sync-bot.d.ts +13 -0
  45. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -0
  46. package/dist/modules/access/directory-sync-bot.js +64 -0
  47. package/dist/modules/access/directory-sync-bot.js.map +1 -0
  48. package/dist/modules/access/group-files.d.ts +83 -0
  49. package/dist/modules/access/group-files.d.ts.map +1 -0
  50. package/dist/modules/access/group-files.js +167 -0
  51. package/dist/modules/access/group-files.js.map +1 -0
  52. package/dist/modules/access/groups-admin.routes.d.ts +19 -0
  53. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -0
  54. package/dist/modules/access/groups-admin.routes.js +98 -0
  55. package/dist/modules/access/groups-admin.routes.js.map +1 -0
  56. package/dist/modules/access/groups-admin.service.d.ts +166 -0
  57. package/dist/modules/access/groups-admin.service.d.ts.map +1 -0
  58. package/dist/modules/access/groups-admin.service.js +442 -0
  59. package/dist/modules/access/groups-admin.service.js.map +1 -0
  60. package/dist/modules/access/groups-edit.d.ts +58 -0
  61. package/dist/modules/access/groups-edit.d.ts.map +1 -0
  62. package/dist/modules/access/groups-edit.js +162 -0
  63. package/dist/modules/access/groups-edit.js.map +1 -0
  64. package/dist/modules/access/reference-scan.d.ts +141 -0
  65. package/dist/modules/access/reference-scan.d.ts.map +1 -0
  66. package/dist/modules/access/reference-scan.js +440 -0
  67. package/dist/modules/access/reference-scan.js.map +1 -0
  68. package/dist/modules/access/roles-admin.service.d.ts +88 -119
  69. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  70. package/dist/modules/access/roles-admin.service.js +230 -384
  71. package/dist/modules/access/roles-admin.service.js.map +1 -1
  72. package/dist/modules/access/roles-edit.d.ts +51 -25
  73. package/dist/modules/access/roles-edit.d.ts.map +1 -1
  74. package/dist/modules/access/roles-edit.js +133 -59
  75. package/dist/modules/access/roles-edit.js.map +1 -1
  76. package/dist/modules/access/synced-groups-committer.d.ts +28 -0
  77. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -0
  78. package/dist/modules/access/synced-groups-committer.js +139 -0
  79. package/dist/modules/access/synced-groups-committer.js.map +1 -0
  80. package/dist/modules/access/synced-groups-writer.d.ts +78 -0
  81. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -0
  82. package/dist/modules/access/synced-groups-writer.js +219 -0
  83. package/dist/modules/access/synced-groups-writer.js.map +1 -0
  84. package/dist/modules/database/core-schema.d.ts +17 -0
  85. package/dist/modules/database/core-schema.d.ts.map +1 -1
  86. package/dist/modules/database/core-schema.js +9 -0
  87. package/dist/modules/database/core-schema.js.map +1 -1
  88. package/dist/modules/mcp/mcp-auth.middleware.d.ts +13 -2
  89. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  90. package/dist/modules/mcp/mcp-auth.middleware.js +61 -2
  91. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  92. package/dist/modules/mcp/mcp.routes.d.ts +9 -3
  93. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  94. package/dist/modules/mcp/mcp.routes.js +126 -2
  95. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  96. package/dist/modules/mcp/mcp.service.d.ts +14 -0
  97. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  98. package/dist/modules/mcp/mcp.service.js +6 -1
  99. package/dist/modules/mcp/mcp.service.js.map +1 -1
  100. package/dist/modules/workflow/file-lock.service.d.ts +11 -1
  101. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  102. package/dist/modules/workflow/file-lock.service.js +15 -1
  103. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  104. package/dist/modules/workflow/git/git.service.d.ts +34 -11
  105. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  106. package/dist/modules/workflow/git/git.service.js +165 -37
  107. package/dist/modules/workflow/git/git.service.js.map +1 -1
  108. package/dist/modules/workflow/locking-filesystem.d.ts +4 -0
  109. package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -1
  110. package/dist/modules/workflow/locking-filesystem.js +181 -28
  111. package/dist/modules/workflow/locking-filesystem.js.map +1 -1
  112. package/dist/modules/workflow/pending-commits.service.d.ts +10 -0
  113. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  114. package/dist/modules/workflow/pending-commits.service.js +19 -1
  115. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  116. package/dist/modules/workflow/workflow.service.d.ts +17 -2
  117. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  118. package/dist/modules/workflow/workflow.service.js +113 -19
  119. package/dist/modules/workflow/workflow.service.js.map +1 -1
  120. package/kb-template/AGENTS.md +4 -1
  121. package/migrations/0004_file_lock_mode.sql +2 -0
  122. package/migrations/meta/0004_snapshot.json +1564 -0
  123. package/migrations/meta/_journal.json +7 -0
  124. package/package.json +3 -3
  125. package/src/core/create-core-server.ts +13 -0
  126. package/src/core/create-core-services.ts +68 -0
  127. package/src/index.ts +10 -0
  128. package/src/modules/access/__tests__/access-control.service.test.ts +9 -3
  129. package/src/modules/access/__tests__/access-groups.test.ts +427 -0
  130. package/src/modules/access/__tests__/access-mutation.service.test.ts +171 -5
  131. package/src/modules/access/__tests__/access-splice.test.ts +65 -0
  132. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +337 -0
  133. package/src/modules/access/__tests__/access.routes.revoke.test.ts +48 -0
  134. package/src/modules/access/__tests__/admin-locked-commit.test.ts +221 -0
  135. package/src/modules/access/__tests__/admin-route-helpers.test.ts +61 -0
  136. package/src/modules/access/__tests__/directory-sync-bot.test.ts +106 -0
  137. package/src/modules/access/__tests__/grant-sources.test.ts +67 -0
  138. package/src/modules/access/__tests__/groups-admin.service.test.ts +440 -0
  139. package/src/modules/access/__tests__/reference-scan.test.ts +288 -0
  140. package/src/modules/access/__tests__/roles-admin.service.test.ts +161 -83
  141. package/src/modules/access/__tests__/roles-capabilities.test.ts +325 -0
  142. package/src/modules/access/__tests__/roles-edit.test.ts +104 -28
  143. package/src/modules/access/__tests__/roles.routes.test.ts +63 -40
  144. package/src/modules/access/__tests__/synced-groups-committer.test.ts +238 -0
  145. package/src/modules/access/__tests__/synced-groups-writer.test.ts +249 -0
  146. package/src/modules/access/access-control.interface.ts +66 -32
  147. package/src/modules/access/access-control.service.ts +536 -73
  148. package/src/modules/access/access-declarations.ts +5 -2
  149. package/src/modules/access/access-mutation.service.ts +88 -14
  150. package/src/modules/access/access-splice.ts +55 -17
  151. package/src/modules/access/access.routes.ts +227 -93
  152. package/src/modules/access/admin-locked-commit.ts +331 -0
  153. package/src/modules/access/admin-route-helpers.ts +55 -0
  154. package/src/modules/access/capability-registry.ts +74 -0
  155. package/src/modules/access/directory-sync-bot.ts +76 -0
  156. package/src/modules/access/group-files.ts +212 -0
  157. package/src/modules/access/groups-admin.routes.ts +113 -0
  158. package/src/modules/access/groups-admin.service.ts +551 -0
  159. package/src/modules/access/groups-edit.ts +187 -0
  160. package/src/modules/access/reference-scan.ts +513 -0
  161. package/src/modules/access/roles-admin.service.ts +290 -418
  162. package/src/modules/access/roles-edit.ts +134 -61
  163. package/src/modules/access/synced-groups-committer.ts +177 -0
  164. package/src/modules/access/synced-groups-writer.ts +303 -0
  165. package/src/modules/database/core-schema.ts +9 -0
  166. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +116 -0
  167. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  168. package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +6 -1
  169. package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +301 -0
  170. package/src/modules/mcp/mcp-auth.middleware.ts +61 -1
  171. package/src/modules/mcp/mcp.routes.ts +137 -2
  172. package/src/modules/mcp/mcp.service.ts +6 -1
  173. package/src/modules/workflow/__tests__/locking-filesystem.test.ts +336 -0
  174. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  175. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +32 -0
  176. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +13 -2
  177. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +139 -1
  178. package/src/modules/workflow/file-lock.service.ts +15 -0
  179. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +0 -1
  180. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +132 -0
  181. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +0 -1
  182. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +0 -1
  183. package/src/modules/workflow/git/git.service.ts +174 -35
  184. package/src/modules/workflow/locking-filesystem.ts +188 -26
  185. package/src/modules/workflow/pending-commits.service.ts +27 -1
  186. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +0 -1
  187. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +0 -1
  188. package/src/modules/workflow/workflow.service.ts +140 -20
@@ -29,6 +29,13 @@
29
29
  "when": 1785943322636,
30
30
  "tag": "0003_deployment_settings",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1787191920033,
37
+ "tag": "0004_file_lock_mode",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bevel-software/platform-core-backend",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,8 +46,8 @@
46
46
  "pg": "^8.20.0",
47
47
  "yaml": "^2.9.0",
48
48
  "zod": "^3.24.0",
49
- "@bevel-software/platform-mcp-core": "0.10.0",
50
- "@bevel-software/platform-shared": "0.10.0"
49
+ "@bevel-software/platform-mcp-core": "0.11.0",
50
+ "@bevel-software/platform-shared": "0.11.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/adm-zip": "^0.5.8",
@@ -28,6 +28,7 @@ import {
28
28
  createSecretsVaultPublicRoutes,
29
29
  } from '../modules/secrets-vault/index.js';
30
30
  import { createAdminAccessRoutes } from '../modules/admin/admin-access.routes.js';
31
+ import { createGroupsAdminRoutes } from '../modules/access/groups-admin.routes.js';
31
32
  import { createUpdateCheckRoutes } from '../modules/update-check/update-check.routes.js';
32
33
  import { createAccountRoutes } from '../modules/auth/account.routes.js';
33
34
  import { createSetupRoutes } from '../modules/settings/setup.routes.js';
@@ -266,6 +267,11 @@ export async function createCoreServer(
266
267
  core.mcpAuthMiddleware,
267
268
  core.authMiddleware,
268
269
  core.usageMeter,
270
+ // The local-token exchange: verifies an MCP OAuth access token and mints
271
+ // the loopback internal token the local MCP server uses for its REST reads.
272
+ core.internalTokenService,
273
+ core.mcpOAuthProvider,
274
+ core.mcpResourceMetadataUrl,
269
275
  ));
270
276
 
271
277
  // MCP OAuth 2.1 authorization server: /authorize, /token, /register,
@@ -427,6 +433,13 @@ export async function createCoreServer(
427
433
  // Admin-status resolver (CORE — see the note in admin-access.routes.ts;
428
434
  // the full admin router is an enterprise `ext.authed` extension).
429
435
  app.use('/api', core.authMiddleware, createAdminAccessRoutes(core.adminAccess));
436
+ // Groups admin (manual-mode CRUD; typed refusals in IdP mode) —
437
+ // admin-gated inside.
438
+ app.use('/api', core.authMiddleware, createGroupsAdminRoutes({
439
+ groupsAdmin: core.groupsAdminService,
440
+ adminAccess: core.adminAccess,
441
+ getUserById: async (id) => (await core.authService.getUserById(id)) ?? null,
442
+ }));
430
443
  // Update check (admin-only inside): the newest published release vs the
431
444
  // running version, cached server-side — see update-check.service.ts.
432
445
  app.use(
@@ -35,6 +35,7 @@ import { OidcAuthProvider } from '../modules/auth/oidc-auth-provider.js';
35
35
  import { createAuthMiddleware } from '../modules/auth/auth.middleware.js';
36
36
  import { AccessControlService } from '../modules/access/access-control.service.js';
37
37
  import { CreatorAccessService } from '../modules/access/creator-access.js';
38
+ import { GroupsAdminService } from '../modules/access/groups-admin.service.js';
38
39
  import { PendingSkillsService, SkillService } from '../modules/skills/index.js';
39
40
  import { ToolManualService } from '../modules/tool-manuals/index.js';
40
41
  import { McpServerEditService } from '../modules/tool-manuals/mcp-server-edit.service.js';
@@ -63,6 +64,12 @@ import {
63
64
  } from '../modules/workflow/pending-commits.worker.js';
64
65
  import { ensureRecoveryBotUser } from '../modules/workflow/recovery-bot.js';
65
66
  import { AdminAccessService } from '../modules/admin/admin-access.service.js';
67
+ import {
68
+ SyncedGroupsWriter,
69
+ type SyncedGroupsSource,
70
+ } from '../modules/access/synced-groups-writer.js';
71
+ import { createSyncedGroupsCommitter } from '../modules/access/synced-groups-committer.js';
72
+ import { ensureDirectorySyncBot } from '../modules/access/directory-sync-bot.js';
66
73
  import { ExternalApiKeyService } from '../modules/tool-auth/external-api-key.service.js';
67
74
  import {
68
75
  InternalTokenService,
@@ -133,6 +140,20 @@ export interface CoreServices {
133
140
  pendingCommitsWorker: PendingCommitsWorker;
134
141
  recoveryBot: AuthUser;
135
142
  adminAccess: AdminAccessService;
143
+ /** Manual-mode groups CRUD + the manual→IdP retirement half. */
144
+ groupsAdminService: GroupsAdminService;
145
+ /**
146
+ * Build the debounced directory → `synced-groups.yaml` materializer for a
147
+ * directory source an OVERLAY provides (e.g. a SCIM mirror fed by the IdP's
148
+ * provisioning engine — core ships no directory integration of its own).
149
+ * Ensures the directory-sync bot user and closes over the git commit
150
+ * pipeline; the overlay only implements {@link SyncedGroupsSource} and
151
+ * calls `notifyMutation()` after each directory change.
152
+ */
153
+ createSyncedGroupsMaterializer: (
154
+ source: SyncedGroupsSource,
155
+ opts?: { debounceMs?: number; log?: (message: string) => void },
156
+ ) => Promise<SyncedGroupsWriter>;
136
157
  /** Newest-release lookup behind `GET /api/update-check` (admin-only). */
137
158
  updateCheckService: UpdateCheckService;
138
159
  /** Deployment settings, env-first — the KB remote resolves through these. */
@@ -151,6 +172,12 @@ export interface CoreServices {
151
172
  mcpService: McpService;
152
173
  mcpAuthMiddleware: ReturnType<typeof createMcpAuthMiddleware>;
153
174
  mcpOAuthProvider: BevelOAuthProvider;
175
+ /**
176
+ * RFC 9728 protected-resource-metadata URL carried on MCP 401 challenges.
177
+ * Published so the route layer's own challenges (the local-token exchange)
178
+ * advertise the same pointer the auth middleware does.
179
+ */
180
+ mcpResourceMetadataUrl: string;
154
181
  toolRegistry: ToolRegistry;
155
182
  toolAuthMiddleware: ReturnType<typeof createToolAuthMiddleware>;
156
183
  manualAuthMiddleware: ReturnType<typeof createManualAuthMiddleware>;
@@ -627,6 +654,7 @@ export async function createCoreServices(
627
654
  externalApiKeyService,
628
655
  mcpOAuthProvider,
629
656
  mcpResourceMetadataUrl,
657
+ internalTokenService,
630
658
  );
631
659
 
632
660
  // ── Unified tool surface ──────────────────────────────────────────────
@@ -725,6 +753,43 @@ export async function createCoreServices(
725
753
  );
726
754
  }
727
755
 
756
+ // Materializer factory for an overlay-provided directory source: every
757
+ // provisioning burst regenerates `synced-groups.yaml` on the default branch
758
+ // (debounced), committed by the sync bot — that file is what puts access
759
+ // resolution in IdP mode. Core itself ships no directory integration, so
760
+ // nothing is constructed here until an overlay brings a source.
761
+ const createSyncedGroupsMaterializer = async (
762
+ source: SyncedGroupsSource,
763
+ opts?: { debounceMs?: number; log?: (message: string) => void },
764
+ ): Promise<SyncedGroupsWriter> => {
765
+ const bot = await ensureDirectorySyncBot(db);
766
+ return new SyncedGroupsWriter({
767
+ source,
768
+ ...createSyncedGroupsCommitter({
769
+ workspaceService,
770
+ workflowService,
771
+ accessControl,
772
+ eventBus,
773
+ kbDirName,
774
+ bot,
775
+ defaultBranchOf: () => DEFAULT_BRANCH,
776
+ }),
777
+ debounceMs: opts?.debounceMs,
778
+ log: opts?.log ?? ((message) => console.warn(message)),
779
+ });
780
+ };
781
+ // Groups — the "who you are" principal sets. Manual-mode CRUD on
782
+ // groups.yaml, IdP-mode refusals, and the connect-time retirement the
783
+ // directory token route drives.
784
+ const groupsAdminService = new GroupsAdminService(
785
+ workspaceService,
786
+ workflowService,
787
+ accessControl,
788
+ kbDirName,
789
+ () => DEFAULT_BRANCH,
790
+ eventBus,
791
+ );
792
+
728
793
  return {
729
794
  config,
730
795
  db,
@@ -759,6 +824,8 @@ export async function createCoreServices(
759
824
  pendingCommitsWorker,
760
825
  recoveryBot,
761
826
  adminAccess,
827
+ groupsAdminService,
828
+ createSyncedGroupsMaterializer,
762
829
  updateCheckService,
763
830
  secretsVaultService,
764
831
  externalApiKeyService,
@@ -766,6 +833,7 @@ export async function createCoreServices(
766
833
  mcpService,
767
834
  mcpAuthMiddleware,
768
835
  mcpOAuthProvider,
836
+ mcpResourceMetadataUrl,
769
837
  toolRegistry,
770
838
  toolAuthMiddleware,
771
839
  manualAuthMiddleware,
package/src/index.ts CHANGED
@@ -52,6 +52,16 @@ export type {
52
52
  } from './modules/tool-auth/external-api-key.interface.js';
53
53
 
54
54
  // Key port/seam types an overlay implements.
55
+ // SyncedGroupsWriter is a CLASS — exported by VALUE so an overlay can
56
+ // construct one; the seam types (including the writer's deps) stay type-only.
57
+ export { SyncedGroupsWriter } from './modules/access/synced-groups-writer.js';
58
+ export type {
59
+ SyncedGroupsSource,
60
+ SyncedGroupRecord,
61
+ SyncedGroupMember,
62
+ SyncedGroupsWriteResult,
63
+ SyncedGroupsWriterDeps,
64
+ } from './modules/access/synced-groups-writer.js';
55
65
  export type { ISessionSink } from './modules/workspace/session-sink.js';
56
66
  export type {
57
67
  ISystemNoticeSink,
@@ -283,6 +283,12 @@ describe('AccessControlService', () => {
283
283
  const e = await svc.eligibleWriters(workspaceId, 'Knowledge/Foo.md');
284
284
  expect(e.roles).toEqual(['Admin']);
285
285
  expect(e.users.map((u) => u.email)).toEqual(['felix@example.com']);
286
+ // The kinded twin of `roles`: a roles.yaml principal reads kind 'role'.
287
+ expect(e.principals).toEqual([{ name: 'Admin', kind: 'role' }]);
288
+ // The Admin-override insertion (write on an access.md is admin-rescued)
289
+ // also carries kind 'role' — it is the role's capability, never a group's.
290
+ const onAccessMd = await svc.eligibleWriters(workspaceId, 'access.md');
291
+ expect(onAccessMd.principals).toContainEqual({ name: 'Admin', kind: 'role' });
286
292
  });
287
293
 
288
294
  it('throws AccessConfigError when roles.yaml is missing', async () => {
@@ -947,7 +953,7 @@ describe('AccessControlService', () => {
947
953
 
948
954
  const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR);
949
955
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Foo.md');
950
- expect(e).toEqual({ restricted: true, roles: [], users: [] });
956
+ expect(e).toEqual({ restricted: true, principals: [], roles: [], users: [] });
951
957
  });
952
958
 
953
959
  it('reports restricted=false when read: everyone applies', async () => {
@@ -957,7 +963,7 @@ describe('AccessControlService', () => {
957
963
 
958
964
  const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR);
959
965
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Foo.md');
960
- expect(e).toEqual({ restricted: false, roles: [], users: [] });
966
+ expect(e).toEqual({ restricted: false, principals: [], roles: [], users: [] });
961
967
  });
962
968
 
963
969
  it('restricted=false when a closer everyone grant shadows a farther by-name deny', async () => {
@@ -972,7 +978,7 @@ describe('AccessControlService', () => {
972
978
  // node really is readable by everyone — not restricted.
973
979
  expect(await svc.canRead(workspaceId, 'felix@example.com', 'Knowledge/Open/Foo.md')).toBe(true);
974
980
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Open/Foo.md');
975
- expect(e).toEqual({ restricted: false, roles: [], users: [] });
981
+ expect(e).toEqual({ restricted: false, principals: [], roles: [], users: [] });
976
982
  });
977
983
 
978
984
  it('restricted=true when a same-scope by-name deny carves someone out of read: everyone', async () => {