@bevel-software/platform-core-backend 0.9.1 → 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 (212) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/dist/core/create-core-server.d.ts.map +1 -1
  3. package/dist/core/create-core-server.js +16 -1
  4. package/dist/core/create-core-server.js.map +1 -1
  5. package/dist/core/create-core-services.d.ts +25 -0
  6. package/dist/core/create-core-services.d.ts.map +1 -1
  7. package/dist/core/create-core-services.js +45 -1
  8. package/dist/core/create-core-services.js.map +1 -1
  9. package/dist/core-config.d.ts +19 -1
  10. package/dist/core-config.d.ts.map +1 -1
  11. package/dist/core-config.js +44 -4
  12. package/dist/core-config.js.map +1 -1
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/modules/access/access-control.interface.d.ts +54 -28
  18. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  19. package/dist/modules/access/access-control.service.d.ts +129 -14
  20. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  21. package/dist/modules/access/access-control.service.js +435 -66
  22. package/dist/modules/access/access-control.service.js.map +1 -1
  23. package/dist/modules/access/access-declarations.d.ts.map +1 -1
  24. package/dist/modules/access/access-declarations.js +5 -3
  25. package/dist/modules/access/access-declarations.js.map +1 -1
  26. package/dist/modules/access/access-mutation.service.d.ts +39 -6
  27. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-mutation.service.js +78 -18
  29. package/dist/modules/access/access-mutation.service.js.map +1 -1
  30. package/dist/modules/access/access-splice.d.ts +31 -4
  31. package/dist/modules/access/access-splice.d.ts.map +1 -1
  32. package/dist/modules/access/access-splice.js +40 -16
  33. package/dist/modules/access/access-splice.js.map +1 -1
  34. package/dist/modules/access/access.routes.d.ts.map +1 -1
  35. package/dist/modules/access/access.routes.js +204 -82
  36. package/dist/modules/access/access.routes.js.map +1 -1
  37. package/dist/modules/access/admin-locked-commit.d.ts +134 -0
  38. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -0
  39. package/dist/modules/access/admin-locked-commit.js +277 -0
  40. package/dist/modules/access/admin-locked-commit.js.map +1 -0
  41. package/dist/modules/access/admin-route-helpers.d.ts +32 -0
  42. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -0
  43. package/dist/modules/access/admin-route-helpers.js +44 -0
  44. package/dist/modules/access/admin-route-helpers.js.map +1 -0
  45. package/dist/modules/access/capability-registry.d.ts +41 -0
  46. package/dist/modules/access/capability-registry.d.ts.map +1 -0
  47. package/dist/modules/access/capability-registry.js +46 -0
  48. package/dist/modules/access/capability-registry.js.map +1 -0
  49. package/dist/modules/access/directory-sync-bot.d.ts +13 -0
  50. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -0
  51. package/dist/modules/access/directory-sync-bot.js +64 -0
  52. package/dist/modules/access/directory-sync-bot.js.map +1 -0
  53. package/dist/modules/access/group-files.d.ts +83 -0
  54. package/dist/modules/access/group-files.d.ts.map +1 -0
  55. package/dist/modules/access/group-files.js +167 -0
  56. package/dist/modules/access/group-files.js.map +1 -0
  57. package/dist/modules/access/groups-admin.routes.d.ts +19 -0
  58. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -0
  59. package/dist/modules/access/groups-admin.routes.js +98 -0
  60. package/dist/modules/access/groups-admin.routes.js.map +1 -0
  61. package/dist/modules/access/groups-admin.service.d.ts +166 -0
  62. package/dist/modules/access/groups-admin.service.d.ts.map +1 -0
  63. package/dist/modules/access/groups-admin.service.js +442 -0
  64. package/dist/modules/access/groups-admin.service.js.map +1 -0
  65. package/dist/modules/access/groups-edit.d.ts +58 -0
  66. package/dist/modules/access/groups-edit.d.ts.map +1 -0
  67. package/dist/modules/access/groups-edit.js +162 -0
  68. package/dist/modules/access/groups-edit.js.map +1 -0
  69. package/dist/modules/access/reference-scan.d.ts +141 -0
  70. package/dist/modules/access/reference-scan.d.ts.map +1 -0
  71. package/dist/modules/access/reference-scan.js +440 -0
  72. package/dist/modules/access/reference-scan.js.map +1 -0
  73. package/dist/modules/access/roles-admin.service.d.ts +88 -119
  74. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  75. package/dist/modules/access/roles-admin.service.js +230 -384
  76. package/dist/modules/access/roles-admin.service.js.map +1 -1
  77. package/dist/modules/access/roles-edit.d.ts +51 -25
  78. package/dist/modules/access/roles-edit.d.ts.map +1 -1
  79. package/dist/modules/access/roles-edit.js +133 -59
  80. package/dist/modules/access/roles-edit.js.map +1 -1
  81. package/dist/modules/access/synced-groups-committer.d.ts +28 -0
  82. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -0
  83. package/dist/modules/access/synced-groups-committer.js +139 -0
  84. package/dist/modules/access/synced-groups-committer.js.map +1 -0
  85. package/dist/modules/access/synced-groups-writer.d.ts +78 -0
  86. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -0
  87. package/dist/modules/access/synced-groups-writer.js +219 -0
  88. package/dist/modules/access/synced-groups-writer.js.map +1 -0
  89. package/dist/modules/database/core-schema.d.ts +17 -0
  90. package/dist/modules/database/core-schema.d.ts.map +1 -1
  91. package/dist/modules/database/core-schema.js +9 -0
  92. package/dist/modules/database/core-schema.js.map +1 -1
  93. package/dist/modules/mcp/mcp-auth.middleware.d.ts +13 -2
  94. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  95. package/dist/modules/mcp/mcp-auth.middleware.js +61 -2
  96. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  97. package/dist/modules/mcp/mcp.routes.d.ts +9 -3
  98. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  99. package/dist/modules/mcp/mcp.routes.js +126 -2
  100. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  101. package/dist/modules/mcp/mcp.service.d.ts +14 -0
  102. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  103. package/dist/modules/mcp/mcp.service.js +6 -1
  104. package/dist/modules/mcp/mcp.service.js.map +1 -1
  105. package/dist/modules/update-check/update-check.routes.d.ts +13 -0
  106. package/dist/modules/update-check/update-check.routes.d.ts.map +1 -0
  107. package/dist/modules/update-check/update-check.routes.js +21 -0
  108. package/dist/modules/update-check/update-check.routes.js.map +1 -0
  109. package/dist/modules/update-check/update-check.service.d.ts +57 -0
  110. package/dist/modules/update-check/update-check.service.d.ts.map +1 -0
  111. package/dist/modules/update-check/update-check.service.js +109 -0
  112. package/dist/modules/update-check/update-check.service.js.map +1 -0
  113. package/dist/modules/workflow/file-lock.service.d.ts +11 -1
  114. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  115. package/dist/modules/workflow/file-lock.service.js +15 -1
  116. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  117. package/dist/modules/workflow/git/git.service.d.ts +34 -11
  118. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  119. package/dist/modules/workflow/git/git.service.js +173 -37
  120. package/dist/modules/workflow/git/git.service.js.map +1 -1
  121. package/dist/modules/workflow/locking-filesystem.d.ts +4 -0
  122. package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -1
  123. package/dist/modules/workflow/locking-filesystem.js +181 -28
  124. package/dist/modules/workflow/locking-filesystem.js.map +1 -1
  125. package/dist/modules/workflow/pending-commits.service.d.ts +10 -0
  126. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  127. package/dist/modules/workflow/pending-commits.service.js +19 -1
  128. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  129. package/dist/modules/workflow/workflow.service.d.ts +17 -2
  130. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  131. package/dist/modules/workflow/workflow.service.js +113 -19
  132. package/dist/modules/workflow/workflow.service.js.map +1 -1
  133. package/dist/version.d.ts +14 -0
  134. package/dist/version.d.ts.map +1 -1
  135. package/dist/version.js +25 -0
  136. package/dist/version.js.map +1 -1
  137. package/kb-template/AGENTS.md +4 -1
  138. package/migrations/0004_file_lock_mode.sql +2 -0
  139. package/migrations/meta/0004_snapshot.json +1564 -0
  140. package/migrations/meta/_journal.json +7 -0
  141. package/package.json +4 -4
  142. package/src/__tests__/core-config.domain.test.ts +92 -0
  143. package/src/core/create-core-server.ts +21 -0
  144. package/src/core/create-core-services.ts +82 -0
  145. package/src/core-config.ts +48 -4
  146. package/src/index.ts +10 -0
  147. package/src/modules/access/__tests__/access-control.service.test.ts +9 -3
  148. package/src/modules/access/__tests__/access-groups.test.ts +427 -0
  149. package/src/modules/access/__tests__/access-mutation.service.test.ts +171 -5
  150. package/src/modules/access/__tests__/access-splice.test.ts +65 -0
  151. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +337 -0
  152. package/src/modules/access/__tests__/access.routes.revoke.test.ts +48 -0
  153. package/src/modules/access/__tests__/admin-locked-commit.test.ts +221 -0
  154. package/src/modules/access/__tests__/admin-route-helpers.test.ts +61 -0
  155. package/src/modules/access/__tests__/directory-sync-bot.test.ts +106 -0
  156. package/src/modules/access/__tests__/grant-sources.test.ts +67 -0
  157. package/src/modules/access/__tests__/groups-admin.service.test.ts +440 -0
  158. package/src/modules/access/__tests__/reference-scan.test.ts +288 -0
  159. package/src/modules/access/__tests__/roles-admin.service.test.ts +161 -83
  160. package/src/modules/access/__tests__/roles-capabilities.test.ts +325 -0
  161. package/src/modules/access/__tests__/roles-edit.test.ts +104 -28
  162. package/src/modules/access/__tests__/roles.routes.test.ts +63 -40
  163. package/src/modules/access/__tests__/synced-groups-committer.test.ts +238 -0
  164. package/src/modules/access/__tests__/synced-groups-writer.test.ts +249 -0
  165. package/src/modules/access/access-control.interface.ts +66 -32
  166. package/src/modules/access/access-control.service.ts +536 -73
  167. package/src/modules/access/access-declarations.ts +5 -2
  168. package/src/modules/access/access-mutation.service.ts +88 -14
  169. package/src/modules/access/access-splice.ts +55 -17
  170. package/src/modules/access/access.routes.ts +227 -93
  171. package/src/modules/access/admin-locked-commit.ts +331 -0
  172. package/src/modules/access/admin-route-helpers.ts +55 -0
  173. package/src/modules/access/capability-registry.ts +74 -0
  174. package/src/modules/access/directory-sync-bot.ts +76 -0
  175. package/src/modules/access/group-files.ts +212 -0
  176. package/src/modules/access/groups-admin.routes.ts +113 -0
  177. package/src/modules/access/groups-admin.service.ts +551 -0
  178. package/src/modules/access/groups-edit.ts +187 -0
  179. package/src/modules/access/reference-scan.ts +513 -0
  180. package/src/modules/access/roles-admin.service.ts +290 -418
  181. package/src/modules/access/roles-edit.ts +134 -61
  182. package/src/modules/access/synced-groups-committer.ts +177 -0
  183. package/src/modules/access/synced-groups-writer.ts +303 -0
  184. package/src/modules/database/core-schema.ts +9 -0
  185. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +116 -0
  186. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  187. package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +6 -1
  188. package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +301 -0
  189. package/src/modules/mcp/mcp-auth.middleware.ts +61 -1
  190. package/src/modules/mcp/mcp.routes.ts +137 -2
  191. package/src/modules/mcp/mcp.service.ts +6 -1
  192. package/src/modules/update-check/__tests__/update-check.routes.test.ts +69 -0
  193. package/src/modules/update-check/__tests__/update-check.service.test.ts +170 -0
  194. package/src/modules/update-check/update-check.routes.ts +28 -0
  195. package/src/modules/update-check/update-check.service.ts +130 -0
  196. package/src/modules/workflow/__tests__/locking-filesystem.test.ts +336 -0
  197. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  198. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +32 -0
  199. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +13 -2
  200. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +139 -1
  201. package/src/modules/workflow/file-lock.service.ts +15 -0
  202. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +0 -1
  203. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +132 -0
  204. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +0 -1
  205. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +0 -1
  206. package/src/modules/workflow/git/git.service.ts +182 -35
  207. package/src/modules/workflow/locking-filesystem.ts +188 -26
  208. package/src/modules/workflow/pending-commits.service.ts +27 -1
  209. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +0 -1
  210. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +0 -1
  211. package/src/modules/workflow/workflow.service.ts +140 -20
  212. package/src/version.ts +28 -0
@@ -0,0 +1,64 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { users } from '../database/core-schema.js';
3
+ /**
4
+ * Synthetic identity for the synced-groups materializer's commits: every
5
+ * `synced-groups.yaml` regeneration lands attributed to this bot, so the git
6
+ * history reads as "the system mirrored the IdP", never as any human.
7
+ * Same `<role>-bot@<host>` convention (and race-safe ensure shape) as the
8
+ * recovery bot.
9
+ */
10
+ // Trimmed BEFORE the empty-fallback so a whitespace-only override falls back,
11
+ // and so the constant always compares equal to a trimmed caller email (the
12
+ // machine-owned write rule trims the caller's side).
13
+ export const DIRECTORY_SYNC_BOT_EMAIL = (process.env.DIRECTORY_SYNC_BOT_EMAIL?.trim() || 'directory-sync@bevel.local').toLowerCase();
14
+ export const DIRECTORY_SYNC_BOT_NAME = 'Directory Sync Bot';
15
+ export async function ensureDirectorySyncBot(db) {
16
+ const inserted = await db
17
+ .insert(users)
18
+ .values({ email: DIRECTORY_SYNC_BOT_EMAIL, name: DIRECTORY_SYNC_BOT_NAME })
19
+ .onConflictDoNothing({ target: users.email })
20
+ .returning();
21
+ if (inserted.length > 0) {
22
+ const row = inserted[0];
23
+ console.log(`[directory-sync] created sync-bot user id=${row.id} email=${row.email}`);
24
+ return { id: row.id, email: row.email, name: row.name, avatarUrl: row.avatarUrl ?? undefined };
25
+ }
26
+ const [row] = await db
27
+ .select()
28
+ .from(users)
29
+ .where(eq(users.email, DIRECTORY_SYNC_BOT_EMAIL))
30
+ .limit(1);
31
+ if (!row) {
32
+ throw new Error(`directory-sync bot (${DIRECTORY_SYNC_BOT_EMAIL}) disappeared between insert-conflict and re-select`);
33
+ }
34
+ // The bot's IDENTITY is its email: the machine-owned write rule on
35
+ // synced-groups.yaml authorizes by DIRECTORY_SYNC_BOT_EMAIL alone. If the
36
+ // email already belongs to a pre-existing HUMAN account (a person signed up
37
+ // with it, or the override points at someone's address), silently binding
38
+ // the materializer to that row would attribute every sync commit to them —
39
+ // and, worse, hand their interactive session the bot's exclusive write
40
+ // authority. The users schema carries no dedicated bot/provenance column,
41
+ // so adoption gates on the strongest invariants the row can carry:
42
+ // 1. NO PASSWORD. The bot is only ever created by this function, without
43
+ // credentials, and never signs in — nothing legitimate ever provisions
44
+ // it a password. A row with a password hash is a login-capable account
45
+ // (a person, or a credential a person could use) no matter what its
46
+ // display name says, so the hash is the hard evidence a name can't be.
47
+ // 2. THE EXPECTED NAME. A display name is mutable and a human account
48
+ // could carry the bot's name (e.g. an SSO service account), so this is
49
+ // only a tripwire — it still catches password-less human rows created
50
+ // under their real names.
51
+ // A password-less SSO account that also mimics the bot's exact display name
52
+ // is indistinguishable at this layer; that shape only arises from the
53
+ // operator deliberately configuring it. Refuse loudly instead of adopting.
54
+ if (row.passwordHash !== null || row.name !== DIRECTORY_SYNC_BOT_NAME) {
55
+ const evidence = row.passwordHash !== null
56
+ ? `it can sign in with a password (name '${row.name}')`
57
+ : `its name is '${row.name}'`;
58
+ throw new Error(`directory-sync bot email ${DIRECTORY_SYNC_BOT_EMAIL} already belongs to an existing account ` +
59
+ `(id=${row.id}) that is not the sync bot — ${evidence}. Refusing to bind the materializer ` +
60
+ `to it. Set DIRECTORY_SYNC_BOT_EMAIL to an address no person uses.`);
61
+ }
62
+ return { id: row.id, email: row.email, name: row.name, avatarUrl: row.avatarUrl ?? undefined };
63
+ }
64
+ //# sourceMappingURL=directory-sync-bot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-sync-bot.js","sourceRoot":"","sources":["../../../src/modules/access/directory-sync-bot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AAEnD;;;;;;GAMG;AAEH,8EAA8E;AAC9E,2EAA2E;AAC3E,qDAAqD;AACrD,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACtC,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,EAAE,IAAI,4BAA4B,CAC7E,CAAC,WAAW,EAAE,CAAC;AAEhB,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAE5D,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,EAAY;IACvD,MAAM,QAAQ,GAAG,MAAM,EAAE;SACtB,MAAM,CAAC,KAAK,CAAC;SACb,MAAM,CAAC,EAAE,KAAK,EAAE,wBAAwB,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;SAC1E,mBAAmB,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;SAC5C,SAAS,EAAE,CAAC;IACf,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,6CAA6C,GAAG,CAAC,EAAE,UAAU,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;QACtF,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS,EAAE,CAAC;IACjG,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;SACnB,MAAM,EAAE;SACR,IAAI,CAAC,KAAK,CAAC;SACX,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CAAC;SAChD,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,uBAAuB,wBAAwB,qDAAqD,CACrG,CAAC;IACJ,CAAC;IACD,mEAAmE;IACnE,0EAA0E;IAC1E,4EAA4E;IAC5E,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,0EAA0E;IAC1E,mEAAmE;IACnE,2EAA2E;IAC3E,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,4EAA4E;IAC5E,wEAAwE;IACxE,4EAA4E;IAC5E,2EAA2E;IAC3E,+BAA+B;IAC/B,4EAA4E;IAC5E,sEAAsE;IACtE,2EAA2E;IAC3E,IAAI,GAAG,CAAC,YAAY,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,KAAK,uBAAuB,EAAE,CAAC;QACtE,MAAM,QAAQ,GACZ,GAAG,CAAC,YAAY,KAAK,IAAI;YACvB,CAAC,CAAC,yCAAyC,GAAG,CAAC,IAAI,IAAI;YACvD,CAAC,CAAC,gBAAgB,GAAG,CAAC,IAAI,GAAG,CAAC;QAClC,MAAM,IAAI,KAAK,CACb,4BAA4B,wBAAwB,0CAA0C;YAC5F,OAAO,GAAG,CAAC,EAAE,gCAAgC,QAAQ,sCAAsC;YAC3F,mEAAmE,CACtE,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS,EAAE,CAAC;AACjG,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Group files — the "who you are" half of the roles/groups split.
3
+ *
4
+ * Groups are people-sets referenced by access grants ("GTM Team can read the
5
+ * battlecards"). A deployment has exactly ONE group source at a time — a mode
6
+ * derived from which file exists, so it holds at any git ref:
7
+ *
8
+ * - `synced-groups.yaml` EXISTS → IdP mode. The file is MACHINE-OWNED,
9
+ * regenerated wholesale from the SCIM directory mirror; membership is
10
+ * managed in the IdP. `groups.yaml` is ignored entirely.
11
+ * - otherwise → manual mode. `groups.yaml` (UI-edited)
12
+ * is the source.
13
+ *
14
+ * Both files share one format, the group twin of roles.yaml:
15
+ *
16
+ * groups:
17
+ * Engineering:
18
+ * - ada@x.io
19
+ * - bo@x.io
20
+ * Empty Group: []
21
+ *
22
+ * Parsing is FORGIVING at the entry level by design — a malformed email or a
23
+ * reserved/duplicate name skips that entry with a warning, it never takes the
24
+ * whole file down. Only structural failures (unparsable YAML, `groups:` not a
25
+ * mapping) reject the file, and even then the RESOLVER degrades to "this file
26
+ * contributes nothing" rather than throwing: unlike roles.yaml there is no
27
+ * admin-lockout risk here, so groups must never be able to brick access
28
+ * resolution. Strict acceptance for WRITES is the job of the write-side
29
+ * validator (`validateGroupsFile` below), which treats warnings as refusals.
30
+ */
31
+ export declare const GROUPS_YAML = "groups.yaml";
32
+ export declare const SYNCED_GROUPS_YAML = "synced-groups.yaml";
33
+ /**
34
+ * THE name-safety predicate for principal display names — the single source
35
+ * of truth every surface derives from (group creation/rename, IdP sync
36
+ * materialization; thin assert wrappers sit on top). Returns a human-readable
37
+ * reason the name is unsafe, or null when it is fine.
38
+ *
39
+ * The character rules keep the emitted YAML parseable and the entry grammar
40
+ * unambiguous:
41
+ * `:` mis-tokenises as a nested mapping key
42
+ * `#` truncated as a comment by the subset parser's stripComment
43
+ * `<` / `>` collide with the `Name <email>` user-reference shape
44
+ * leading - tokenises as a list item
45
+ * \x00-\x1f control chars / newlines break line structure outright
46
+ * `role/…` reserved: that spelling is the EXPLICIT role token in access
47
+ * entries — a group carrying it could never be referenced.
48
+ */
49
+ export declare function unsafeNameReason(displayName: string): string | null;
50
+ /** One parsed group: display name + canonicalised member emails. */
51
+ export interface GroupDefinition {
52
+ displayName: string;
53
+ emails: Set<string>;
54
+ }
55
+ /** Canonical group name → definition, in file order. */
56
+ export type GroupsIndex = Map<string, GroupDefinition>;
57
+ export interface ParsedGroupsFile {
58
+ ok: true;
59
+ groups: GroupsIndex;
60
+ /** Entry-level problems that were skipped over (bad email, dup name, …). */
61
+ warnings: string[];
62
+ }
63
+ export interface GroupsFileError {
64
+ ok: false;
65
+ errors: string[];
66
+ }
67
+ /**
68
+ * Parse a group file (either of the two — same grammar). See the module note
69
+ * for the forgiving-vs-structural split.
70
+ */
71
+ export declare function parseGroupsFile(text: string, filename: string): ParsedGroupsFile | GroupsFileError;
72
+ /**
73
+ * Write-gate twin of {@link parseGroupsFile}: strict — structural errors AND
74
+ * entry-level warnings both refuse the candidate, so a UI write can never land
75
+ * a group the resolver would silently skip.
76
+ */
77
+ export declare function validateGroupsFile(text: string, filename: string): {
78
+ ok: true;
79
+ } | {
80
+ ok: false;
81
+ errors: string[];
82
+ };
83
+ //# sourceMappingURL=group-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-files.d.ts","sourceRoot":"","sources":["../../../src/modules/access/group-files.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWnE;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACrB;AAED,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,WAAW,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,gBAAgB,GAAG,eAAe,CAyFpC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAWhD"}
@@ -0,0 +1,167 @@
1
+ import { EMAIL_REGEX, GROUP_REF_PREFIX, RESERVED_ROLE_NAMES, ROLE_TOKEN_PREFIX, canonicalEmail, canonicalRoleName, parseYamlSubset, } from './access-control.service.js';
2
+ /**
3
+ * Group files — the "who you are" half of the roles/groups split.
4
+ *
5
+ * Groups are people-sets referenced by access grants ("GTM Team can read the
6
+ * battlecards"). A deployment has exactly ONE group source at a time — a mode
7
+ * derived from which file exists, so it holds at any git ref:
8
+ *
9
+ * - `synced-groups.yaml` EXISTS → IdP mode. The file is MACHINE-OWNED,
10
+ * regenerated wholesale from the SCIM directory mirror; membership is
11
+ * managed in the IdP. `groups.yaml` is ignored entirely.
12
+ * - otherwise → manual mode. `groups.yaml` (UI-edited)
13
+ * is the source.
14
+ *
15
+ * Both files share one format, the group twin of roles.yaml:
16
+ *
17
+ * groups:
18
+ * Engineering:
19
+ * - ada@x.io
20
+ * - bo@x.io
21
+ * Empty Group: []
22
+ *
23
+ * Parsing is FORGIVING at the entry level by design — a malformed email or a
24
+ * reserved/duplicate name skips that entry with a warning, it never takes the
25
+ * whole file down. Only structural failures (unparsable YAML, `groups:` not a
26
+ * mapping) reject the file, and even then the RESOLVER degrades to "this file
27
+ * contributes nothing" rather than throwing: unlike roles.yaml there is no
28
+ * admin-lockout risk here, so groups must never be able to brick access
29
+ * resolution. Strict acceptance for WRITES is the job of the write-side
30
+ * validator (`validateGroupsFile` below), which treats warnings as refusals.
31
+ */
32
+ export const GROUPS_YAML = 'groups.yaml';
33
+ export const SYNCED_GROUPS_YAML = 'synced-groups.yaml';
34
+ /**
35
+ * THE name-safety predicate for principal display names — the single source
36
+ * of truth every surface derives from (group creation/rename, IdP sync
37
+ * materialization; thin assert wrappers sit on top). Returns a human-readable
38
+ * reason the name is unsafe, or null when it is fine.
39
+ *
40
+ * The character rules keep the emitted YAML parseable and the entry grammar
41
+ * unambiguous:
42
+ * `:` mis-tokenises as a nested mapping key
43
+ * `#` truncated as a comment by the subset parser's stripComment
44
+ * `<` / `>` collide with the `Name <email>` user-reference shape
45
+ * leading - tokenises as a list item
46
+ * \x00-\x1f control chars / newlines break line structure outright
47
+ * `role/…` reserved: that spelling is the EXPLICIT role token in access
48
+ * entries — a group carrying it could never be referenced.
49
+ */
50
+ export function unsafeNameReason(displayName) {
51
+ const trimmed = displayName.trim();
52
+ if (!trimmed)
53
+ return 'empty name';
54
+ if (/[:#<>]/.test(trimmed))
55
+ return "contains ':', '#', '<', or '>'";
56
+ // eslint-disable-next-line no-control-regex
57
+ if (/[\x00-\x1f]/.test(trimmed))
58
+ return 'contains control characters';
59
+ if (trimmed.startsWith('-'))
60
+ return "starts with '-'";
61
+ if (canonicalRoleName(trimmed).startsWith(ROLE_TOKEN_PREFIX)) {
62
+ return `starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix (the explicit role token in access entries)`;
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Parse a group file (either of the two — same grammar). See the module note
68
+ * for the forgiving-vs-structural split.
69
+ */
70
+ export function parseGroupsFile(text, filename) {
71
+ // Tolerate empty keys at parse: this file's contract is ENTRY-level
72
+ // forgiveness (a blank group name is skipped with a warning below), and a
73
+ // hard tokenizer error here would retire every OTHER group fail-closed.
74
+ const parsed = parseYamlSubset(text, { tolerateEmptyKeys: true });
75
+ if (!parsed.ok)
76
+ return { ok: false, errors: [`${filename}: ${parsed.error}`] };
77
+ const root = parsed.value;
78
+ if (root == null || typeof root !== 'object' || Array.isArray(root)) {
79
+ return { ok: false, errors: [`${filename}: must be a top-level mapping`] };
80
+ }
81
+ const groupsNode = root.groups;
82
+ // An empty file (or one with no `groups:` key yet) is a valid empty set —
83
+ // a fresh deployment's groups.yaml starts this way.
84
+ if (groupsNode == null)
85
+ return { ok: true, groups: new Map(), warnings: [] };
86
+ if (typeof groupsNode !== 'object' || Array.isArray(groupsNode)) {
87
+ return { ok: false, errors: [`${filename}: 'groups' must be a mapping`] };
88
+ }
89
+ const groups = new Map();
90
+ const warnings = [];
91
+ for (const [displayName, value] of Object.entries(groupsNode)) {
92
+ const canonical = canonicalRoleName(displayName);
93
+ if (!canonical) {
94
+ warnings.push(`${filename}: empty group name — skipped`);
95
+ continue;
96
+ }
97
+ // THE shared name-safety predicate (see `unsafeNameReason` above) — the
98
+ // same rules every write surface asserts. The parser must apply it too:
99
+ // a name the writers refuse (control chars, `<`/`>`, a leading `-`, the
100
+ // reserved `role/` prefix) can still reach a group file by hand edit or
101
+ // through a skewed writer, and accepting it here would let the resolver
102
+ // honor — and `validateGroupsFile` pass — a name no editor can ever
103
+ // produce or reference safely.
104
+ const unsafe = unsafeNameReason(displayName);
105
+ if (unsafe) {
106
+ warnings.push(`${filename}: group ${JSON.stringify(displayName)} ${unsafe} — skipped`);
107
+ continue;
108
+ }
109
+ if (RESERVED_ROLE_NAMES.has(canonical)) {
110
+ warnings.push(`${filename}: group '${displayName}' uses reserved name '${canonical}' — skipped`);
111
+ continue;
112
+ }
113
+ if (groups.has(canonical)) {
114
+ warnings.push(`${filename}: group '${displayName}' canonicalises to '${canonical}', already declared as '${groups.get(canonical).displayName}' — skipped`);
115
+ continue;
116
+ }
117
+ if (value !== null && !Array.isArray(value)) {
118
+ warnings.push(`${filename}: group '${displayName}' must be a list of emails — skipped`);
119
+ continue;
120
+ }
121
+ const emails = new Set();
122
+ for (const raw of value ?? []) {
123
+ if (typeof raw !== 'string') {
124
+ warnings.push(`${filename}: group '${displayName}' has a non-string entry — entry skipped`);
125
+ continue;
126
+ }
127
+ const email = canonicalEmail(raw);
128
+ // The reserved `group:` prefix would PASS the email regex
129
+ // (`group:lee@x.io` shapes like an email) but it is the roles.yaml
130
+ // group-reference token, not an address — groups contain emails, never
131
+ // other groups. Skip it with its own warning so the write gate refuses
132
+ // it and the resolver never grants a colon-bearing "email".
133
+ if (email.startsWith(GROUP_REF_PREFIX)) {
134
+ warnings.push(`${filename}: group '${displayName}' has a '${GROUP_REF_PREFIX}'-prefixed entry ${JSON.stringify(raw)} — groups contain emails, not group references; entry skipped`);
135
+ continue;
136
+ }
137
+ if (!EMAIL_REGEX.test(email)) {
138
+ warnings.push(`${filename}: group '${displayName}' has malformed email '${raw}' — entry skipped`);
139
+ continue;
140
+ }
141
+ emails.add(email);
142
+ }
143
+ groups.set(canonical, { displayName: displayName.trim(), emails });
144
+ }
145
+ return { ok: true, groups, warnings };
146
+ }
147
+ /**
148
+ * Write-gate twin of {@link parseGroupsFile}: strict — structural errors AND
149
+ * entry-level warnings both refuse the candidate, so a UI write can never land
150
+ * a group the resolver would silently skip.
151
+ */
152
+ export function validateGroupsFile(text, filename) {
153
+ // Strict PARSE first (no empty-key tolerance): the read side forgives a
154
+ // blank key entry-level so other groups keep resolving, but a WRITE that
155
+ // contains one — anywhere, including outside the `groups:` mapping — must
156
+ // refuse rather than land bytes the tolerant reader silently drops.
157
+ const strict = parseYamlSubset(text.trim() ? text : 'groups:');
158
+ if (!strict.ok)
159
+ return { ok: false, errors: [`${filename}: ${strict.error}`] };
160
+ const parsed = parseGroupsFile(text, filename);
161
+ if (!parsed.ok)
162
+ return { ok: false, errors: parsed.errors };
163
+ if (parsed.warnings.length > 0)
164
+ return { ok: false, errors: parsed.warnings };
165
+ return { ok: true };
166
+ }
167
+ //# sourceMappingURL=group-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-files.js","sourceRoot":"","sources":["../../../src/modules/access/group-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import express from 'express';
2
+ import type { AuthUser } from '@bevel-software/platform-shared';
3
+ import type { IAdminAccessService } from '../admin/admin.interface.js';
4
+ import type { GroupsAdminService } from './groups-admin.service.js';
5
+ import '../auth/auth.middleware.js';
6
+ /**
7
+ * Admin surface for GROUPS — the "who you are" half of the roles/groups
8
+ * split. Manual-mode CRUD on `groups.yaml`; in IdP mode the service refuses
9
+ * mutations with a typed 409 (`kind: 'idp-mode'`) and this router simply
10
+ * relays it. Admin-gated like the other admin routers: the roster is the org
11
+ * chart, not something every signed-in user may enumerate.
12
+ */
13
+ export declare function createGroupsAdminRoutes(deps: {
14
+ groupsAdmin: GroupsAdminService;
15
+ adminAccess: IAdminAccessService;
16
+ /** Resolve the acting user for commit attribution. */
17
+ getUserById: (id: string) => Promise<AuthUser | null>;
18
+ }): express.Router;
19
+ //# sourceMappingURL=groups-admin.routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups-admin.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/access/groups-admin.routes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAGvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,4BAA4B,CAAC;AAEpC;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,WAAW,EAAE,kBAAkB,CAAC;IAChC,WAAW,EAAE,mBAAmB,CAAC;IACjC,sDAAsD;IACtD,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;CACvD,GAAG,OAAO,CAAC,MAAM,CA4FjB"}
@@ -0,0 +1,98 @@
1
+ import express from 'express';
2
+ import { canonicalRoleName } from './access-control.service.js';
3
+ import { sendError as sharedSendError, requireNonEmptyString as sharedRequireNonEmptyString } from './admin-route-helpers.js';
4
+ import '../auth/auth.middleware.js'; // Express Request augmentation
5
+ /**
6
+ * Admin surface for GROUPS — the "who you are" half of the roles/groups
7
+ * split. Manual-mode CRUD on `groups.yaml`; in IdP mode the service refuses
8
+ * mutations with a typed 409 (`kind: 'idp-mode'`) and this router simply
9
+ * relays it. Admin-gated like the other admin routers: the roster is the org
10
+ * chart, not something every signed-in user may enumerate.
11
+ */
12
+ export function createGroupsAdminRoutes(deps) {
13
+ const { groupsAdmin, adminAccess, getUserById } = deps;
14
+ const router = express.Router();
15
+ const requireAdmin = async (req, res, next) => {
16
+ if (!(await adminAccess.isAdmin(req.userEmail))) {
17
+ res.status(403).json({ error: 'Admins only' });
18
+ return;
19
+ }
20
+ next();
21
+ };
22
+ const actorOf = async (req) => {
23
+ const user = req.userId ? await getUserById(req.userId) : null;
24
+ if (user)
25
+ return user;
26
+ // The JWT authenticated but the row is gone (erased mid-session) — the
27
+ // identity claims are still the honest attribution we hold.
28
+ return { id: req.userId ?? 'unknown', email: req.userEmail ?? 'unknown', name: req.userEmail ?? 'unknown' };
29
+ };
30
+ // Shared access-family error shape + input coercion (admin-route-helpers):
31
+ // typed domain errors (incl. the roster's broken-groups 422) render
32
+ // themselves; anything else logs server-side and answers generically.
33
+ const sendError = (res, err) => sharedSendError(res, err, 'groups');
34
+ const requireNonEmptyString = (value, field) => sharedRequireNonEmptyString(value, field, { trim: true });
35
+ // GET /api/admin/groups — mode + roster (groups with grant references and
36
+ // role assignments) + `groupsHealth` (the broken-source banner marker; a
37
+ // broken MANUAL groups.yaml instead answers 422 with the parse message so
38
+ // the operator sees what to fix).
39
+ router.get('/admin/groups', requireAdmin, async (_req, res) => {
40
+ try {
41
+ res.json(await groupsAdmin.getRoster());
42
+ }
43
+ catch (err) {
44
+ sendError(res, err);
45
+ }
46
+ });
47
+ router.post('/admin/groups', requireAdmin, async (req, res) => {
48
+ try {
49
+ const displayName = requireNonEmptyString((req.body ?? {}).displayName, 'displayName');
50
+ res.json(await groupsAdmin.createGroup(await actorOf(req), displayName));
51
+ }
52
+ catch (err) {
53
+ sendError(res, err);
54
+ }
55
+ });
56
+ router.delete('/admin/groups/:canonical', requireAdmin, async (req, res) => {
57
+ try {
58
+ const canonical = canonicalRoleName(String(req.params.canonical));
59
+ res.json(await groupsAdmin.deleteGroup(await actorOf(req), canonical));
60
+ }
61
+ catch (err) {
62
+ sendError(res, err);
63
+ }
64
+ });
65
+ // PATCH /api/admin/groups/:canonical { newDisplayName } — rename;
66
+ // canonical-changing renames rewrite grant references atomically.
67
+ router.patch('/admin/groups/:canonical', requireAdmin, async (req, res) => {
68
+ try {
69
+ const canonical = canonicalRoleName(String(req.params.canonical));
70
+ const newDisplayName = requireNonEmptyString((req.body ?? {}).newDisplayName, 'newDisplayName');
71
+ res.json(await groupsAdmin.renameGroup(await actorOf(req), canonical, newDisplayName));
72
+ }
73
+ catch (err) {
74
+ sendError(res, err);
75
+ }
76
+ });
77
+ router.post('/admin/groups/:canonical/members', requireAdmin, async (req, res) => {
78
+ try {
79
+ const canonical = canonicalRoleName(String(req.params.canonical));
80
+ const email = requireNonEmptyString((req.body ?? {}).email, 'email');
81
+ res.json(await groupsAdmin.addMember(await actorOf(req), canonical, email));
82
+ }
83
+ catch (err) {
84
+ sendError(res, err);
85
+ }
86
+ });
87
+ router.delete('/admin/groups/:canonical/members/:email', requireAdmin, async (req, res) => {
88
+ try {
89
+ const canonical = canonicalRoleName(String(req.params.canonical));
90
+ res.json(await groupsAdmin.removeMember(await actorOf(req), canonical, String(req.params.email)));
91
+ }
92
+ catch (err) {
93
+ sendError(res, err);
94
+ }
95
+ });
96
+ return router;
97
+ }
98
+ //# sourceMappingURL=groups-admin.routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups-admin.routes.js","sourceRoot":"","sources":["../../../src/modules/access/groups-admin.routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Admin Groups service — CRUD on the default-branch `groups.yaml` (manual
3
+ * mode), the mode probe, and the connect-time retirement of manual groups.
4
+ *
5
+ * Shares `RolesAdminService`'s write pipeline via the SHARED
6
+ * `AdminLockedCommits` helper (one lock/commit implementation — the twin
7
+ * copies diverged once into a real bug) with the policy differences the
8
+ * roles/groups split defines:
9
+ *
10
+ * - MODE GATE: every mutation refuses in IdP mode (`synced-groups.yaml`
11
+ * exists on the default branch). Groups are managed in the identity
12
+ * provider then; a second write surface would fragment org management —
13
+ * which is the thing the mode model exists to prevent.
14
+ * - COLLISION GATE: create/rename refuse a name whose canonical form is a
15
+ * roles.yaml role. Bare-name precedence would let the group win, but a
16
+ * deliberately-created shadow of a capability role is far more likely a
17
+ * mistake than an intent — the IdP can still produce collisions, which
18
+ * precedence then resolves (group first, `role/<name>` for the role).
19
+ * - DEGRADE LOUDLY: a broken groups source never fails access resolution
20
+ * closed — groups contribute nothing and the model carries a
21
+ * `groupsHealth` marker, which the roster exposes so the Groups page can
22
+ * banner it. A broken MANUAL groups.yaml additionally makes the roster
23
+ * answer 422 with the parse message (the operator must see what to fix —
24
+ * never a dead Groups page, never a silent empty roster they might
25
+ * "repair" by re-creating groups over live bytes).
26
+ */
27
+ import type { WorkflowEventBus } from '../workflow/event-bus.js';
28
+ import type { AuthUser, IWorkspaceService, IWorkflowService } from '@bevel-software/platform-shared';
29
+ import { WorkflowDomainError } from '../workflow/workflow.errors.js';
30
+ import type { IAccessControl } from './access-control.interface.js';
31
+ import { type GroupsHealth } from './access-control.service.js';
32
+ /** A group mutation that cannot proceed. `payload.kind` distinguishes the
33
+ * mode refusal (`'idp-mode'`) for the UI. */
34
+ export declare class GroupsAdminError extends WorkflowDomainError {
35
+ constructor(message: string, status?: number, payload?: Record<string, unknown>);
36
+ }
37
+ export type GroupsMode = 'manual' | 'idp';
38
+ export interface GroupRosterEntry {
39
+ canonical: string;
40
+ displayName: string;
41
+ members: string[];
42
+ /** Every access rule referencing this group — same scan the rename uses. */
43
+ referencedBy: {
44
+ path: string;
45
+ verb: string;
46
+ }[];
47
+ /**
48
+ * Canonical names of roles carrying a `group:<canonical>` assignment to
49
+ * this group — so the delete confirm can warn that those roles will lose
50
+ * the members they inherit through it (deleteGroup unassigns them
51
+ * atomically).
52
+ */
53
+ assignedToRoles: string[];
54
+ }
55
+ export interface GroupsRoster {
56
+ mode: GroupsMode;
57
+ groups: GroupRosterEntry[];
58
+ /**
59
+ * Health of the active groups source — `ok: false` when the source exists
60
+ * but is unreadable/unparseable (resolution degrades: groups contribute
61
+ * nothing). Shipped for the Groups page banner (frontend increment).
62
+ */
63
+ groupsHealth: GroupsHealth;
64
+ }
65
+ export declare class GroupsAdminService {
66
+ private readonly workspaceService;
67
+ private readonly workflowService;
68
+ private readonly accessControl;
69
+ private readonly kbDirName;
70
+ /** Live-binding thunk — see RolesAdminService's identical note. */
71
+ private readonly defaultBranchOf;
72
+ private readonly eventBus?;
73
+ /** Shared lock/commit plumbing — see module doc. */
74
+ private readonly locked;
75
+ /** Shared cached reference scanner. */
76
+ private readonly references;
77
+ constructor(workspaceService: IWorkspaceService, workflowService: IWorkflowService, accessControl: IAccessControl, kbDirName: string,
78
+ /** Live-binding thunk — see RolesAdminService's identical note. */
79
+ defaultBranchOf: () => string, eventBus?: WorkflowEventBus | undefined);
80
+ private get defaultBranch();
81
+ private get workspaceId();
82
+ private ensureWorkspace;
83
+ /**
84
+ * IdP mode iff `synced-groups.yaml` exists — same rule the resolver applies.
85
+ *
86
+ * KNOWN GAP, deliberate: this cannot see the connect WINDOW — a directory
87
+ * connection that is configured but whose first provisioning push has not
88
+ * landed the synced file yet. Core's `SyncedGroupsSource` seam exposes only
89
+ * `listGroups()` (the overlay owns the connection), so "connected but not
90
+ * yet materialized" is not cheaply knowable server-side; the UI keeps its
91
+ * own suppression for that window.
92
+ */
93
+ getMode(): Promise<GroupsMode>;
94
+ /**
95
+ * Mode + roster + source health. Reads the ACTIVE source through the same
96
+ * `loadActiveGroups` rule the resolver applies, so mode, health, and the
97
+ * group set can never disagree with resolution:
98
+ *
99
+ * - IdP mode (synced file present, even broken): the roster IS the synced
100
+ * file — read-only in the UI; the manual file is retired and showing it
101
+ * would misreport who has access. A broken synced file yields an EMPTY
102
+ * roster with the `groupsHealth` marker (mode stays 'idp' — no
103
+ * fallback to manual, which would resurrect retired groups).
104
+ * - Manual mode, broken groups.yaml: 422 with the parse message — the
105
+ * repair path must show the operator what to fix, never a dead page.
106
+ */
107
+ getRoster(): Promise<GroupsRoster>;
108
+ /**
109
+ * Display names from the ACTIVE group source (synced in IdP mode, manual
110
+ * otherwise). Malformed files degrade to an empty list, matching the
111
+ * resolver's degrade rule.
112
+ */
113
+ listActiveGroupNames(): Promise<string[]>;
114
+ /** Manual group display names — what the connect-time warning dialog lists. */
115
+ listManualGroupNames(): Promise<string[]>;
116
+ createGroup(actor: AuthUser, displayName: string): Promise<GroupsRoster>;
117
+ /**
118
+ * Delete a group AND unassign it from every role — the `group:<canonical>`
119
+ * members in roles.yaml — in the SAME atomic locked commit (mirror of the
120
+ * rename's ref rewrite): a delete that left the refs would silently shrink
121
+ * each assigned role's membership behind a mere log warning. Grant
122
+ * references in access.md are NOT touched — a dangling grant resolves to
123
+ * nothing by design (and the roster's `referencedBy` fed the confirm
124
+ * dialog's warning).
125
+ */
126
+ deleteGroup(actor: AuthUser, canonical: string): Promise<GroupsRoster>;
127
+ addMember(actor: AuthUser, canonical: string, email: string): Promise<GroupsRoster>;
128
+ removeMember(actor: AuthUser, canonical: string, email: string): Promise<GroupsRoster>;
129
+ /**
130
+ * Rename — canonical-changing renames rewrite every grant reference in ONE
131
+ * atomic commit (same machinery, same reasoning as ref unassignment on
132
+ * delete: a partial rewrite is a silent access drop).
133
+ */
134
+ renameGroup(actor: AuthUser, canonical: string, newDisplayName: string): Promise<GroupsRoster>;
135
+ private renameGroupLocked;
136
+ /**
137
+ * Connect-time retirement: delete `groups.yaml` in one commit. Called by the
138
+ * SCIM connect flow AFTER the admin confirmed the warning dialog ("groups
139
+ * you don't recreate in the IdP will be lost"). Recovery is a git revert —
140
+ * the file's history keeps every retired group. No-op when the file is
141
+ * already absent.
142
+ */
143
+ retireManualGroups(actor: AuthUser): Promise<boolean>;
144
+ private guardEdit;
145
+ private assertLoadable;
146
+ private assertManualMode;
147
+ /**
148
+ * Refuse a group name that IS a role. Bare-name precedence (group-first)
149
+ * would make the group win resolution, so this is deliberate FRICTION, not
150
+ * a correctness need: an admin naming a group after a capability role is
151
+ * far more likely shadowing by accident than by intent. IdP-sourced
152
+ * collisions still happen (the sync writer doesn't consult roles.yaml) and
153
+ * precedence resolves them.
154
+ */
155
+ private assertRoleNameFree;
156
+ /**
157
+ * Which roles carry a `group:<canonical>` assignment, per group canonical.
158
+ * Advisory (feeds the roster's `assignedToRoles` warning field): a broken
159
+ * roles.yaml yields an empty map rather than failing the roster.
160
+ */
161
+ private rolesReferencingGroups;
162
+ private runEdit;
163
+ private runEditLocked;
164
+ private afterWrite;
165
+ }
166
+ //# sourceMappingURL=groups-admin.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups-admin.service.d.ts","sourceRoot":"","sources":["../../../src/modules/access/groups-admin.service.ts"],"names":[],"mappings":"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"}