@bevel-software/platform-core-backend 0.4.0 → 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 +157 -54
- 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 +97 -5
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +393 -114
- 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__/pending-commits.enqueue-if-absent.test.ts +82 -72
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +295 -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 +311 -306
- 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 +172 -64
- 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 +365 -19
- 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,
|
|
@@ -1403,12 +1426,23 @@ export class GitService implements IGitService {
|
|
|
1403
1426
|
// A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
|
|
1404
1427
|
// which makes every subsequent git command behave unexpectedly. Return the
|
|
1405
1428
|
// working tree to a clean state before surfacing the original error.
|
|
1406
|
-
|
|
1407
|
-
|
|
1429
|
+
let abortSucceeded = true;
|
|
1430
|
+
await this.git(cwd, ['rebase', '--abort']).catch(() => {
|
|
1431
|
+
abortSucceeded = false;
|
|
1432
|
+
});
|
|
1433
|
+
if (abortSucceeded && conflictedPaths.length > 0) {
|
|
1408
1434
|
// Typed so the workflow layer can queue background recovery — this
|
|
1409
1435
|
// divergence never resolves on its own (every retry pull hits the
|
|
1410
1436
|
// same conflict) and the unpushed local commits are someone's saved
|
|
1411
1437
|
// content stuck violating save=share.
|
|
1438
|
+
//
|
|
1439
|
+
// Gated on the abort succeeding: a failed abort means either no
|
|
1440
|
+
// rebase was ever active (the unmerged paths pre-date this pull —
|
|
1441
|
+
// some earlier operation left the index broken) or the clone is in
|
|
1442
|
+
// a state git itself can't unwind. Queueing recovery there would
|
|
1443
|
+
// point the worker's commitFile at a repo mid-conflict, risking a
|
|
1444
|
+
// commit full of conflict markers — surface the raw error instead
|
|
1445
|
+
// and leave diagnosis to a human.
|
|
1412
1446
|
throw new PullRebaseConflictError(
|
|
1413
1447
|
branch,
|
|
1414
1448
|
conflictedPaths,
|
|
@@ -1597,60 +1631,6 @@ export class GitService implements IGitService {
|
|
|
1597
1631
|
});
|
|
1598
1632
|
}
|
|
1599
1633
|
|
|
1600
|
-
async revertCommit(
|
|
1601
|
-
workspaceId: string,
|
|
1602
|
-
user: AuthUser,
|
|
1603
|
-
sha: string,
|
|
1604
|
-
): Promise<CommitAttribution> {
|
|
1605
|
-
if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
|
|
1606
|
-
throw new WorkflowValidationError('invalid commit sha');
|
|
1607
|
-
}
|
|
1608
|
-
assertValidAuthor(user);
|
|
1609
|
-
return this.mutex.run(workspaceId, async () => {
|
|
1610
|
-
const cwd = await this.repoDir(workspaceId);
|
|
1611
|
-
const branch = await this.currentBranch(cwd);
|
|
1612
|
-
|
|
1613
|
-
// Revert creates a new commit reversing `sha`. Same model as commit:
|
|
1614
|
-
// gate only on protected branches; reverts on feature/draft branches
|
|
1615
|
-
// are free (they produce a commit that would still need to merge via
|
|
1616
|
-
// PR to affect canonical state).
|
|
1617
|
-
if (isProtectedBranch(branch)) {
|
|
1618
|
-
const { stdout: diffOut } = await this.git(cwd, [
|
|
1619
|
-
'diff-tree', '--no-commit-id', '--name-only', '-r', sha,
|
|
1620
|
-
]);
|
|
1621
|
-
const touched = diffOut.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
1622
|
-
await this.assertCanWriteAtRef(workspaceId, 'HEAD', user.email, touched);
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
await this.git(cwd, [
|
|
1626
|
-
'revert',
|
|
1627
|
-
'--no-edit',
|
|
1628
|
-
`--author=${user.name} <${user.email}>`,
|
|
1629
|
-
sha,
|
|
1630
|
-
]);
|
|
1631
|
-
|
|
1632
|
-
const { stdout } = await this.git(cwd, [
|
|
1633
|
-
'log',
|
|
1634
|
-
'-1',
|
|
1635
|
-
'--pretty=format:%H%x00%an%x00%ae%x00%s%x00%aI',
|
|
1636
|
-
]);
|
|
1637
|
-
const [headSha, authorName, authorEmail, subj, committedAt] = stdout.split('\x00');
|
|
1638
|
-
|
|
1639
|
-
// A revert may have touched roles.yaml or access.md — drop cached
|
|
1640
|
-
// access state so the next gate read reflects the rolled-back tree.
|
|
1641
|
-
// Mirrors the commit / pull paths.
|
|
1642
|
-
this.accessControl?.invalidate(workspaceId);
|
|
1643
|
-
|
|
1644
|
-
return {
|
|
1645
|
-
sha: headSha?.trim() ?? '',
|
|
1646
|
-
authorName: authorName ?? '',
|
|
1647
|
-
authorEmail: authorEmail ?? '',
|
|
1648
|
-
subject: subj ?? '',
|
|
1649
|
-
committedAt: committedAt?.trim() ?? new Date().toISOString(),
|
|
1650
|
-
};
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
1634
|
async logForFile(
|
|
1655
1635
|
workspaceId: string,
|
|
1656
1636
|
relativePath: string,
|
|
@@ -1715,6 +1695,44 @@ export class GitService implements IGitService {
|
|
|
1715
1695
|
});
|
|
1716
1696
|
}
|
|
1717
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
|
+
|
|
1718
1736
|
async diffFileBetweenBranches(
|
|
1719
1737
|
workspaceId: string,
|
|
1720
1738
|
relativePath: string,
|
|
@@ -1848,8 +1866,20 @@ export class GitService implements IGitService {
|
|
|
1848
1866
|
this.git(cwd, ['diff', '-M', '-z', '--name-status', range]),
|
|
1849
1867
|
this.git(cwd, ['diff', '-M', '-z', '--numstat', range]),
|
|
1850
1868
|
]);
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
+
}
|
|
1853
1883
|
|
|
1854
1884
|
// `--name-status` and `--numstat` enumerate the same files in the same
|
|
1855
1885
|
// order (same `-M` over the same range), so we zip by index. If the two
|
|
@@ -1913,7 +1943,85 @@ export class GitService implements IGitService {
|
|
|
1913
1943
|
const { stdout } = await this.git(cwd, [
|
|
1914
1944
|
'diff', '-M', '--name-only', `${baseRef}...${headRef}`,
|
|
1915
1945
|
]);
|
|
1916
|
-
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
|
+
}
|
|
1917
2025
|
});
|
|
1918
2026
|
}
|
|
1919
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
|
}
|