@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
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Pure parse → edit → re-emit editor for `groups.yaml` — the manual-mode
3
+ * group file. Mirrors `roles-edit.ts` (same re-emit rationale: no body, no
4
+ * comments to preserve, deterministic canonical output), with two deliberate
5
+ * differences:
6
+ *
7
+ * - No Admin invariants — groups carry no capabilities, so there is no
8
+ * lockout class of mistake here.
9
+ * - Members are EMAILS ONLY. `group:` references are a roles.yaml grammar
10
+ * (a role assigned to a group); a group containing a group is nesting,
11
+ * which is a deliberate non-goal.
12
+ *
13
+ * Policy that needs more than this file's text — IdP-mode gating, collision
14
+ * with role names — lives in `groups-admin.service.ts`.
15
+ */
16
+
17
+ import {
18
+ canonicalRoleName,
19
+ canonicalEmail,
20
+ EMAIL_REGEX,
21
+ GROUP_REF_PREFIX,
22
+ RESERVED_ROLE_NAMES,
23
+ } from './access-control.service.js';
24
+ import { GROUPS_YAML, parseGroupsFile, unsafeNameReason } from './group-files.js';
25
+
26
+ /** Bad-input failure when editing groups.yaml. */
27
+ export class GroupsEditError extends Error {
28
+ readonly status: number;
29
+ constructor(message: string, status = 422) {
30
+ super(message);
31
+ this.name = 'GroupsEditError';
32
+ this.status = status;
33
+ }
34
+ }
35
+
36
+ export interface GroupModel {
37
+ displayName: string;
38
+ /** canonicalised, de-duplicated, in first-seen order */
39
+ members: string[];
40
+ }
41
+
42
+ export type GroupsModel = GroupModel[];
43
+
44
+ export interface GroupsEditResult {
45
+ text: string;
46
+ changed: boolean;
47
+ }
48
+
49
+ /** Thin assert wrapper over the SHARED name-safety predicate (`group-files.ts`
50
+ * `unsafeNameReason`) — one namespace, one set of rules, including the
51
+ * reserved `role/` prefix. */
52
+ export function assertSafeGroupDisplayName(displayName: string): void {
53
+ const reason = unsafeNameReason(displayName);
54
+ if (reason) {
55
+ throw new GroupsEditError(`group name ${reason}: ${JSON.stringify(displayName)}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Parse groups.yaml into the ordered model; `[]` for an empty/missing file.
61
+ *
62
+ * DERIVED from the resolver's own `parseGroupsFile` — one parser for the
63
+ * grammar, so the editor's skip rules (empty/reserved/duplicate names,
64
+ * malformed member emails) can never drift from the resolver's: the admin
65
+ * roster and the edit surface must never show an entry the resolver ignores,
66
+ * or an admin "manages" a group that grants nothing. Consequence, deliberate:
67
+ * the next edit's re-emit garbage-collects those dead entries from the file.
68
+ * Structural failures throw GroupsEditError (422) — the caller reads under
69
+ * the lock, so a broken on-disk file is an operator problem surfaced loudly,
70
+ * not a silent reset.
71
+ */
72
+ export function parseGroupsModel(text: string): GroupsModel {
73
+ if (!text.trim()) return [];
74
+ const parsed = parseGroupsFile(text, GROUPS_YAML);
75
+ if (!parsed.ok) throw new GroupsEditError(parsed.errors.join('; '));
76
+ return [...parsed.groups.values()].map((def) => ({
77
+ displayName: def.displayName,
78
+ members: [...def.emails],
79
+ }));
80
+ }
81
+
82
+ /** Deterministic canonical emit; empty groups as `Name: []`. */
83
+ export function emitGroupsModel(model: GroupsModel): string {
84
+ const lines: string[] = ['groups:'];
85
+ for (const group of model) {
86
+ if (group.members.length === 0) {
87
+ lines.push(` ${group.displayName}: []`);
88
+ } else {
89
+ lines.push(` ${group.displayName}:`);
90
+ for (const email of group.members) lines.push(` - ${email}`);
91
+ }
92
+ }
93
+ return lines.join('\n') + '\n';
94
+ }
95
+
96
+ function findGroup(model: GroupsModel, canonical: string): GroupModel | undefined {
97
+ return model.find((g) => canonicalRoleName(g.displayName) === canonical);
98
+ }
99
+
100
+ function reemit(original: string, model: GroupsModel): GroupsEditResult {
101
+ const text = emitGroupsModel(model);
102
+ let originalCanonical: string;
103
+ try {
104
+ originalCanonical = emitGroupsModel(parseGroupsModel(original));
105
+ } catch {
106
+ originalCanonical = original;
107
+ }
108
+ return { text, changed: text !== originalCanonical };
109
+ }
110
+
111
+ export function createGroup(text: string, displayName: string): GroupsEditResult {
112
+ assertSafeGroupDisplayName(displayName);
113
+ const canonical = canonicalRoleName(displayName);
114
+ if (RESERVED_ROLE_NAMES.has(canonical)) {
115
+ throw new GroupsEditError(`'${displayName.trim()}' is a reserved name and cannot be a group`);
116
+ }
117
+ const model = parseGroupsModel(text);
118
+ if (findGroup(model, canonical)) {
119
+ throw new GroupsEditError(`a group named '${displayName.trim()}' already exists`);
120
+ }
121
+ model.push({ displayName: displayName.trim(), members: [] });
122
+ return reemit(text, model);
123
+ }
124
+
125
+ export function deleteGroup(text: string, canonical: string): GroupsEditResult {
126
+ const model = parseGroupsModel(text);
127
+ const idx = model.findIndex((g) => canonicalRoleName(g.displayName) === canonical);
128
+ if (idx < 0) throw new GroupsEditError(`group not found: ${canonical}`, 404);
129
+ model.splice(idx, 1);
130
+ return reemit(text, model);
131
+ }
132
+
133
+ export function addGroupMember(text: string, canonical: string, rawEmail: string): GroupsEditResult {
134
+ const email = canonicalEmail(rawEmail);
135
+ // A `group:`-prefixed value is roles.yaml grammar (a role assigned to a
136
+ // group) — and `group:lee@x.io` would pass the email regex, landing a dead
137
+ // entry the resolver reads as a plain (never-matching) address. Mirrors
138
+ // `roles-edit.ts` addMember; groups do not nest, so there is no "right
139
+ // editor" to route to — a group member is always an email.
140
+ if (email.startsWith(GROUP_REF_PREFIX)) {
141
+ throw new GroupsEditError(
142
+ `member ${JSON.stringify(rawEmail)} starts with '${GROUP_REF_PREFIX}' — a group member is always a person's email (groups cannot contain other groups). Drop the '${GROUP_REF_PREFIX}' prefix and add the email itself`,
143
+ );
144
+ }
145
+ if (!EMAIL_REGEX.test(email)) {
146
+ throw new GroupsEditError(`malformed email: ${JSON.stringify(rawEmail)}`);
147
+ }
148
+ const model = parseGroupsModel(text);
149
+ const group = findGroup(model, canonical);
150
+ if (!group) throw new GroupsEditError(`group not found: ${canonical}`, 404);
151
+ if (group.members.includes(email)) return { text: emitGroupsModel(model), changed: false };
152
+ group.members.push(email);
153
+ return reemit(text, model);
154
+ }
155
+
156
+ export function removeGroupMember(text: string, canonical: string, rawEmail: string): GroupsEditResult {
157
+ const email = canonicalEmail(rawEmail);
158
+ const model = parseGroupsModel(text);
159
+ const group = findGroup(model, canonical);
160
+ if (!group) throw new GroupsEditError(`group not found: ${canonical}`, 404);
161
+ const idx = group.members.indexOf(email);
162
+ if (idx < 0) return { text: emitGroupsModel(model), changed: false };
163
+ group.members.splice(idx, 1);
164
+ return reemit(text, model);
165
+ }
166
+
167
+ /** Rename a group's display name (canonical may change — service layer owns
168
+ * the reference rewrite when it does). */
169
+ export function renameGroupDisplay(
170
+ text: string,
171
+ canonical: string,
172
+ newDisplayName: string,
173
+ ): GroupsEditResult {
174
+ assertSafeGroupDisplayName(newDisplayName);
175
+ const newCanonical = canonicalRoleName(newDisplayName);
176
+ if (RESERVED_ROLE_NAMES.has(newCanonical)) {
177
+ throw new GroupsEditError(`'${newDisplayName.trim()}' is a reserved name and cannot be a group`);
178
+ }
179
+ const model = parseGroupsModel(text);
180
+ const group = findGroup(model, canonical);
181
+ if (!group) throw new GroupsEditError(`group not found: ${canonical}`, 404);
182
+ if (newCanonical !== canonical && findGroup(model, newCanonical)) {
183
+ throw new GroupsEditError(`a group named '${newDisplayName.trim()}' already exists`);
184
+ }
185
+ group.displayName = newDisplayName.trim();
186
+ return reemit(text, model);
187
+ }