@bevel-software/platform-core-backend 0.4.1 → 0.5.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.
- package/dist/core/core-ports.d.ts +15 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +40 -6
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +17 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +153 -29
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts +70 -21
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +134 -58
- package/dist/core-config.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +6 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +74 -5
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +116 -15
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +93 -0
- package/dist/modules/access/access-declarations.d.ts.map +1 -0
- package/dist/modules/access/access-declarations.js +168 -0
- package/dist/modules/access/access-declarations.js.map +1 -0
- package/dist/modules/access/access-mutation.service.js +4 -4
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.d.ts +16 -1
- package/dist/modules/access/access-splice.d.ts.map +1 -1
- package/dist/modules/access/access-splice.js +40 -21
- 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 +51 -0
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +8 -0
- package/dist/modules/access/creator-access.d.ts.map +1 -1
- package/dist/modules/access/creator-access.js +21 -2
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/admin/admin-access.service.d.ts +1 -1
- package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
- package/dist/modules/admin/admin-access.service.js +3 -2
- package/dist/modules/admin/admin-access.service.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +50 -1
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +39 -10
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +38 -16
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +124 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +45 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/groups/group-provision.service.d.ts +130 -0
- package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
- package/dist/modules/groups/group-provision.service.js +288 -0
- package/dist/modules/groups/group-provision.service.js.map +1 -0
- package/dist/modules/groups/groups.contract.d.ts +106 -0
- package/dist/modules/groups/groups.contract.d.ts.map +1 -0
- package/dist/modules/groups/groups.contract.js +36 -0
- package/dist/modules/groups/groups.contract.js.map +1 -0
- package/dist/modules/groups/groups.routes.d.ts +42 -0
- package/dist/modules/groups/groups.routes.d.ts.map +1 -0
- package/dist/modules/groups/groups.routes.js +379 -0
- package/dist/modules/groups/groups.routes.js.map +1 -0
- package/dist/modules/groups/groups.service.d.ts +60 -0
- package/dist/modules/groups/groups.service.d.ts.map +1 -0
- package/dist/modules/groups/groups.service.js +172 -0
- package/dist/modules/groups/groups.service.js.map +1 -0
- package/dist/modules/groups/index.d.ts +7 -0
- package/dist/modules/groups/index.d.ts.map +1 -0
- package/dist/modules/groups/index.js +6 -0
- package/dist/modules/groups/index.js.map +1 -0
- package/dist/modules/groups/join-proposals.d.ts +53 -0
- package/dist/modules/groups/join-proposals.d.ts.map +1 -0
- package/dist/modules/groups/join-proposals.js +67 -0
- package/dist/modules/groups/join-proposals.js.map +1 -0
- package/dist/modules/groups/join-requests.service.d.ts +81 -0
- package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
- package/dist/modules/groups/join-requests.service.js +135 -0
- package/dist/modules/groups/join-requests.service.js.map +1 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
- package/dist/modules/settings/deployment-settings.service.js +368 -0
- package/dist/modules/settings/deployment-settings.service.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +49 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -0
- package/dist/modules/settings/setup.routes.js +292 -0
- package/dist/modules/settings/setup.routes.js.map +1 -0
- package/dist/modules/skills/index.d.ts +2 -1
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts +40 -0
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
- package/dist/modules/skills/pending-skills.service.js +147 -0
- package/dist/modules/skills/pending-skills.service.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +32 -3
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.routes.d.ts +3 -2
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +18 -1
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +22 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +24 -12
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +1 -1
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/event-bus.d.ts +11 -0
- package/dist/modules/workflow/event-bus.d.ts.map +1 -1
- package/dist/modules/workflow/event-bus.js +22 -0
- package/dist/modules/workflow/event-bus.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +9 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +16 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/branch-name.d.ts +1 -1
- package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
- package/dist/modules/workflow/git/branch-name.js +1 -1
- package/dist/modules/workflow/git/branch-name.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +31 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +144 -52
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +19 -9
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +95 -14
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +92 -3
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +381 -108
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
- package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
- package/dist/modules/workspace/bevel-ignore.js +5 -1
- package/dist/modules/workspace/bevel-ignore.js.map +1 -1
- package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +228 -51
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts +2 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +51 -18
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +16 -4
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +36 -9
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +8 -3
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/kb-layout.test.js +2 -2
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +18 -0
- package/dist/test-setup.js.map +1 -0
- package/kb-template/.bevelignore +6 -2
- package/kb-template/AGENTS.md +272 -0
- package/kb-template/KnowledgeBase/How to get started.md +65 -0
- package/kb-template/access.md +36 -36
- package/migrations/0002_user_onboarding_done.sql +1 -0
- package/migrations/0003_deployment_settings.sql +9 -0
- package/migrations/meta/0001_snapshot.json +1486 -0
- package/migrations/meta/0002_snapshot.json +1493 -0
- package/migrations/meta/0003_snapshot.json +1552 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +3 -3
- package/src/__tests__/branch-model.test.ts +79 -0
- package/src/__tests__/core-config.admin.test.ts +114 -0
- package/src/__tests__/database-url.test.ts +70 -0
- package/src/core/core-ports.ts +15 -0
- package/src/core/create-core-server.ts +55 -6
- package/src/core/create-core-services.ts +201 -36
- package/src/core-config.ts +139 -59
- package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
- package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
- package/src/modules/access/__tests__/access-splice.test.ts +21 -0
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
- package/src/modules/access/access-control.interface.ts +11 -0
- package/src/modules/access/access-control.service.ts +113 -15
- package/src/modules/access/access-declarations.ts +251 -0
- package/src/modules/access/access-mutation.service.ts +4 -4
- package/src/modules/access/access-splice.ts +64 -21
- package/src/modules/access/access.routes.ts +61 -0
- package/src/modules/access/creator-access.ts +22 -2
- package/src/modules/admin/admin-access.service.ts +4 -3
- package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
- package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
- package/src/modules/auth/auth.routes.ts +50 -1
- package/src/modules/auth/auth.service.ts +55 -17
- package/src/modules/database/core-schema.ts +46 -0
- package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
- package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
- package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
- package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
- package/src/modules/groups/group-provision.service.ts +328 -0
- package/src/modules/groups/groups.contract.ts +107 -0
- package/src/modules/groups/groups.routes.ts +433 -0
- package/src/modules/groups/groups.service.ts +193 -0
- package/src/modules/groups/index.ts +17 -0
- package/src/modules/groups/join-proposals.ts +114 -0
- package/src/modules/groups/join-requests.service.ts +185 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
- package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
- package/src/modules/settings/deployment-settings.service.ts +419 -0
- package/src/modules/settings/setup.routes.ts +317 -0
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/index.ts +3 -0
- package/src/modules/skills/pending-skills.service.ts +168 -0
- package/src/modules/skills/skills.contract.ts +34 -3
- package/src/modules/skills/skills.routes.ts +23 -2
- package/src/modules/skills/skills.service.ts +24 -16
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
- package/src/modules/tool-manuals/index.ts +2 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
- package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
- package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
- package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +251 -5
- package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
- package/src/modules/workflow/event-bus.ts +25 -0
- package/src/modules/workflow/file-lock.service.ts +17 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
- package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
- package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +5 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
- package/src/modules/workflow/git/branch-name.ts +5 -1
- package/src/modules/workflow/git/git.service.ts +159 -62
- package/src/modules/workflow/git/pull-request.service.ts +22 -11
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow.routes.ts +91 -13
- package/src/modules/workflow/workflow.service.ts +353 -13
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
- package/src/modules/workspace/bevel-ignore.ts +66 -62
- package/src/modules/workspace/kb-seed.interface.ts +1 -1
- package/src/modules/workspace/kb-seed.service.ts +247 -53
- package/src/modules/workspace/workspace.routes.ts +54 -18
- package/src/modules/workspace/workspace.service.ts +45 -7
- package/src/modules/workspace/workspace.tools.ts +922 -917
- package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
- package/src/shared/__tests__/join-request.test.ts +96 -0
- package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +2 -2
- package/src/test-setup.ts +18 -0
- package/kb-template/Agents/README.md +0 -38
- package/kb-template/CLAUDE.md +0 -419
- package/kb-template/Data/.gitkeep +0 -0
- package/kb-template/Data/README.md +0 -39
- package/kb-template/KnowledgeBase/README.md +0 -37
- package/kb-template/Pipelines/.gitkeep +0 -0
- package/kb-template/Pipelines/README.md +0 -38
- package/kb-template/Skills/.gitkeep +0 -0
- package/kb-template/Skills/README.md +0 -32
- package/kb-template/Tools/.gitkeep +0 -0
- package/kb-template/Tools/README.md +0 -42
- /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
|
@@ -14,6 +14,7 @@ describe('computeViewerCanCancel', () => {
|
|
|
14
14
|
authorId: AUTHOR_HASH,
|
|
15
15
|
viewerEmail: undefined,
|
|
16
16
|
viewerCanBypassMerge: false,
|
|
17
|
+
viewerWritesAllFiles: false,
|
|
17
18
|
}),
|
|
18
19
|
).toBe(false);
|
|
19
20
|
});
|
|
@@ -25,6 +26,7 @@ describe('computeViewerCanCancel', () => {
|
|
|
25
26
|
authorId: AUTHOR_HASH,
|
|
26
27
|
viewerEmail: EMAIL,
|
|
27
28
|
viewerCanBypassMerge: false,
|
|
29
|
+
viewerWritesAllFiles: false,
|
|
28
30
|
}),
|
|
29
31
|
).toBe(true);
|
|
30
32
|
});
|
|
@@ -36,6 +38,7 @@ describe('computeViewerCanCancel', () => {
|
|
|
36
38
|
authorId: OTHER_AUTHOR_HASH,
|
|
37
39
|
viewerEmail: EMAIL,
|
|
38
40
|
viewerCanBypassMerge: true,
|
|
41
|
+
viewerWritesAllFiles: false,
|
|
39
42
|
}),
|
|
40
43
|
).toBe(true);
|
|
41
44
|
});
|
|
@@ -47,6 +50,7 @@ describe('computeViewerCanCancel', () => {
|
|
|
47
50
|
authorId: AUTHOR_HASH,
|
|
48
51
|
viewerEmail: EMAIL,
|
|
49
52
|
viewerCanBypassMerge: true,
|
|
53
|
+
viewerWritesAllFiles: false,
|
|
50
54
|
}),
|
|
51
55
|
).toBe(true);
|
|
52
56
|
});
|
|
@@ -58,6 +62,7 @@ describe('computeViewerCanCancel', () => {
|
|
|
58
62
|
authorId: AUTHOR_HASH,
|
|
59
63
|
viewerEmail: EMAIL,
|
|
60
64
|
viewerCanBypassMerge: false,
|
|
65
|
+
viewerWritesAllFiles: false,
|
|
61
66
|
}),
|
|
62
67
|
).toBe(false);
|
|
63
68
|
});
|
|
@@ -69,6 +74,7 @@ describe('computeViewerCanCancel', () => {
|
|
|
69
74
|
authorId: OTHER_AUTHOR_HASH,
|
|
70
75
|
viewerEmail: EMAIL,
|
|
71
76
|
viewerCanBypassMerge: true,
|
|
77
|
+
viewerWritesAllFiles: false,
|
|
72
78
|
}),
|
|
73
79
|
).toBe(false);
|
|
74
80
|
});
|
|
@@ -80,6 +86,7 @@ describe('computeViewerCanCancel', () => {
|
|
|
80
86
|
authorId: undefined,
|
|
81
87
|
viewerEmail: EMAIL,
|
|
82
88
|
viewerCanBypassMerge: false,
|
|
89
|
+
viewerWritesAllFiles: false,
|
|
83
90
|
}),
|
|
84
91
|
).toBe(false);
|
|
85
92
|
});
|
|
@@ -95,7 +102,44 @@ describe('computeViewerCanCancel', () => {
|
|
|
95
102
|
authorId: AUTHOR_HASH,
|
|
96
103
|
viewerEmail: ' Juan@Bevel.Software ',
|
|
97
104
|
viewerCanBypassMerge: false,
|
|
105
|
+
viewerWritesAllFiles: false,
|
|
98
106
|
}),
|
|
99
107
|
).toBe(true);
|
|
100
108
|
});
|
|
109
|
+
|
|
110
|
+
it('returns true for a non-author, non-admin who writes EVERY changed file (the reject route’s third grant)', () => {
|
|
111
|
+
expect(
|
|
112
|
+
computeViewerCanCancel({
|
|
113
|
+
state: 'open',
|
|
114
|
+
authorId: OTHER_AUTHOR_HASH,
|
|
115
|
+
viewerEmail: EMAIL,
|
|
116
|
+
viewerCanBypassMerge: false,
|
|
117
|
+
viewerWritesAllFiles: true,
|
|
118
|
+
}),
|
|
119
|
+
).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('fails closed for an anonymous viewer even with the owner grant', () => {
|
|
123
|
+
expect(
|
|
124
|
+
computeViewerCanCancel({
|
|
125
|
+
state: 'open',
|
|
126
|
+
authorId: OTHER_AUTHOR_HASH,
|
|
127
|
+
viewerEmail: undefined,
|
|
128
|
+
viewerCanBypassMerge: false,
|
|
129
|
+
viewerWritesAllFiles: true,
|
|
130
|
+
}),
|
|
131
|
+
).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('terminal states refuse the owner grant too', () => {
|
|
135
|
+
expect(
|
|
136
|
+
computeViewerCanCancel({
|
|
137
|
+
state: 'merged',
|
|
138
|
+
authorId: OTHER_AUTHOR_HASH,
|
|
139
|
+
viewerEmail: EMAIL,
|
|
140
|
+
viewerCanBypassMerge: false,
|
|
141
|
+
viewerWritesAllFiles: true,
|
|
142
|
+
}),
|
|
143
|
+
).toBe(false);
|
|
144
|
+
});
|
|
101
145
|
});
|
|
@@ -7,7 +7,11 @@ export { PROTECTED_BRANCHES, isProtectedBranch } from '@bevel-software/platform-
|
|
|
7
7
|
// picker's `canDelete` rule and the backend authorisation check on
|
|
8
8
|
// `deleteBranch` must agree on every edge case (uppercase email, dotted
|
|
9
9
|
// localpart, etc.).
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
branchAuthorLocalpart,
|
|
12
|
+
isBranchAuthoredBy,
|
|
13
|
+
isOwnSuggestionsBranch,
|
|
14
|
+
} from '@bevel-software/platform-shared';
|
|
11
15
|
|
|
12
16
|
const VALID_BRANCH_REGEX = /^[A-Za-z0-9][A-Za-z0-9/_\-.]*$/;
|
|
13
17
|
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
assertValidBranchName,
|
|
23
23
|
assertValidRelativePath,
|
|
24
24
|
isBranchAuthoredBy,
|
|
25
|
+
isOwnSuggestionsBranch,
|
|
25
26
|
isProtectedBranch,
|
|
26
27
|
PROTECTED_BRANCHES,
|
|
27
28
|
} from './branch-name.js';
|
|
@@ -607,7 +608,7 @@ export class GitService implements IGitService {
|
|
|
607
608
|
workspaceId: string,
|
|
608
609
|
name: string,
|
|
609
610
|
user: AuthUser,
|
|
610
|
-
opts: { onlyIfNoRemote?: boolean } = {},
|
|
611
|
+
opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean } = {},
|
|
611
612
|
): Promise<void> {
|
|
612
613
|
assertValidBranchName(name);
|
|
613
614
|
if (isProtectedBranch(name)) {
|
|
@@ -622,7 +623,12 @@ export class GitService implements IGitService {
|
|
|
622
623
|
);
|
|
623
624
|
}
|
|
624
625
|
|
|
625
|
-
if (opts.
|
|
626
|
+
if (opts.systemCleanup) {
|
|
627
|
+
// Internal post-merge retirement (never reachable from a route): the
|
|
628
|
+
// merge that emptied this branch was itself authorized, so no
|
|
629
|
+
// authorship check — the protected-branch and checked-out guards
|
|
630
|
+
// above still hold.
|
|
631
|
+
} else if (opts.onlyIfNoRemote) {
|
|
626
632
|
// Legacy orphan-cleanup path (PR merged + remote head pruned). Skip
|
|
627
633
|
// the authorship check — callers prune any orphan they encounter,
|
|
628
634
|
// not just their own — but keep the "refuse if origin still has the
|
|
@@ -642,7 +648,13 @@ export class GitService implements IGitService {
|
|
|
642
648
|
// that predicate is the existing source of truth for `Admin`-role
|
|
643
649
|
// membership inside the access model — no separate `isAdmin()` plumbing
|
|
644
650
|
// needed.
|
|
645
|
-
|
|
651
|
+
// Two authorship conventions: `<localpart>/…` UI drafts, and the
|
|
652
|
+
// `suggestions/<who>-<id8>/…` namespace the propose flow files its
|
|
653
|
+
// branches under — a user resetting THEIR OWN suggestion bundle is
|
|
654
|
+
// deleting their own branch.
|
|
655
|
+
const isAuthor =
|
|
656
|
+
isBranchAuthoredBy(name, user.email) ||
|
|
657
|
+
isOwnSuggestionsBranch(name, { email: user.email, id: user.id });
|
|
646
658
|
const isAdmin = this.accessControl
|
|
647
659
|
? await this.accessControl.canWrite(workspaceId, user.email, 'roles.yaml')
|
|
648
660
|
: false;
|
|
@@ -1017,7 +1029,11 @@ export class GitService implements IGitService {
|
|
|
1017
1029
|
});
|
|
1018
1030
|
}
|
|
1019
1031
|
|
|
1020
|
-
async push(
|
|
1032
|
+
async push(
|
|
1033
|
+
workspaceId: string,
|
|
1034
|
+
user: AuthUser,
|
|
1035
|
+
opts?: { systemAuthorized?: boolean },
|
|
1036
|
+
): Promise<void> {
|
|
1021
1037
|
return this.mutex.run(workspaceId, async () => {
|
|
1022
1038
|
const cwd = await this.repoDir(workspaceId);
|
|
1023
1039
|
const branch = await this.currentBranch(cwd);
|
|
@@ -1030,7 +1046,14 @@ export class GitService implements IGitService {
|
|
|
1030
1046
|
// PR. Gate against `origin/<branch>` — the published state — for
|
|
1031
1047
|
// protected pushes; that's defence-in-depth against local rebases or
|
|
1032
1048
|
// `commit-tree` shenanigans that might bypass the commit-time gate.
|
|
1033
|
-
|
|
1049
|
+
// `systemAuthorized` skips the gate for flows whose ENDPOINT is the
|
|
1050
|
+
// authorization — group provisioning commits an access.md into a
|
|
1051
|
+
// folder that does not exist at origin yet, which this gate can only
|
|
1052
|
+
// ever read as "write: Admin". The provisioning service has already
|
|
1053
|
+
// decided the write is legitimate (unused name, exclusive create);
|
|
1054
|
+
// gating it here again just refuses every non-admin the product
|
|
1055
|
+
// promised a group to.
|
|
1056
|
+
if (isProtectedBranch(branch) && !opts?.systemAuthorized) {
|
|
1034
1057
|
const touched = await this.unpushedTouchedPaths(cwd);
|
|
1035
1058
|
await this.assertCanWriteAtRef(
|
|
1036
1059
|
workspaceId,
|
|
@@ -1608,60 +1631,6 @@ export class GitService implements IGitService {
|
|
|
1608
1631
|
});
|
|
1609
1632
|
}
|
|
1610
1633
|
|
|
1611
|
-
async revertCommit(
|
|
1612
|
-
workspaceId: string,
|
|
1613
|
-
user: AuthUser,
|
|
1614
|
-
sha: string,
|
|
1615
|
-
): Promise<CommitAttribution> {
|
|
1616
|
-
if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
|
|
1617
|
-
throw new WorkflowValidationError('invalid commit sha');
|
|
1618
|
-
}
|
|
1619
|
-
assertValidAuthor(user);
|
|
1620
|
-
return this.mutex.run(workspaceId, async () => {
|
|
1621
|
-
const cwd = await this.repoDir(workspaceId);
|
|
1622
|
-
const branch = await this.currentBranch(cwd);
|
|
1623
|
-
|
|
1624
|
-
// Revert creates a new commit reversing `sha`. Same model as commit:
|
|
1625
|
-
// gate only on protected branches; reverts on feature/draft branches
|
|
1626
|
-
// are free (they produce a commit that would still need to merge via
|
|
1627
|
-
// PR to affect canonical state).
|
|
1628
|
-
if (isProtectedBranch(branch)) {
|
|
1629
|
-
const { stdout: diffOut } = await this.git(cwd, [
|
|
1630
|
-
'diff-tree', '--no-commit-id', '--name-only', '-r', sha,
|
|
1631
|
-
]);
|
|
1632
|
-
const touched = diffOut.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
1633
|
-
await this.assertCanWriteAtRef(workspaceId, 'HEAD', user.email, touched);
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
await this.git(cwd, [
|
|
1637
|
-
'revert',
|
|
1638
|
-
'--no-edit',
|
|
1639
|
-
`--author=${user.name} <${user.email}>`,
|
|
1640
|
-
sha,
|
|
1641
|
-
]);
|
|
1642
|
-
|
|
1643
|
-
const { stdout } = await this.git(cwd, [
|
|
1644
|
-
'log',
|
|
1645
|
-
'-1',
|
|
1646
|
-
'--pretty=format:%H%x00%an%x00%ae%x00%s%x00%aI',
|
|
1647
|
-
]);
|
|
1648
|
-
const [headSha, authorName, authorEmail, subj, committedAt] = stdout.split('\x00');
|
|
1649
|
-
|
|
1650
|
-
// A revert may have touched roles.yaml or access.md — drop cached
|
|
1651
|
-
// access state so the next gate read reflects the rolled-back tree.
|
|
1652
|
-
// Mirrors the commit / pull paths.
|
|
1653
|
-
this.accessControl?.invalidate(workspaceId);
|
|
1654
|
-
|
|
1655
|
-
return {
|
|
1656
|
-
sha: headSha?.trim() ?? '',
|
|
1657
|
-
authorName: authorName ?? '',
|
|
1658
|
-
authorEmail: authorEmail ?? '',
|
|
1659
|
-
subject: subj ?? '',
|
|
1660
|
-
committedAt: committedAt?.trim() ?? new Date().toISOString(),
|
|
1661
|
-
};
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
1634
|
async logForFile(
|
|
1666
1635
|
workspaceId: string,
|
|
1667
1636
|
relativePath: string,
|
|
@@ -1726,6 +1695,44 @@ export class GitService implements IGitService {
|
|
|
1726
1695
|
});
|
|
1727
1696
|
}
|
|
1728
1697
|
|
|
1698
|
+
/**
|
|
1699
|
+
* Full file contents on both sides of one commit: `baseline` is the file at
|
|
1700
|
+
* `<sha>^` (the parent), `current` at `<sha>`. Null means the file is absent
|
|
1701
|
+
* at that ref — added files have `baseline: null`, deleted files
|
|
1702
|
+
* `current: null`. A root commit (no parent) also yields `baseline: null`.
|
|
1703
|
+
* Feeds the rendered-markdown history view, which needs before/after text
|
|
1704
|
+
* rather than the patch `diffFileAtCommit` returns.
|
|
1705
|
+
*/
|
|
1706
|
+
async fileContentsAtCommit(
|
|
1707
|
+
workspaceId: string,
|
|
1708
|
+
relativePath: string,
|
|
1709
|
+
sha: string,
|
|
1710
|
+
): Promise<{ baseline: string | null; current: string | null }> {
|
|
1711
|
+
assertValidRelativePath(relativePath);
|
|
1712
|
+
if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
|
|
1713
|
+
throw new WorkflowValidationError('invalid commit sha');
|
|
1714
|
+
}
|
|
1715
|
+
const repoRelativePath = this.stripRepoPrefix(relativePath);
|
|
1716
|
+
return this.mutex.run(workspaceId, async () => {
|
|
1717
|
+
const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
|
|
1718
|
+
let baseline: string | null;
|
|
1719
|
+
try {
|
|
1720
|
+
baseline = await this.readFileAtRef(workspaceId, `${sha}^`, repoRelativePath);
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
// `readFileAtRef` maps "path absent at ref" to null but propagates an
|
|
1723
|
+
// unresolvable ref. `<sha>^` on a root commit is exactly that (git
|
|
1724
|
+
// reports it as "invalid object name '<sha>^'", or "unknown/bad
|
|
1725
|
+
// revision" in other codepaths), and it legitimately means "no parent
|
|
1726
|
+
// side" — fold it into null. Anything else stays fatal.
|
|
1727
|
+
const stderr =
|
|
1728
|
+
(err as { stderr?: string }).stderr ?? (err instanceof Error ? err.message : String(err));
|
|
1729
|
+
if (/invalid object name|unknown revision|bad revision/i.test(stderr)) baseline = null;
|
|
1730
|
+
else throw err;
|
|
1731
|
+
}
|
|
1732
|
+
return { baseline, current };
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1729
1736
|
async diffFileBetweenBranches(
|
|
1730
1737
|
workspaceId: string,
|
|
1731
1738
|
relativePath: string,
|
|
@@ -1859,8 +1866,20 @@ export class GitService implements IGitService {
|
|
|
1859
1866
|
this.git(cwd, ['diff', '-M', '-z', '--name-status', range]),
|
|
1860
1867
|
this.git(cwd, ['diff', '-M', '-z', '--numstat', range]),
|
|
1861
1868
|
]);
|
|
1862
|
-
|
|
1863
|
-
|
|
1869
|
+
let statuses = parseNameStatusZ(nameStatusOut);
|
|
1870
|
+
let counts = parseNumstatZ(numstatOut);
|
|
1871
|
+
// roles.yaml can NEVER change through a merge — `preserveBaseRolesYaml`
|
|
1872
|
+
// restores the base copy onto the source branch before every merge — so
|
|
1873
|
+
// listing it as "changed" claims something the merge will not do, and
|
|
1874
|
+
// (worse) makes its approval a requirement for a change that cannot
|
|
1875
|
+
// land. Filter it from the review surface entirely; the neutralisation
|
|
1876
|
+
// reads the raw refs itself and is unaffected. Both lists are filtered
|
|
1877
|
+
// IN STEP so the index-zip below stays aligned.
|
|
1878
|
+
if (statuses.some((s) => s.path === 'roles.yaml')) {
|
|
1879
|
+
const keep = statuses.map((s) => s.path !== 'roles.yaml');
|
|
1880
|
+
statuses = statuses.filter((_, i) => keep[i]);
|
|
1881
|
+
if (counts.length === keep.length) counts = counts.filter((_, i) => keep[i]);
|
|
1882
|
+
}
|
|
1864
1883
|
|
|
1865
1884
|
// `--name-status` and `--numstat` enumerate the same files in the same
|
|
1866
1885
|
// order (same `-M` over the same range), so we zip by index. If the two
|
|
@@ -1924,7 +1943,85 @@ export class GitService implements IGitService {
|
|
|
1924
1943
|
const { stdout } = await this.git(cwd, [
|
|
1925
1944
|
'diff', '-M', '--name-only', `${baseRef}...${headRef}`,
|
|
1926
1945
|
]);
|
|
1927
|
-
return
|
|
1946
|
+
return (
|
|
1947
|
+
stdout
|
|
1948
|
+
.split('\n')
|
|
1949
|
+
.map((s) => s.trim())
|
|
1950
|
+
.filter(Boolean)
|
|
1951
|
+
// Same rule as `changedFilesForPr`: a roles.yaml change never
|
|
1952
|
+
// survives a merge, so it is not a touched path for routing or
|
|
1953
|
+
// summaries either.
|
|
1954
|
+
.filter((p) => p !== 'roles.yaml')
|
|
1955
|
+
);
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* The merge-base commit of a change request's two branches (their origin
|
|
1961
|
+
* refs), or null when they share no history. This is the "before" the CR's
|
|
1962
|
+
* three-dot diff is read against — the ref a per-file revert restores from,
|
|
1963
|
+
* so the reverted file drops OUT of that same diff.
|
|
1964
|
+
*/
|
|
1965
|
+
async mergeBaseForPr(
|
|
1966
|
+
workspaceId: string,
|
|
1967
|
+
baseBranch: string,
|
|
1968
|
+
headBranch: string,
|
|
1969
|
+
): Promise<string | null> {
|
|
1970
|
+
assertValidBranchName(baseBranch);
|
|
1971
|
+
assertValidBranchName(headBranch);
|
|
1972
|
+
const cwd = await this.repoDir(workspaceId);
|
|
1973
|
+
await this.fetchPrRefs(cwd, baseBranch, headBranch);
|
|
1974
|
+
return this.mutex.run(workspaceId, async () => {
|
|
1975
|
+
const baseRef = await this.resolveBranchRef(cwd, baseBranch);
|
|
1976
|
+
const headRef = await this.resolveBranchRef(cwd, headBranch);
|
|
1977
|
+
try {
|
|
1978
|
+
const { stdout } = await this.git(cwd, ['merge-base', baseRef, headRef]);
|
|
1979
|
+
return stdout.trim() || null;
|
|
1980
|
+
} catch (err) {
|
|
1981
|
+
// Exit 1 is git's specific "no common ancestor" answer; anything else
|
|
1982
|
+
// is an infra failure that must not masquerade as it.
|
|
1983
|
+
if ((err as { exitCode?: number }).exitCode === 1) return null;
|
|
1984
|
+
throw err;
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Restore one path in the working tree (and index) to its content at `ref`;
|
|
1991
|
+
* a path ABSENT at `ref` is deleted — the revert of an added file is its
|
|
1992
|
+
* removal. Byte-exact via git itself (`checkout <ref> -- <path>`), never a
|
|
1993
|
+
* read-as-string round-trip that would mangle binary files. The caller owns
|
|
1994
|
+
* committing the result (see `commitFile`).
|
|
1995
|
+
*/
|
|
1996
|
+
async restorePathFromRef(
|
|
1997
|
+
workspaceId: string,
|
|
1998
|
+
ref: string,
|
|
1999
|
+
repoRelativePath: string,
|
|
2000
|
+
): Promise<void> {
|
|
2001
|
+
assertValidRelativePath(repoRelativePath);
|
|
2002
|
+
return this.mutex.run(workspaceId, async () => {
|
|
2003
|
+
const cwd = await this.repoDir(workspaceId);
|
|
2004
|
+
let existsAtRef = true;
|
|
2005
|
+
try {
|
|
2006
|
+
await this.git(cwd, ['cat-file', '-e', `${ref}:${repoRelativePath}`]);
|
|
2007
|
+
} catch {
|
|
2008
|
+
existsAtRef = false;
|
|
2009
|
+
}
|
|
2010
|
+
if (existsAtRef) {
|
|
2011
|
+
await this.git(cwd, ['checkout', ref, '--', repoRelativePath]);
|
|
2012
|
+
} else {
|
|
2013
|
+
// First re-align the INDEX with HEAD for this path. A previous failed
|
|
2014
|
+
// attempt (the git-rm era of this method) can have left a staged
|
|
2015
|
+
// deletion — index entry gone — and `git add` can only stage a
|
|
2016
|
+
// deletion for a path the index still tracks. No-op on a clean tree.
|
|
2017
|
+
await this.git(cwd, ['reset', '-q', 'HEAD', '--', repoRelativePath]);
|
|
2018
|
+
// Then delete from the WORKING TREE only, never `git rm`: the
|
|
2019
|
+
// follow-up `commitFile` stages via `git add -- <path>`, and dropping
|
|
2020
|
+
// the index entry too made that add match nothing — the whole revert
|
|
2021
|
+
// failed with "pathspec did not match any files". A tracked file
|
|
2022
|
+
// missing from disk is exactly what `git add` records as deleted.
|
|
2023
|
+
await fs.rm(path.join(cwd, repoRelativePath), { force: true });
|
|
2024
|
+
}
|
|
1928
2025
|
});
|
|
1929
2026
|
}
|
|
1930
2027
|
|
|
@@ -183,10 +183,13 @@ export class PullRequestService implements IPullRequestService {
|
|
|
183
183
|
return summaries;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
async listPrsAuthoredBy(
|
|
186
|
+
async listPrsAuthoredBy(
|
|
187
|
+
loginOrEmail: string,
|
|
188
|
+
opts: { fresh?: boolean } = {},
|
|
189
|
+
): Promise<PullRequestSummary[]> {
|
|
187
190
|
const needle = loginOrEmail.trim().toLowerCase();
|
|
188
191
|
if (!needle) return [];
|
|
189
|
-
const prs = await this.listOpenPrs();
|
|
192
|
+
const prs = await this.listOpenPrs(opts);
|
|
190
193
|
const needleIsEmail = needle.includes('@');
|
|
191
194
|
const needleHash = needleIsEmail ? hashEmail(needle) : null;
|
|
192
195
|
return prs.filter((p) => {
|
|
@@ -378,6 +381,11 @@ export class PullRequestService implements IPullRequestService {
|
|
|
378
381
|
authorId: summary.authorId,
|
|
379
382
|
viewerEmail: opts.viewerEmail,
|
|
380
383
|
viewerCanBypassMerge,
|
|
384
|
+
// The reject route's third grant (see `rejectChangeRequest`): write on
|
|
385
|
+
// every changed file at origin/<base>. Derived from the per-file
|
|
386
|
+
// `viewerCanApprove` flags — the SAME `canWriteBatchAtRef` predicate the
|
|
387
|
+
// route enforces — so the hint cannot drift from the enforcement.
|
|
388
|
+
viewerWritesAllFiles: approvals.length > 0 && approvals.every((a) => a.viewerCanApprove),
|
|
381
389
|
});
|
|
382
390
|
|
|
383
391
|
const detail: PullRequestDetail = {
|
|
@@ -423,23 +431,26 @@ export class PullRequestService implements IPullRequestService {
|
|
|
423
431
|
}
|
|
424
432
|
|
|
425
433
|
/**
|
|
426
|
-
* Pure predicate for the
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* `canWriteAtRef('roles.yaml')` predicate)
|
|
430
|
-
*
|
|
434
|
+
* Pure predicate for the `viewerCanCancel` hint. The viewer can cancel iff
|
|
435
|
+
* the PR is open AND they're the author (hash-match against the stored
|
|
436
|
+
* author), an admin (`viewerCanBypassMerge` is the proxy — same
|
|
437
|
+
* `canWriteAtRef('roles.yaml')` predicate), or they hold write on EVERY
|
|
438
|
+
* changed file (`viewerWritesAllFiles`) — the reject route's full
|
|
439
|
+
* authorization set, mirrored exactly so the hint never claims less (or
|
|
440
|
+
* more) than the server enforces. Fail-closed: no viewer email → false,
|
|
441
|
+
* whatever the grants say.
|
|
431
442
|
*/
|
|
432
443
|
export function computeViewerCanCancel(input: {
|
|
433
444
|
state: PullRequestState;
|
|
434
445
|
authorId: string | undefined;
|
|
435
446
|
viewerEmail: string | undefined;
|
|
436
447
|
viewerCanBypassMerge: boolean;
|
|
448
|
+
viewerWritesAllFiles: boolean;
|
|
437
449
|
}): boolean {
|
|
438
450
|
if (input.state !== 'open') return false;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return viewerIsAuthor || input.viewerCanBypassMerge;
|
|
451
|
+
if (!input.viewerEmail) return false;
|
|
452
|
+
const viewerIsAuthor = !!(input.authorId && input.authorId === hashEmail(input.viewerEmail));
|
|
453
|
+
return viewerIsAuthor || input.viewerCanBypassMerge || input.viewerWritesAllFiles;
|
|
443
454
|
}
|
|
444
455
|
|
|
445
456
|
export const __testing = {
|
|
@@ -100,7 +100,10 @@ describe('ReviewWorkflowService.cancelPr', () => {
|
|
|
100
100
|
).rejects.toMatchObject({
|
|
101
101
|
name: 'CancelAuthError',
|
|
102
102
|
status: 403,
|
|
103
|
-
|
|
103
|
+
// Names the FULL authorization set — the facade's owner grant
|
|
104
|
+
// (write on every changed file) included, so the refusal never
|
|
105
|
+
// undersells who could actually do this.
|
|
106
|
+
message: expect.stringMatching(/author, an admin, or having edit access to every file/i),
|
|
104
107
|
});
|
|
105
108
|
expect(updateReturning).not.toHaveBeenCalled();
|
|
106
109
|
});
|
|
@@ -106,7 +106,8 @@ class CancelStateError extends WorkflowDomainError {
|
|
|
106
106
|
class CancelAuthError extends WorkflowDomainError {
|
|
107
107
|
constructor() {
|
|
108
108
|
super(
|
|
109
|
-
"You can't cancel this change request —
|
|
109
|
+
"You can't cancel this change request — that takes being its author, an admin, " +
|
|
110
|
+
'or having edit access to every file it changes.',
|
|
110
111
|
403,
|
|
111
112
|
);
|
|
112
113
|
this.name = 'CancelAuthError';
|
|
@@ -500,7 +501,11 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
500
501
|
})
|
|
501
502
|
.onConflictDoNothing();
|
|
502
503
|
|
|
503
|
-
|
|
504
|
+
// WITH the caller as viewer: these approvals go straight back to the UI
|
|
505
|
+
// that just clicked, and omitting the viewer computed every
|
|
506
|
+
// `viewerCanApprove` as false — one approval made the apply button and
|
|
507
|
+
// every remaining approve control vanish until a fresh detail fetch.
|
|
508
|
+
return this.getApprovalStates(prNumber, files, headSha, baseBranch, prAuthorIdHash, workspaceId, user.email);
|
|
504
509
|
}
|
|
505
510
|
|
|
506
511
|
evaluateMergeGate(input: MergeGateInput): MergeGateResult {
|
|
@@ -801,6 +806,10 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
801
806
|
),
|
|
802
807
|
);
|
|
803
808
|
|
|
804
|
-
|
|
809
|
+
// WITH the caller as viewer: these approvals go straight back to the UI
|
|
810
|
+
// that just clicked, and omitting the viewer computed every
|
|
811
|
+
// `viewerCanApprove` as false — one approval made the apply button and
|
|
812
|
+
// every remaining approve control vanish until a fresh detail fetch.
|
|
813
|
+
return this.getApprovalStates(prNumber, files, headSha, baseBranch, prAuthorIdHash, workspaceId, user.email);
|
|
805
814
|
}
|
|
806
815
|
}
|
|
@@ -266,17 +266,6 @@ export function createWorkflowRoutes(
|
|
|
266
266
|
}
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
router.post('/workspace/:id/workflow/changes/:sha/revert', async (req, res) => {
|
|
270
|
-
const user = await requireUser(req, res);
|
|
271
|
-
if (!user) return;
|
|
272
|
-
try {
|
|
273
|
-
res.json(await workflow.revertChange(req.params.id, user, req.params.sha));
|
|
274
|
-
} catch (err) {
|
|
275
|
-
const { status, body } = toHttpError(err);
|
|
276
|
-
res.status(status).json(body);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
269
|
router.get('/workspace/:id/workflow/compare-file', async (req, res) => {
|
|
281
270
|
if (!(await requireUser(req, res))) return;
|
|
282
271
|
const pathParam = typeof req.query.path === 'string' ? req.query.path : '';
|
|
@@ -312,6 +301,23 @@ export function createWorkflowRoutes(
|
|
|
312
301
|
}
|
|
313
302
|
});
|
|
314
303
|
|
|
304
|
+
router.get('/workspace/:id/workflow/file-at-change', async (req, res) => {
|
|
305
|
+
if (!(await requireUser(req, res))) return;
|
|
306
|
+
const pathParam = typeof req.query.path === 'string' ? req.query.path : '';
|
|
307
|
+
const sha = typeof req.query.sha === 'string' ? req.query.sha : '';
|
|
308
|
+
if (!pathParam || !sha) {
|
|
309
|
+
res.status(400).json({ error: 'path and sha are required' });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const { baseline, current } = await workflow.fileAtChange(req.params.id, pathParam, sha);
|
|
314
|
+
res.json({ baseline, current });
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const { status, body } = toHttpError(err);
|
|
317
|
+
res.status(status).json(body);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
315
321
|
// ── File locks ────────────────────────────────────────────────────────────
|
|
316
322
|
|
|
317
323
|
router.post('/workspace/:id/workflow/locks', async (req, res) => {
|
|
@@ -431,8 +437,12 @@ export function createWorkflowRoutes(
|
|
|
431
437
|
router.get('/workflow/change-requests/mine', async (req, res) => {
|
|
432
438
|
const user = await requireUser(req, res);
|
|
433
439
|
if (!user) return;
|
|
440
|
+
// `?fresh=1` bypasses the 30s list cache — event-driven refreshes (a
|
|
441
|
+
// proposal or suggestion-routed upload just landed) ask because they KNOW
|
|
442
|
+
// the list changed, and a cached answer would hide their own change.
|
|
443
|
+
const fresh = isTruthyQuery(req.query.fresh);
|
|
434
444
|
try {
|
|
435
|
-
res.json(await workflow.listChangeRequestsAuthoredBy(user.email));
|
|
445
|
+
res.json(await workflow.listChangeRequestsAuthoredBy(user.email, { fresh }));
|
|
436
446
|
} catch (err) {
|
|
437
447
|
const { status, body } = toHttpError(err);
|
|
438
448
|
res.status(status).json(body);
|
|
@@ -497,7 +507,7 @@ export function createWorkflowRoutes(
|
|
|
497
507
|
// blip when the caller's workspace isn't yet provisioned.
|
|
498
508
|
}
|
|
499
509
|
}
|
|
500
|
-
|
|
510
|
+
let detail = await workflow.getChangeRequestDetail(num, {
|
|
501
511
|
fresh,
|
|
502
512
|
workspaceId,
|
|
503
513
|
viewerEmail,
|
|
@@ -506,6 +516,26 @@ export function createWorkflowRoutes(
|
|
|
506
516
|
res.status(404).json({ error: 'change request not found' });
|
|
507
517
|
return;
|
|
508
518
|
}
|
|
519
|
+
// Lazy reconcile (the join-requests pattern): an OPEN request whose
|
|
520
|
+
// diff has emptied — everything it proposed has since landed, been
|
|
521
|
+
// reverted, or been undone on its branch — closes itself on
|
|
522
|
+
// observation and its branch retires. The close re-verifies the
|
|
523
|
+
// emptiness authoritatively and never fires on a failed diff.
|
|
524
|
+
if (detail.state === 'open' && detail.files.length === 0 && req.userId) {
|
|
525
|
+
try {
|
|
526
|
+
const user = await authService.getUserById(req.userId);
|
|
527
|
+
if (user && (await workflow.closeEmptyChangeRequest(num, user))) {
|
|
528
|
+
detail =
|
|
529
|
+
(await workflow.getChangeRequestDetail(num, {
|
|
530
|
+
fresh: true,
|
|
531
|
+
workspaceId,
|
|
532
|
+
viewerEmail,
|
|
533
|
+
})) ?? detail;
|
|
534
|
+
}
|
|
535
|
+
} catch (err) {
|
|
536
|
+
console.warn(`[cr] lazy empty-close of #${num} failed (non-fatal):`, err);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
509
539
|
res.json(detail);
|
|
510
540
|
} catch (err) {
|
|
511
541
|
const { status, body } = toHttpError(err);
|
|
@@ -513,6 +543,54 @@ export function createWorkflowRoutes(
|
|
|
513
543
|
}
|
|
514
544
|
});
|
|
515
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Delete a change request outright — close it and retire its source
|
|
548
|
+
* branch. Admin-only; the moderation verb for a shared deployment.
|
|
549
|
+
*/
|
|
550
|
+
router.delete('/workflow/change-requests/:number', async (req, res) => {
|
|
551
|
+
const num = parsePrNumber(req.params.number);
|
|
552
|
+
if (num === null) {
|
|
553
|
+
res.status(400).json({ error: 'invalid change request number' });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const user = await requireUser(req, res);
|
|
557
|
+
if (!user) return;
|
|
558
|
+
try {
|
|
559
|
+
await workflow.deleteChangeRequest(num, user);
|
|
560
|
+
res.json({ deleted: true });
|
|
561
|
+
} catch (err) {
|
|
562
|
+
const { status, body } = toHttpError(err);
|
|
563
|
+
res.status(status).json(body);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Decline ONE file of a change request: restore its merge-base version on
|
|
569
|
+
* the source branch, dropping it from the request's diff. Takes the same
|
|
570
|
+
* permission as approving the file. When the last file is declined the
|
|
571
|
+
* request closes itself and its branch retires (`closed: true`).
|
|
572
|
+
*/
|
|
573
|
+
router.post('/workflow/change-requests/:number/files/revert', async (req, res) => {
|
|
574
|
+
const num = parsePrNumber(req.params.number);
|
|
575
|
+
if (num === null) {
|
|
576
|
+
res.status(400).json({ error: 'invalid change request number' });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const user = await requireUser(req, res);
|
|
580
|
+
if (!user) return;
|
|
581
|
+
const body = (req.body ?? {}) as { path?: string };
|
|
582
|
+
if (typeof body.path !== 'string' || !body.path) {
|
|
583
|
+
res.status(400).json({ error: 'path is required' });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
res.json(await workflow.revertChangeRequestFile(num, user, body.path));
|
|
588
|
+
} catch (err) {
|
|
589
|
+
const { status, body: errBody } = toHttpError(err);
|
|
590
|
+
res.status(status).json(errBody);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
516
594
|
router.post('/workflow/change-requests', async (req, res) => {
|
|
517
595
|
const user = await requireUser(req, res);
|
|
518
596
|
if (!user) return;
|