@bevel-software/platform-core-backend 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +6118 -0
- 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 +5 -4
- 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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { GroupIndexService, groupsWorkspaceId } from './groups.service.js';
|
|
2
|
+
export { GroupProvisionService, GroupProvisionError, groupAccessMd, personalAccessMd, } from './group-provision.service.js';
|
|
3
|
+
export { JoinRequestsService, type JoinRequest } from './join-requests.service.js';
|
|
4
|
+
export { pendingProposals, type JoinProposal } from './join-proposals.js';
|
|
5
|
+
export { createGroupsRoutes } from './groups.routes.js';
|
|
6
|
+
export type { IGroupIndexService, GroupCatalogEntry, GroupSummary, ResolvedPrincipals, ResolvedReaders, } from './groups.contract.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/groups/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,eAAe,GAChB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { GroupIndexService, groupsWorkspaceId } from './groups.service.js';
|
|
2
|
+
export { GroupProvisionService, GroupProvisionError, groupAccessMd, personalAccessMd, } from './group-provision.service.js';
|
|
3
|
+
export { JoinRequestsService } from './join-requests.service.js';
|
|
4
|
+
export { pendingProposals } from './join-proposals.js';
|
|
5
|
+
export { createGroupsRoutes } from './groups.routes.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modules/groups/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAoB,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAqB,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Verb } from '../access/access-control.service.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a join branch is PROPOSING, relative to the default branch.
|
|
4
|
+
*
|
|
5
|
+
* A join request is a change request whose branch edits one group's
|
|
6
|
+
* `access.md`. Rather than treating that change request as an all-or-nothing
|
|
7
|
+
* merge, the group's managers see its individual proposals — "grant Ali read",
|
|
8
|
+
* "grant GTM Team write" — and answer them one at a time. Approving a proposal
|
|
9
|
+
* writes THAT ONE grant onto the default branch through the ordinary access
|
|
10
|
+
* mutation path; the branch is never merged, so nothing else it happens to
|
|
11
|
+
* contain can ride along, and a change request naming five people can be
|
|
12
|
+
* answered with two yeses and three ignores.
|
|
13
|
+
*
|
|
14
|
+
* A proposal is any (principal, verb) GRANT the branch's copy of the file
|
|
15
|
+
* carries that the default branch's copy does not. Deliberately:
|
|
16
|
+
*
|
|
17
|
+
* - grants only. A `deny` entry is not something to "accept", and a branch
|
|
18
|
+
* that REMOVES an existing grant proposes nothing — it is a revocation,
|
|
19
|
+
* which is not what this surface is for. Both are simply invisible here,
|
|
20
|
+
* and both leave the branch as a normal change request in the review UI.
|
|
21
|
+
* - every verb, not just `read`. A branch asking for `write` must be visible
|
|
22
|
+
* AS a write request rather than hiding behind "asked to join"; and the
|
|
23
|
+
* subset test that retires a request has to cover everything the file can
|
|
24
|
+
* express, or a request proposing `write` would never settle.
|
|
25
|
+
*
|
|
26
|
+
* When the list comes back EMPTY the branch adds nothing the default branch
|
|
27
|
+
* does not already grant — its proposals have all been accepted (or were
|
|
28
|
+
* never anything) — and the change request has no reason to stay open.
|
|
29
|
+
*/
|
|
30
|
+
export interface JoinProposal {
|
|
31
|
+
verb: Verb;
|
|
32
|
+
/** Canonical identity — lowercased email, or canonical role name. */
|
|
33
|
+
id: string;
|
|
34
|
+
principal: {
|
|
35
|
+
kind: 'user';
|
|
36
|
+
email: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'role';
|
|
40
|
+
role: string;
|
|
41
|
+
};
|
|
42
|
+
/** How to name this principal in the UI. */
|
|
43
|
+
label: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The grants `branchText` adds over `defaultText` — the proposals a manager
|
|
47
|
+
* can accept. Empty ⇒ the branch's rules are a subset of the default's.
|
|
48
|
+
*
|
|
49
|
+
* `path` is only used to label parse errors; both texts are the same file at
|
|
50
|
+
* two refs.
|
|
51
|
+
*/
|
|
52
|
+
export declare function pendingProposals(branchText: string | null, defaultText: string | null, path: string): JoinProposal[];
|
|
53
|
+
//# sourceMappingURL=join-proposals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join-proposals.d.ts","sourceRoot":"","sources":["../../../src/modules/groups/join-proposals.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,IAAI,EACV,MAAM,qCAAqC,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,IAAI,CAAC;IACX,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EACL;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GACpD;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;CACf;AAgCD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,IAAI,EAAE,MAAM,GACX,YAAY,EAAE,CA0BhB"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { KNOWN_VERBS, parseAccessFile, } from '../access/access-control.service.js';
|
|
2
|
+
/** Canonical identity of an entry, for set comparison across the two files. */
|
|
3
|
+
function identityOf(entry) {
|
|
4
|
+
return entry.kind === 'user' ? `user:${entry.email}` : `role:${entry.role}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Folder rules from one copy of an `access.md`, per verb, grants only.
|
|
8
|
+
*
|
|
9
|
+
* A file that fails to parse yields NO grants. On the branch side that means
|
|
10
|
+
* a malformed proposal offers nothing to accept (fail-closed). On the default
|
|
11
|
+
* side it means every branch grant looks incoming — which is the safe
|
|
12
|
+
* direction too: the manager is shown proposals to consider rather than
|
|
13
|
+
* having them silently swallowed by an unreadable baseline.
|
|
14
|
+
*/
|
|
15
|
+
function grantsByVerb(text, path) {
|
|
16
|
+
const out = new Map();
|
|
17
|
+
for (const verb of KNOWN_VERBS)
|
|
18
|
+
out.set(verb, new Map());
|
|
19
|
+
if (text === null)
|
|
20
|
+
return out;
|
|
21
|
+
const parsed = parseAccessFile(text, path);
|
|
22
|
+
if (!parsed.ok)
|
|
23
|
+
return out;
|
|
24
|
+
for (const verb of KNOWN_VERBS) {
|
|
25
|
+
const byId = out.get(verb);
|
|
26
|
+
for (const entry of parsed.file.entries[verb]) {
|
|
27
|
+
if (entry.deny)
|
|
28
|
+
continue;
|
|
29
|
+
byId.set(identityOf(entry), entry);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The grants `branchText` adds over `defaultText` — the proposals a manager
|
|
36
|
+
* can accept. Empty ⇒ the branch's rules are a subset of the default's.
|
|
37
|
+
*
|
|
38
|
+
* `path` is only used to label parse errors; both texts are the same file at
|
|
39
|
+
* two refs.
|
|
40
|
+
*/
|
|
41
|
+
export function pendingProposals(branchText, defaultText, path) {
|
|
42
|
+
const branch = grantsByVerb(branchText, path);
|
|
43
|
+
const base = grantsByVerb(defaultText, path);
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const verb of KNOWN_VERBS) {
|
|
46
|
+
const baseIds = base.get(verb);
|
|
47
|
+
for (const [id, entry] of branch.get(verb)) {
|
|
48
|
+
if (baseIds.has(id))
|
|
49
|
+
continue;
|
|
50
|
+
out.push(entry.kind === 'user'
|
|
51
|
+
? {
|
|
52
|
+
verb,
|
|
53
|
+
id,
|
|
54
|
+
principal: { kind: 'user', email: entry.email, displayName: entry.displayName },
|
|
55
|
+
label: entry.displayName,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
verb,
|
|
59
|
+
id,
|
|
60
|
+
principal: { kind: 'role', role: entry.displayRole },
|
|
61
|
+
label: entry.displayRole,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=join-proposals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join-proposals.js","sourceRoot":"","sources":["../../../src/modules/groups/join-proposals.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,eAAe,GAGhB,MAAM,qCAAqC,CAAC;AAyC7C,+EAA+E;AAC/E,SAAS,UAAU,CAAC,KAAkB;IACpC,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,YAAY,CAAC,IAAmB,EAAE,IAAY;IACrD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkC,CAAC;IACtD,KAAK,MAAM,IAAI,IAAI,WAAW;QAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;IACzD,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,GAAG,CAAC;IAC9B,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;QAC5B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9C,IAAI,KAAK,CAAC,IAAI;gBAAE,SAAS;YACzB,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAyB,EACzB,WAA0B,EAC1B,IAAY;IAEZ,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;QAChC,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAE,EAAE,CAAC;YAC5C,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC9B,GAAG,CAAC,IAAI,CACN,KAAK,CAAC,IAAI,KAAK,MAAM;gBACnB,CAAC,CAAC;oBACE,IAAI;oBACJ,EAAE;oBACF,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE;oBAC/E,KAAK,EAAE,KAAK,CAAC,WAAW;iBACzB;gBACH,CAAC,CAAC;oBACE,IAAI;oBACJ,EAAE;oBACF,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,WAAW,EAAE;oBACpD,KAAK,EAAE,KAAK,CAAC,WAAW;iBACzB,CACN,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type AuthUser, type ChangeRequest, type IWorkflowService } from '@bevel-software/platform-shared';
|
|
2
|
+
import { type WorkspaceService } from '../workspace/workspace.service.js';
|
|
3
|
+
import { type JoinProposal } from './join-proposals.js';
|
|
4
|
+
/** One open join change request, with what it still proposes. */
|
|
5
|
+
export interface JoinRequest {
|
|
6
|
+
number: number;
|
|
7
|
+
branch: string;
|
|
8
|
+
requesterName: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
/** Grants the branch adds over the default branch. Never empty (see below). */
|
|
11
|
+
proposals: JoinProposal[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Join requests as PROPOSALS, and the reconciliation that retires them.
|
|
15
|
+
*
|
|
16
|
+
* The lifecycle has no state of its own — it is derived, every time, from two
|
|
17
|
+
* copies of one file:
|
|
18
|
+
*
|
|
19
|
+
* open the branch's `access.md` grants something the default's does not
|
|
20
|
+
* settled it does not
|
|
21
|
+
*
|
|
22
|
+
* so "approve" is not an operation on the request at all. A manager grants a
|
|
23
|
+
* principal through the ordinary access path (the same lock + commit every
|
|
24
|
+
* other access edit uses, gated the same way), the default branch gains that
|
|
25
|
+
* entry, and the proposal stops being pending because the diff no longer
|
|
26
|
+
* contains it. When the last one goes, the change request has nothing left to
|
|
27
|
+
* say and is rejected, its branch deleted.
|
|
28
|
+
*
|
|
29
|
+
* That reconciliation runs LAZILY on every listing as well as on demand, so a
|
|
30
|
+
* grant made anywhere — this banner, the Manage-access dialog, a hand edit —
|
|
31
|
+
* settles the request that asked for it. Nothing has to remember to.
|
|
32
|
+
*/
|
|
33
|
+
export declare class JoinRequestsService {
|
|
34
|
+
private readonly workspaceService;
|
|
35
|
+
private readonly workflow;
|
|
36
|
+
constructor(workspaceService: WorkspaceService, workflow: IWorkflowService);
|
|
37
|
+
/**
|
|
38
|
+
* Open join requests for `group`, each carrying only its still-pending
|
|
39
|
+
* proposals. Requests with none left are reconciled away (rejected, branch
|
|
40
|
+
* deleted) and omitted.
|
|
41
|
+
*
|
|
42
|
+
* `actor` is who the reconciliation acts as — a manager of the group, since
|
|
43
|
+
* rejecting a change request requires write on everything it touches.
|
|
44
|
+
*/
|
|
45
|
+
list(group: string, folder: string, crs: ChangeRequest[], actor: AuthUser): Promise<JoinRequest[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Re-check ONE request and settle it if nothing is pending. Returns whether
|
|
48
|
+
* it was settled. Called right after a grant so the banner updates in the
|
|
49
|
+
* same round-trip instead of waiting for the next listing.
|
|
50
|
+
*/
|
|
51
|
+
reconcile(group: string, folder: string, cr: ChangeRequest, actor: AuthUser): Promise<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* Close a request whose proposals have all landed, and delete its branch.
|
|
54
|
+
*
|
|
55
|
+
* Rejecting rather than merging is the point: the grants are already on the
|
|
56
|
+
* default branch, so merging would replay a diff that is now empty of
|
|
57
|
+
* meaning while dragging along anything else the branch carries. The branch
|
|
58
|
+
* goes too — it exists only to hold a proposal that no longer exists.
|
|
59
|
+
*
|
|
60
|
+
* Best-effort by contract: this runs inside a listing, and a request that
|
|
61
|
+
* cannot be closed right now must not take the listing down with it. The
|
|
62
|
+
* next pass tries again.
|
|
63
|
+
*/
|
|
64
|
+
private settle;
|
|
65
|
+
/**
|
|
66
|
+
* The file at one branch, or null when it is absent/unreadable there.
|
|
67
|
+
*
|
|
68
|
+
* Every read runs against the DEFAULT branch's clone at `origin/<branch>`,
|
|
69
|
+
* so one workspace answers for every request and a join branch never needs
|
|
70
|
+
* a clone of its own. `readFileAtRef` takes a REPO-relative path (the KB
|
|
71
|
+
* dir is the repo root there).
|
|
72
|
+
*/
|
|
73
|
+
private readAt;
|
|
74
|
+
/**
|
|
75
|
+
* Refresh remote-tracking refs before reading them. Throttled inside, and
|
|
76
|
+
* best-effort: a request pushed seconds ago is worth one fetch, but a fetch
|
|
77
|
+
* failure must degrade to "read what we have" rather than empty the list.
|
|
78
|
+
*/
|
|
79
|
+
private refresh;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=join-requests.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join-requests.service.d.ts","sourceRoot":"","sources":["../../../src/modules/groups/join-requests.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAwB,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAChG,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE1E,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,SAAS,EAAE,YAAY,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,gBAAgB,EAAE,gBAAgB,EAClC,QAAQ,EAAE,gBAAgB;IAG7C;;;;;;;OAOG;IACG,IAAI,CACR,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,aAAa,EAAE,EACpB,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,WAAW,EAAE,CAAC;IA2BzB;;;;OAIG;IACG,SAAS,CACb,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,OAAO,CAAC;IAcnB;;;;;;;;;;;OAWG;YACW,MAAM;IAgCpB;;;;;;;OAOG;YACW,MAAM;IAYpB;;;;OAIG;YACW,OAAO;CAKtB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DEFAULT_BRANCH, isJoinBranchFor, } from '@bevel-software/platform-shared';
|
|
2
|
+
import { workspaceIdForBranch } from '../workspace/workspace.service.js';
|
|
3
|
+
import { pendingProposals } from './join-proposals.js';
|
|
4
|
+
/**
|
|
5
|
+
* Join requests as PROPOSALS, and the reconciliation that retires them.
|
|
6
|
+
*
|
|
7
|
+
* The lifecycle has no state of its own — it is derived, every time, from two
|
|
8
|
+
* copies of one file:
|
|
9
|
+
*
|
|
10
|
+
* open the branch's `access.md` grants something the default's does not
|
|
11
|
+
* settled it does not
|
|
12
|
+
*
|
|
13
|
+
* so "approve" is not an operation on the request at all. A manager grants a
|
|
14
|
+
* principal through the ordinary access path (the same lock + commit every
|
|
15
|
+
* other access edit uses, gated the same way), the default branch gains that
|
|
16
|
+
* entry, and the proposal stops being pending because the diff no longer
|
|
17
|
+
* contains it. When the last one goes, the change request has nothing left to
|
|
18
|
+
* say and is rejected, its branch deleted.
|
|
19
|
+
*
|
|
20
|
+
* That reconciliation runs LAZILY on every listing as well as on demand, so a
|
|
21
|
+
* grant made anywhere — this banner, the Manage-access dialog, a hand edit —
|
|
22
|
+
* settles the request that asked for it. Nothing has to remember to.
|
|
23
|
+
*/
|
|
24
|
+
export class JoinRequestsService {
|
|
25
|
+
workspaceService;
|
|
26
|
+
workflow;
|
|
27
|
+
constructor(workspaceService, workflow) {
|
|
28
|
+
this.workspaceService = workspaceService;
|
|
29
|
+
this.workflow = workflow;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Open join requests for `group`, each carrying only its still-pending
|
|
33
|
+
* proposals. Requests with none left are reconciled away (rejected, branch
|
|
34
|
+
* deleted) and omitted.
|
|
35
|
+
*
|
|
36
|
+
* `actor` is who the reconciliation acts as — a manager of the group, since
|
|
37
|
+
* rejecting a change request requires write on everything it touches.
|
|
38
|
+
*/
|
|
39
|
+
async list(group, folder, crs, actor) {
|
|
40
|
+
await this.refresh();
|
|
41
|
+
const accessPath = `${folder}/access.md`;
|
|
42
|
+
const baseText = await this.readAt(DEFAULT_BRANCH, accessPath);
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const cr of crs) {
|
|
45
|
+
if (cr.state !== 'open' || !isJoinBranchFor(cr.branch, group))
|
|
46
|
+
continue;
|
|
47
|
+
const proposals = pendingProposals(await this.readAt(cr.branch, accessPath), baseText, accessPath);
|
|
48
|
+
if (proposals.length === 0) {
|
|
49
|
+
await this.settle(cr, actor);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
out.push({
|
|
53
|
+
number: cr.number,
|
|
54
|
+
branch: cr.branch,
|
|
55
|
+
requesterName: cr.appAuthor?.name ?? 'Someone',
|
|
56
|
+
createdAt: cr.createdAt,
|
|
57
|
+
proposals,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Re-check ONE request and settle it if nothing is pending. Returns whether
|
|
64
|
+
* it was settled. Called right after a grant so the banner updates in the
|
|
65
|
+
* same round-trip instead of waiting for the next listing.
|
|
66
|
+
*/
|
|
67
|
+
async reconcile(group, folder, cr, actor) {
|
|
68
|
+
if (cr.state !== 'open' || !isJoinBranchFor(cr.branch, group))
|
|
69
|
+
return false;
|
|
70
|
+
await this.refresh();
|
|
71
|
+
const accessPath = `${folder}/access.md`;
|
|
72
|
+
const proposals = pendingProposals(await this.readAt(cr.branch, accessPath), await this.readAt(DEFAULT_BRANCH, accessPath), accessPath);
|
|
73
|
+
if (proposals.length > 0)
|
|
74
|
+
return false;
|
|
75
|
+
await this.settle(cr, actor);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Close a request whose proposals have all landed, and delete its branch.
|
|
80
|
+
*
|
|
81
|
+
* Rejecting rather than merging is the point: the grants are already on the
|
|
82
|
+
* default branch, so merging would replay a diff that is now empty of
|
|
83
|
+
* meaning while dragging along anything else the branch carries. The branch
|
|
84
|
+
* goes too — it exists only to hold a proposal that no longer exists.
|
|
85
|
+
*
|
|
86
|
+
* Best-effort by contract: this runs inside a listing, and a request that
|
|
87
|
+
* cannot be closed right now must not take the listing down with it. The
|
|
88
|
+
* next pass tries again.
|
|
89
|
+
*/
|
|
90
|
+
async settle(cr, actor) {
|
|
91
|
+
const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
|
|
92
|
+
try {
|
|
93
|
+
await this.workflow.rejectChangeRequest(cr.number, actor, cr.state, cr.authorId ?? null, cr.base, wsId);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.warn(`[groups] could not close settled join request #${cr.number}: ${err instanceof Error ? err.message : String(err)}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await this.workflow.deleteBranch(wsId, cr.branch, actor);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
// The request is closed either way; a leftover branch is cosmetic and
|
|
104
|
+
// the author can still delete it themselves.
|
|
105
|
+
console.warn(`[groups] closed join request #${cr.number} but could not delete "${cr.branch}": ${err instanceof Error ? err.message : String(err)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The file at one branch, or null when it is absent/unreadable there.
|
|
110
|
+
*
|
|
111
|
+
* Every read runs against the DEFAULT branch's clone at `origin/<branch>`,
|
|
112
|
+
* so one workspace answers for every request and a join branch never needs
|
|
113
|
+
* a clone of its own. `readFileAtRef` takes a REPO-relative path (the KB
|
|
114
|
+
* dir is the repo root there).
|
|
115
|
+
*/
|
|
116
|
+
async readAt(branch, repoRelPath) {
|
|
117
|
+
try {
|
|
118
|
+
return await this.workspaceService.readFileAtRef(workspaceIdForBranch(DEFAULT_BRANCH), `origin/${branch}`, repoRelPath);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Refresh remote-tracking refs before reading them. Throttled inside, and
|
|
126
|
+
* best-effort: a request pushed seconds ago is worth one fetch, but a fetch
|
|
127
|
+
* failure must degrade to "read what we have" rather than empty the list.
|
|
128
|
+
*/
|
|
129
|
+
async refresh() {
|
|
130
|
+
await this.workspaceService
|
|
131
|
+
.ensureRemotesFetched(workspaceIdForBranch(DEFAULT_BRANCH))
|
|
132
|
+
.catch(() => undefined);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=join-requests.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join-requests.service.js","sourceRoot":"","sources":["../../../src/modules/groups/join-requests.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,26 @@ export interface SecretsVaultRoutesDeps {
|
|
|
16
16
|
/** Public base URL of the SPA — where the OAuth callback lands the browser. */
|
|
17
17
|
publicFrontendUrl: string;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Is this a SAME-ORIGIN path we're willing to bounce the browser back to after a
|
|
21
|
+
* sign-in? The callback builds `${publicFrontendUrl}${dest}#…`, so anything that
|
|
22
|
+
* survives this check gets concatenated onto our own origin — which makes every
|
|
23
|
+
* rule here an open-redirect rule, not a formatting preference:
|
|
24
|
+
* - must start with a single `/` — `//evil.com` is a protocol-relative URL and
|
|
25
|
+
* `https://evil.com` an absolute one; both would leave our origin entirely.
|
|
26
|
+
* - no `\` — WHATWG folds a backslash to a slash for http(s), so `/\evil.com`
|
|
27
|
+
* is `//evil.com` by another spelling.
|
|
28
|
+
* - no `#` — the callback appends its own fragment; a caller-supplied one would
|
|
29
|
+
* swallow the `#authorized=…`/`#error=…` the landing page reads.
|
|
30
|
+
* - no CR/LF — never let a caller-controlled string reach a `Location` header
|
|
31
|
+
* with a line break in it (response splitting).
|
|
32
|
+
* - ≤512 chars — a signed `state` rides in the provider's authorize URL, and
|
|
33
|
+
* some providers cap its length; this keeps the round-trip inside every
|
|
34
|
+
* budget without needing to know each provider's limit.
|
|
35
|
+
* Exported for the route tests: this predicate IS the security boundary, so it
|
|
36
|
+
* is pinned by a unit table rather than only exercised end-to-end.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isSafeReturnPath(returnTo: unknown): returnTo is string;
|
|
19
39
|
/**
|
|
20
40
|
* Authenticated Secrets Vault CRUD + OAuth start. Every route is scoped to
|
|
21
41
|
* `req.userId` — secrets are private per user. Mounted behind the JWT middleware.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets-vault.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/secrets-vault/secrets-vault.routes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAG9B,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,kBAAkB,EAAmC,MAAM,0CAA0C,CAAC;AAEpH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,4BAA4B,CAAC;AAEpC,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,oBAAoB,CAAC;IACnC,2FAA2F;IAC3F,iBAAiB,EAAE,kBAAkB,CAAC;IACtC,uFAAuF;IACvF,aAAa,EAAE,cAAc,CAAC;IAC9B,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,gBAAgB,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAMD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"secrets-vault.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/secrets-vault/secrets-vault.routes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAG9B,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,kBAAkB,EAAmC,MAAM,0CAA0C,CAAC;AAEpH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,4BAA4B,CAAC;AAEpC,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,oBAAoB,CAAC;IACnC,2FAA2F;IAC3F,iBAAiB,EAAE,kBAAkB,CAAC;IACtC,uFAAuF;IACvF,aAAa,EAAE,cAAc,CAAC;IAC9B,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,gBAAgB,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,MAAM,CAUtE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAmarF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAkC3F"}
|
|
@@ -8,6 +8,34 @@ import '../auth/auth.middleware.js'; // Express Request augmentation (req.userId
|
|
|
8
8
|
function redirectUriFor(publicBackendUrl) {
|
|
9
9
|
return `${publicBackendUrl}/api/secrets/oauth/callback`;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Is this a SAME-ORIGIN path we're willing to bounce the browser back to after a
|
|
13
|
+
* sign-in? The callback builds `${publicFrontendUrl}${dest}#…`, so anything that
|
|
14
|
+
* survives this check gets concatenated onto our own origin — which makes every
|
|
15
|
+
* rule here an open-redirect rule, not a formatting preference:
|
|
16
|
+
* - must start with a single `/` — `//evil.com` is a protocol-relative URL and
|
|
17
|
+
* `https://evil.com` an absolute one; both would leave our origin entirely.
|
|
18
|
+
* - no `\` — WHATWG folds a backslash to a slash for http(s), so `/\evil.com`
|
|
19
|
+
* is `//evil.com` by another spelling.
|
|
20
|
+
* - no `#` — the callback appends its own fragment; a caller-supplied one would
|
|
21
|
+
* swallow the `#authorized=…`/`#error=…` the landing page reads.
|
|
22
|
+
* - no CR/LF — never let a caller-controlled string reach a `Location` header
|
|
23
|
+
* with a line break in it (response splitting).
|
|
24
|
+
* - ≤512 chars — a signed `state` rides in the provider's authorize URL, and
|
|
25
|
+
* some providers cap its length; this keeps the round-trip inside every
|
|
26
|
+
* budget without needing to know each provider's limit.
|
|
27
|
+
* Exported for the route tests: this predicate IS the security boundary, so it
|
|
28
|
+
* is pinned by a unit table rather than only exercised end-to-end.
|
|
29
|
+
*/
|
|
30
|
+
export function isSafeReturnPath(returnTo) {
|
|
31
|
+
return (typeof returnTo === 'string' &&
|
|
32
|
+
returnTo.length <= 512 &&
|
|
33
|
+
returnTo.startsWith('/') &&
|
|
34
|
+
!returnTo.startsWith('//') &&
|
|
35
|
+
!returnTo.includes('\\') &&
|
|
36
|
+
!returnTo.includes('#') &&
|
|
37
|
+
!/[\r\n]/.test(returnTo));
|
|
38
|
+
}
|
|
11
39
|
/**
|
|
12
40
|
* Authenticated Secrets Vault CRUD + OAuth start. Every route is scoped to
|
|
13
41
|
* `req.userId` — secrets are private per user. Mounted behind the JWT middleware.
|
|
@@ -15,7 +43,14 @@ function redirectUriFor(publicBackendUrl) {
|
|
|
15
43
|
export function createSecretsVaultRoutes(deps) {
|
|
16
44
|
const { secretsVault, toolManualService, accessControl } = deps;
|
|
17
45
|
const router = express.Router();
|
|
18
|
-
|
|
46
|
+
// A FUNCTION, not a constant. Routers are constructed at boot, and on a
|
|
47
|
+
// deployment configured through the setup screen the branch model does not
|
|
48
|
+
// exist yet at that moment — `DEFAULT_BRANCH` is still ''. The live binding
|
|
49
|
+
// updates when setup applies the model, but only reads INSIDE a function
|
|
50
|
+
// body see it; a construction-time capture would keep handing an empty
|
|
51
|
+
// workspace id to the access resolver until the next restart ("Invalid
|
|
52
|
+
// workspace ID" on every /secrets/tools call).
|
|
53
|
+
const defaultWs = () => workspaceIdForBranch(DEFAULT_BRANCH);
|
|
19
54
|
// The vault key = the exact key UTCP looks the var up under (doubles underscores
|
|
20
55
|
// in the manual namespace), so storage and resolution agree for snake_case ids.
|
|
21
56
|
const varKey = (manualName, varName) => utcpNamespacedKey(manualName, varName);
|
|
@@ -134,7 +169,7 @@ export function createSecretsVaultRoutes(deps) {
|
|
|
134
169
|
path: m.path,
|
|
135
170
|
type: m.type,
|
|
136
171
|
setup: m.setup ?? null,
|
|
137
|
-
canWrite: await accessControl.canWrite(defaultWs, email, m.path),
|
|
172
|
+
canWrite: await accessControl.canWrite(defaultWs(), email, m.path),
|
|
138
173
|
variables: (m.variables ?? []).map((v) => {
|
|
139
174
|
const key = varKey(m.name, v.name);
|
|
140
175
|
const st = statusByKey.get(key);
|
|
@@ -252,7 +287,7 @@ export function createSecretsVaultRoutes(deps) {
|
|
|
252
287
|
if (found.variable.scope !== 'admin') {
|
|
253
288
|
return void res.status(422).json({ error: 'This variable is set by each user, not the tool owner.' });
|
|
254
289
|
}
|
|
255
|
-
if (!(await accessControl.canWrite(defaultWs, email, found.manual.path))) {
|
|
290
|
+
if (!(await accessControl.canWrite(defaultWs(), email, found.manual.path))) {
|
|
256
291
|
return void res.status(403).json({ error: 'You need write access to this tool to set its shared secrets.' });
|
|
257
292
|
}
|
|
258
293
|
const body = req.body ?? {};
|
|
@@ -309,7 +344,7 @@ export function createSecretsVaultRoutes(deps) {
|
|
|
309
344
|
if (!found.variable.oauth) {
|
|
310
345
|
return void res.status(422).json({ error: 'This variable is not an OAuth sign-in.' });
|
|
311
346
|
}
|
|
312
|
-
if (!(await accessControl.canWrite(defaultWs, email, found.manual.path))) {
|
|
347
|
+
if (!(await accessControl.canWrite(defaultWs(), email, found.manual.path))) {
|
|
313
348
|
return void res.status(403).json({ error: 'You need write access to this tool to set its client secret.' });
|
|
314
349
|
}
|
|
315
350
|
const clientSecret = (req.body ?? {}).clientSecret;
|
|
@@ -335,7 +370,9 @@ export function createSecretsVaultRoutes(deps) {
|
|
|
335
370
|
});
|
|
336
371
|
// Start sign-in for a tool's OAuth-backed variable. Provisions the caller's row
|
|
337
372
|
// from the owner-set secret and returns the provider consent URL; the callback
|
|
338
|
-
// bounces back to
|
|
373
|
+
// bounces back to wherever the flow started (via `r` in the signed state) — an
|
|
374
|
+
// optional `{ returnTo }` body when the caller asks for a specific page, else
|
|
375
|
+
// /connect, which is where every caller came from before `returnTo` existed.
|
|
339
376
|
router.post('/secrets/tools/:slug/vars/:var/oauth/start', async (req, res) => {
|
|
340
377
|
const userId = req.userId;
|
|
341
378
|
const email = req.userEmail;
|
|
@@ -363,11 +400,17 @@ export function createSecretsVaultRoutes(deps) {
|
|
|
363
400
|
// on the next sign-in. The secret/clientId/addresses stay owner-pinned.
|
|
364
401
|
scopes: found.variable.oauth.scopes,
|
|
365
402
|
});
|
|
403
|
+
// The return path is signed INTO the state (HMAC-SHA256) rather than kept
|
|
404
|
+
// in a query param or a cookie: the callback arrives un-authenticated from
|
|
405
|
+
// the provider, so the state is the only thing it can trust. An absent or
|
|
406
|
+
// unsafe `returnTo` falls back to the legacy `'connect'` marker, which
|
|
407
|
+
// keeps a body-less start byte-identical to what it produced before.
|
|
408
|
+
const returnTo = (req.body ?? {}).returnTo;
|
|
366
409
|
const state = signState(deps.stateSecret, {
|
|
367
410
|
u: userId,
|
|
368
411
|
i: id,
|
|
369
412
|
n: randomBytes(8).toString('hex'),
|
|
370
|
-
r: 'connect',
|
|
413
|
+
r: isSafeReturnPath(returnTo) ? returnTo : 'connect',
|
|
371
414
|
});
|
|
372
415
|
const consentUrl = new URL(url);
|
|
373
416
|
consentUrl.searchParams.set('state', state);
|
|
@@ -386,7 +429,7 @@ export function createSecretsVaultRoutes(deps) {
|
|
|
386
429
|
const found = await findManualVar(email, req.params.slug, req.params.var);
|
|
387
430
|
if (!found)
|
|
388
431
|
return void res.status(404).json({ error: 'Tool or variable not found' });
|
|
389
|
-
if (!(await accessControl.canWrite(defaultWs, email, found.manual.path))) {
|
|
432
|
+
if (!(await accessControl.canWrite(defaultWs(), email, found.manual.path))) {
|
|
390
433
|
return void res.status(403).json({ error: 'You need write access to this tool to remove its shared secrets.' });
|
|
391
434
|
}
|
|
392
435
|
await secretsVault.removeShared(varKey(found.manual.name, found.variable.name));
|
|
@@ -426,16 +469,21 @@ export function createSecretsVaultPublicRoutes(deps) {
|
|
|
426
469
|
const code = typeof req.query.code === 'string' ? req.query.code : '';
|
|
427
470
|
const stateRaw = typeof req.query.state === 'string' ? req.query.state : '';
|
|
428
471
|
// The landing page depends on where the flow started (the `r` field in the
|
|
429
|
-
// signed state):
|
|
430
|
-
//
|
|
431
|
-
// fall back to /secrets.
|
|
472
|
+
// signed state): an explicit same-origin path returns there, the legacy
|
|
473
|
+
// `'connect'` marker to /connect, the standalone Secrets page to /secrets.
|
|
474
|
+
// Pre-verification errors can't know `r`, so they fall back to /secrets.
|
|
432
475
|
const back = (frag, dest = '/secrets') => res.redirect(`${deps.publicFrontendUrl}${dest}#${frag}`);
|
|
433
476
|
if (!code || !stateRaw)
|
|
434
477
|
return void back(`error=${encodeURIComponent('Invalid OAuth callback.')}`);
|
|
435
478
|
const state = verifyState(deps.stateSecret, stateRaw);
|
|
436
479
|
if (!state)
|
|
437
480
|
return void back(`error=${encodeURIComponent('OAuth state mismatch.')}`);
|
|
438
|
-
|
|
481
|
+
// Re-validated HERE even though `r` was validated on the way in and is
|
|
482
|
+
// HMAC-signed: this is the line that concatenates a stored string onto our
|
|
483
|
+
// public origin and hands it to the browser as a `Location`. Defense in
|
|
484
|
+
// depth — the check costs nothing, and it means an open redirect would need
|
|
485
|
+
// BOTH the signing key and a validation bug, not either one.
|
|
486
|
+
const dest = isSafeReturnPath(state.r) ? state.r : state.r === 'connect' ? '/connect' : '/secrets';
|
|
439
487
|
try {
|
|
440
488
|
await secretsVault.completeOAuth(state.u, state.i, code, redirectUriFor(deps.publicBackendUrl));
|
|
441
489
|
back(`authorized=${encodeURIComponent(state.i)}`, dest);
|