@bevel-software/platform-core-backend 0.10.0 → 0.11.1
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.
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +12 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +34 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +54 -28
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +129 -14
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +435 -66
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts.map +1 -1
- package/dist/modules/access/access-declarations.js +5 -3
- package/dist/modules/access/access-declarations.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +39 -6
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +78 -18
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.d.ts +31 -4
- package/dist/modules/access/access-splice.d.ts.map +1 -1
- package/dist/modules/access/access-splice.js +40 -16
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +204 -82
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/admin-locked-commit.d.ts +134 -0
- package/dist/modules/access/admin-locked-commit.d.ts.map +1 -0
- package/dist/modules/access/admin-locked-commit.js +277 -0
- package/dist/modules/access/admin-locked-commit.js.map +1 -0
- package/dist/modules/access/admin-route-helpers.d.ts +32 -0
- package/dist/modules/access/admin-route-helpers.d.ts.map +1 -0
- package/dist/modules/access/admin-route-helpers.js +44 -0
- package/dist/modules/access/admin-route-helpers.js.map +1 -0
- package/dist/modules/access/capability-registry.d.ts +41 -0
- package/dist/modules/access/capability-registry.d.ts.map +1 -0
- package/dist/modules/access/capability-registry.js +46 -0
- package/dist/modules/access/capability-registry.js.map +1 -0
- package/dist/modules/access/directory-sync-bot.d.ts +13 -0
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -0
- package/dist/modules/access/directory-sync-bot.js +64 -0
- package/dist/modules/access/directory-sync-bot.js.map +1 -0
- package/dist/modules/access/group-files.d.ts +83 -0
- package/dist/modules/access/group-files.d.ts.map +1 -0
- package/dist/modules/access/group-files.js +167 -0
- package/dist/modules/access/group-files.js.map +1 -0
- package/dist/modules/access/groups-admin.routes.d.ts +19 -0
- package/dist/modules/access/groups-admin.routes.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.routes.js +98 -0
- package/dist/modules/access/groups-admin.routes.js.map +1 -0
- package/dist/modules/access/groups-admin.service.d.ts +166 -0
- package/dist/modules/access/groups-admin.service.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.service.js +442 -0
- package/dist/modules/access/groups-admin.service.js.map +1 -0
- package/dist/modules/access/groups-edit.d.ts +58 -0
- package/dist/modules/access/groups-edit.d.ts.map +1 -0
- package/dist/modules/access/groups-edit.js +162 -0
- package/dist/modules/access/groups-edit.js.map +1 -0
- package/dist/modules/access/reference-scan.d.ts +141 -0
- package/dist/modules/access/reference-scan.d.ts.map +1 -0
- package/dist/modules/access/reference-scan.js +440 -0
- package/dist/modules/access/reference-scan.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +88 -119
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +230 -384
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access/roles-edit.d.ts +51 -25
- package/dist/modules/access/roles-edit.d.ts.map +1 -1
- package/dist/modules/access/roles-edit.js +133 -59
- package/dist/modules/access/roles-edit.js.map +1 -1
- package/dist/modules/access/synced-groups-committer.d.ts +28 -0
- package/dist/modules/access/synced-groups-committer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-committer.js +139 -0
- package/dist/modules/access/synced-groups-committer.js.map +1 -0
- package/dist/modules/access/synced-groups-writer.d.ts +78 -0
- package/dist/modules/access/synced-groups-writer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-writer.js +219 -0
- package/dist/modules/access/synced-groups-writer.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +17 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +9 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +13 -2
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +61 -2
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +9 -3
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +126 -2
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +14 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +6 -1
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +11 -1
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +15 -1
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +34 -11
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +165 -37
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/locking-filesystem.d.ts +4 -0
- package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/workflow/locking-filesystem.js +181 -28
- package/dist/modules/workflow/locking-filesystem.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +10 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +19 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +17 -2
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +113 -19
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/kb-template/AGENTS.md +4 -1
- package/migrations/0004_file_lock_mode.sql +2 -0
- package/migrations/meta/0004_snapshot.json +1564 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -3
- package/src/core/create-core-server.ts +13 -0
- package/src/core/create-core-services.ts +68 -0
- package/src/index.ts +10 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +9 -3
- package/src/modules/access/__tests__/access-groups.test.ts +427 -0
- package/src/modules/access/__tests__/access-mutation.service.test.ts +171 -5
- package/src/modules/access/__tests__/access-splice.test.ts +65 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +337 -0
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +48 -0
- package/src/modules/access/__tests__/admin-locked-commit.test.ts +221 -0
- package/src/modules/access/__tests__/admin-route-helpers.test.ts +61 -0
- package/src/modules/access/__tests__/directory-sync-bot.test.ts +106 -0
- package/src/modules/access/__tests__/grant-sources.test.ts +67 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +440 -0
- package/src/modules/access/__tests__/reference-scan.test.ts +288 -0
- package/src/modules/access/__tests__/roles-admin.service.test.ts +161 -83
- package/src/modules/access/__tests__/roles-capabilities.test.ts +325 -0
- package/src/modules/access/__tests__/roles-edit.test.ts +104 -28
- package/src/modules/access/__tests__/roles.routes.test.ts +63 -40
- package/src/modules/access/__tests__/synced-groups-committer.test.ts +238 -0
- package/src/modules/access/__tests__/synced-groups-writer.test.ts +249 -0
- package/src/modules/access/access-control.interface.ts +66 -32
- package/src/modules/access/access-control.service.ts +536 -73
- package/src/modules/access/access-declarations.ts +5 -2
- package/src/modules/access/access-mutation.service.ts +88 -14
- package/src/modules/access/access-splice.ts +55 -17
- package/src/modules/access/access.routes.ts +227 -93
- package/src/modules/access/admin-locked-commit.ts +331 -0
- package/src/modules/access/admin-route-helpers.ts +55 -0
- package/src/modules/access/capability-registry.ts +74 -0
- package/src/modules/access/directory-sync-bot.ts +76 -0
- package/src/modules/access/group-files.ts +212 -0
- package/src/modules/access/groups-admin.routes.ts +113 -0
- package/src/modules/access/groups-admin.service.ts +551 -0
- package/src/modules/access/groups-edit.ts +187 -0
- package/src/modules/access/reference-scan.ts +513 -0
- package/src/modules/access/roles-admin.service.ts +290 -418
- package/src/modules/access/roles-edit.ts +134 -61
- package/src/modules/access/synced-groups-committer.ts +177 -0
- package/src/modules/access/synced-groups-writer.ts +303 -0
- package/src/modules/database/core-schema.ts +9 -0
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +116 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +6 -1
- package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +301 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +61 -1
- package/src/modules/mcp/mcp.routes.ts +137 -2
- package/src/modules/mcp/mcp.service.ts +6 -1
- package/src/modules/workflow/__tests__/locking-filesystem.test.ts +336 -0
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +32 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +139 -1
- package/src/modules/workflow/file-lock.service.ts +15 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +132 -0
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +0 -1
- package/src/modules/workflow/git/git.service.ts +174 -35
- package/src/modules/workflow/locking-filesystem.ts +188 -26
- package/src/modules/workflow/pending-commits.service.ts +27 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +0 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +0 -1
- 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
|
+
}
|