@bevel-software/platform-core-backend 0.21.0 → 0.23.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 +11 -1
- 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 +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +62 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +158 -23
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +34 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +89 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +0 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +11 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-requests.contract.d.ts +75 -0
- package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
- package/dist/modules/access/access-requests.contract.js +20 -0
- package/dist/modules/access/access-requests.contract.js.map +1 -0
- package/dist/modules/access/access-requests.routes.d.ts +35 -0
- package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
- package/dist/modules/access/access-requests.routes.js +237 -0
- package/dist/modules/access/access-requests.routes.js.map +1 -0
- package/dist/modules/access/access-requests.service.d.ts +123 -0
- package/dist/modules/access/access-requests.service.d.ts.map +1 -0
- package/dist/modules/access/access-requests.service.js +337 -0
- package/dist/modules/access/access-requests.service.js.map +1 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +13 -15
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +12 -0
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +24 -0
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +96 -3
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +187 -9
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +41 -24
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +35 -27
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/github-app/github-app.client.d.ts +101 -0
- package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.client.js +222 -0
- package/dist/modules/github-app/github-app.client.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +102 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.connection.js +185 -0
- package/dist/modules/github-app/github-app.connection.js.map +1 -0
- package/dist/modules/github-app/github-app.routes.d.ts +59 -0
- package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.routes.js +323 -0
- package/dist/modules/github-app/github-app.routes.js.map +1 -0
- package/dist/modules/github-app/index.d.ts +4 -0
- package/dist/modules/github-app/index.d.ts.map +1 -0
- package/dist/modules/github-app/index.js +4 -0
- package/dist/modules/github-app/index.js.map +1 -0
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/remote-url.d.ts +22 -0
- package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
- package/dist/modules/kb-fs/remote-url.js +35 -0
- package/dist/modules/kb-fs/remote-url.js.map +1 -0
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/plugins/join-proposals.d.ts +17 -5
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
- package/dist/modules/plugins/join-proposals.js +76 -22
- package/dist/modules/plugins/join-proposals.js.map +1 -1
- package/dist/modules/plugins/join-requests.service.d.ts +111 -18
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-requests.service.js +173 -29
- package/dist/modules/plugins/join-requests.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +3 -2
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +101 -7
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/managed-repository.d.ts +43 -0
- package/dist/modules/settings/managed-repository.d.ts.map +1 -0
- package/dist/modules/settings/managed-repository.js +60 -0
- package/dist/modules/settings/managed-repository.js.map +1 -0
- package/dist/modules/settings/repository-source.d.ts +128 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -0
- package/dist/modules/settings/repository-source.js +150 -0
- package/dist/modules/settings/repository-source.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +78 -4
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +379 -17
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.js +24 -63
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +15 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +170 -6
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +5 -0
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +99 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +55 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +3 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +94 -13
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +258 -51
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +92 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +115 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +272 -26
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +2 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +337 -117
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +64 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +83 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +30 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +0 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +170 -1
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +69 -15
- package/src/core/create-core-services.ts +190 -27
- package/src/core/lifecycle.ts +98 -13
- package/src/core-config.ts +13 -14
- package/src/index.ts +12 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
- package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access-requests.contract.ts +93 -0
- package/src/modules/access/access-requests.routes.ts +286 -0
- package/src/modules/access/access-requests.service.ts +420 -0
- package/src/modules/access/access.routes.ts +13 -15
- package/src/modules/access-model/access-grammar.ts +22 -0
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
- package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +207 -7
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
- package/src/modules/github-app/github-app.client.ts +268 -0
- package/src/modules/github-app/github-app.connection.ts +204 -0
- package/src/modules/github-app/github-app.routes.ts +359 -0
- package/src/modules/github-app/index.ts +19 -0
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/remote-url.ts +35 -0
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
- package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
- package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/plugins/join-proposals.ts +87 -19
- package/src/modules/plugins/join-requests.service.ts +199 -39
- package/src/modules/plugins/plugins.routes.ts +3 -2
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +117 -6
- package/src/modules/settings/managed-repository.ts +68 -0
- package/src/modules/settings/repository-source.ts +197 -0
- package/src/modules/settings/setup.routes.ts +463 -15
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skill-access-requests.routes.ts +25 -75
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +4 -2
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-context.ts +15 -0
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
- package/src/modules/workflow/git/git.service.ts +193 -4
- package/src/modules/workflow/git/node-git-runner.ts +6 -0
- package/src/modules/workflow/git/pull-request.service.ts +119 -0
- package/src/modules/workflow/pending-commits.service.ts +59 -1
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.routes.ts +3 -1
- package/src/modules/workflow/workflow.service.ts +290 -55
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +273 -25
- package/src/modules/workspace/workspace.tools.ts +373 -119
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/domain-errors.ts +89 -0
- package/src/shared/git.contract.ts +37 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
- package/src/tenancy/static-tenant-source.ts +0 -3
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
6
|
+
import type { KbContext } from '../../../shared/kb-context.js';
|
|
7
|
+
import { createSetupRoutes } from '../setup.routes.js';
|
|
8
|
+
import { DeploymentSettingsService } from '../deployment-settings.service.js';
|
|
9
|
+
import { ManagedRepository } from '../managed-repository.js';
|
|
10
|
+
import { RepositorySource, type GitHubAppRepository } from '../repository-source.js';
|
|
11
|
+
import type { Database } from '../../database/connection.js';
|
|
12
|
+
import type { IAdminAccessService } from '../../admin/admin.interface.js';
|
|
13
|
+
import type { ConnectionCheck, RepositoryConnection } from '../connection-check.js';
|
|
14
|
+
|
|
15
|
+
const ENC_KEY = 'kToAi8FXWDpDn3A6yQ/60O39bv05N7XzVOIu/0CJrFc=';
|
|
16
|
+
const KB_ENV = ['KB_REPO_URL', 'GIT_TOKEN', 'GIT_USERNAME', 'GIT_MODE', 'GITHUB_TOKEN', 'DEFAULT_BRANCH', 'PROTECTED_BRANCHES', 'GITHUB_APP_REPOSITORY'] as const;
|
|
17
|
+
|
|
18
|
+
let server: HttpServer | null = null;
|
|
19
|
+
let savedEnv: Partial<Record<(typeof KB_ENV)[number], string | undefined>> = {};
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
savedEnv = {};
|
|
23
|
+
for (const k of KB_ENV) {
|
|
24
|
+
savedEnv[k] = process.env[k];
|
|
25
|
+
delete process.env[k];
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
server?.close();
|
|
31
|
+
server = null;
|
|
32
|
+
for (const k of KB_ENV) {
|
|
33
|
+
const original = savedEnv[k];
|
|
34
|
+
if (original === undefined) delete process.env[k];
|
|
35
|
+
else process.env[k] = original;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
type Answer = ConnectionCheck | ((connection: RepositoryConnection) => ConnectionCheck);
|
|
40
|
+
const CONNECTED: ConnectionCheck = { outcome: 'connected', branches: ['trunk', 'draft'], defaultBranch: 'trunk', empty: false };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The setup router over a deployment that has a GitHub App installed (or
|
|
44
|
+
* not), with GitHub's answer to the connection check given by the suite.
|
|
45
|
+
*/
|
|
46
|
+
function listen(
|
|
47
|
+
opts: {
|
|
48
|
+
kb?: KbContext;
|
|
49
|
+
installed?: boolean;
|
|
50
|
+
tokenGiven?: boolean;
|
|
51
|
+
offered?: boolean;
|
|
52
|
+
github?: Answer;
|
|
53
|
+
/** What the person who connected GitHub could push to. */
|
|
54
|
+
permitted?: string[];
|
|
55
|
+
} = {},
|
|
56
|
+
) {
|
|
57
|
+
const db = {
|
|
58
|
+
select: () => ({ from: () => Promise.resolve([]) }),
|
|
59
|
+
insert: () => ({ values: () => ({ onConflictDoUpdate: () => Promise.resolve() }) }),
|
|
60
|
+
delete: () => ({ where: () => Promise.resolve() }),
|
|
61
|
+
} as unknown as Database;
|
|
62
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
63
|
+
const kb = opts.kb ?? testKbContext({ branchModel: null });
|
|
64
|
+
let prepared = 0;
|
|
65
|
+
const githubApp: GitHubAppRepository = {
|
|
66
|
+
url: (read) => (read('githubRepository') ? `https://github.com/${read('githubRepository')}.git` : ''),
|
|
67
|
+
answered: (read) => (opts.installed ?? true) && Boolean(read('githubRepository')),
|
|
68
|
+
prepare: async () => {
|
|
69
|
+
prepared += 1;
|
|
70
|
+
if (opts.tokenGiven === false) throw new Error('GitHub answered 404');
|
|
71
|
+
},
|
|
72
|
+
token: () => (opts.tokenGiven === false || prepared === 0 ? null : 'an-installation-token'),
|
|
73
|
+
permits: (repository) => (opts.permitted ?? ['acme/kb', 'acme/another']).includes(repository),
|
|
74
|
+
};
|
|
75
|
+
const source = new RepositorySource({
|
|
76
|
+
read: (key) => settings.resolve(key),
|
|
77
|
+
managed: new ManagedRepository('/data/backups/managed-repository'),
|
|
78
|
+
githubApp,
|
|
79
|
+
});
|
|
80
|
+
const probed: RepositoryConnection[] = [];
|
|
81
|
+
let phaseRuns = 0;
|
|
82
|
+
const app = express();
|
|
83
|
+
app.use(express.json());
|
|
84
|
+
app.use((req, _res, next) => {
|
|
85
|
+
req.userEmail = 'root@example.com';
|
|
86
|
+
req.userId = 'user-1';
|
|
87
|
+
next();
|
|
88
|
+
});
|
|
89
|
+
app.use(
|
|
90
|
+
'/api',
|
|
91
|
+
createSetupRoutes(
|
|
92
|
+
settings,
|
|
93
|
+
{ isAdmin: async () => true } as IAdminAccessService,
|
|
94
|
+
{ runAll: async () => void (phaseRuns += 1) },
|
|
95
|
+
kb,
|
|
96
|
+
undefined,
|
|
97
|
+
async (connection) => {
|
|
98
|
+
probed.push(connection);
|
|
99
|
+
const answer = opts.github ?? CONNECTED;
|
|
100
|
+
return typeof answer === 'function' ? answer(connection) : answer;
|
|
101
|
+
},
|
|
102
|
+
undefined,
|
|
103
|
+
undefined,
|
|
104
|
+
undefined,
|
|
105
|
+
undefined,
|
|
106
|
+
{ source, ensureManaged: async () => undefined, ...(opts.offered === false ? {} : { githubApp }) },
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
server = app.listen(0);
|
|
110
|
+
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
111
|
+
/** `confirm` is the admin's answer to the question a move is asked. */
|
|
112
|
+
const save = async (entries: Record<string, string>, confirm?: 'keep' | 'close') => {
|
|
113
|
+
const res = await fetch(`${base}/api/setup/settings`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ settings: entries, ...(confirm ? { confirmRepositoryChange: confirm } : {}) }),
|
|
117
|
+
});
|
|
118
|
+
return { status: res.status, body: (await res.json()) as Record<string, unknown> & { problems?: Record<string, string> } };
|
|
119
|
+
};
|
|
120
|
+
const status = async () => (await fetch(`${base}/api/setup/status`)).json() as Promise<Record<string, unknown>>;
|
|
121
|
+
return { save, status, settings, source, probed, kb, phaseRuns: () => phaseRuns };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const CHOICE = { gitMode: 'github-app', githubRepository: 'acme/kb' };
|
|
125
|
+
|
|
126
|
+
describe('setup status: GitHub as a way of having a repository', () => {
|
|
127
|
+
it('is offered second, by a deployment that can connect to it', async () => {
|
|
128
|
+
expect(await listen().status()).toMatchObject({ repository: { mode: null, modes: ['managed', 'github-app', 'token'] } });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('is not offered by one that cannot', async () => {
|
|
132
|
+
expect(await listen({ offered: false }).status()).toMatchObject({ repository: { modes: ['managed', 'token'] } });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('choosing a repository on GitHub', () => {
|
|
137
|
+
it('finishes setup once GitHub accepts the installation for reading and writing', async () => {
|
|
138
|
+
const { save, probed, settings, kb, phaseRuns, source } = listen();
|
|
139
|
+
const { status, body } = await save(CHOICE);
|
|
140
|
+
expect(status, JSON.stringify(body)).toBe(200);
|
|
141
|
+
expect(body).toMatchObject({ ok: true, complete: true, restartRequired: false, repository: { mode: 'github-app' } });
|
|
142
|
+
// Asked with what git will present: the installation's token, never a stored one.
|
|
143
|
+
expect(probed).toEqual([{ url: 'https://github.com/acme/kb.git', token: 'an-installation-token', username: 'x-access-token' }]);
|
|
144
|
+
expect(source.url()).toBe('https://github.com/acme/kb.git');
|
|
145
|
+
expect(settings.resolve('githubRepository')).toBe('acme/kb');
|
|
146
|
+
expect(phaseRuns()).toBe(1);
|
|
147
|
+
// The repository said what it calls its trunk.
|
|
148
|
+
expect(kb.defaultBranch).toBe('trunk');
|
|
149
|
+
expect([...kb.protectedBranches]).toEqual(['trunk']);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('names the usual branch for a repository that is empty', async () => {
|
|
153
|
+
const { save, kb } = listen({ github: { outcome: 'connected', branches: [], defaultBranch: null, empty: true } });
|
|
154
|
+
expect((await save(CHOICE)).status).toBe(200);
|
|
155
|
+
expect(kb.defaultBranch).toBe('main');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('keeps the branches an admin named over the repository trunk', async () => {
|
|
159
|
+
const { save, settings } = listen();
|
|
160
|
+
await save({ ...CHOICE, defaultBranch: 'live', protectedBranches: 'live, draft' });
|
|
161
|
+
expect(settings.resolve('defaultBranch')).toBe('live');
|
|
162
|
+
expect(settings.resolve('protectedBranches')).toBe('live, draft');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The name is the one part of this connection an admin types, and the
|
|
167
|
+
* installation's token would answer for any repository the installation
|
|
168
|
+
* covers. So the name is held to what the person who connected GitHub
|
|
169
|
+
* could push to with their own account, BEFORE that token is used for
|
|
170
|
+
* anything: GitHub is not asked, so nothing about the repository, not
|
|
171
|
+
* even that it exists, comes back.
|
|
172
|
+
*/
|
|
173
|
+
it('refuses a repository the person who connected GitHub could not push to, without asking GitHub', async () => {
|
|
174
|
+
const { save, settings, probed, phaseRuns } = listen({ permitted: ['x/docs'] });
|
|
175
|
+
for (const name of ['x/payroll', 'x/handbook', 'someone-else/secrets']) {
|
|
176
|
+
const { status, body } = await save({ gitMode: 'github-app', githubRepository: name });
|
|
177
|
+
expect(status, name).toBe(400);
|
|
178
|
+
expect(body.problems?.githubRepository, name).toMatch(/your own GitHub account can write to/);
|
|
179
|
+
}
|
|
180
|
+
expect(probed).toEqual([]);
|
|
181
|
+
expect(settings.resolve('githubRepository')).toBe('');
|
|
182
|
+
expect(settings.resolve('gitMode')).toBe('');
|
|
183
|
+
expect(phaseRuns()).toBe(0);
|
|
184
|
+
// The one they can push to is theirs to connect.
|
|
185
|
+
expect((await save({ gitMode: 'github-app', githubRepository: 'x/docs' })).status).toBe(200);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('refuses a repository the installation no longer reaches, and stores nothing', async () => {
|
|
189
|
+
const { save, settings, phaseRuns } = listen({
|
|
190
|
+
github: { outcome: 'rejected', reason: 'not-found', field: 'kbRepoUrl', error: 'repository not found' },
|
|
191
|
+
});
|
|
192
|
+
const { status, body } = await save({ gitMode: 'github-app', githubRepository: 'acme/kb' });
|
|
193
|
+
expect(status).toBe(400);
|
|
194
|
+
expect(body.problems).toEqual({
|
|
195
|
+
githubRepository: 'The GitHub App cannot reach that repository. Add the repository to the app’s installation on GitHub.',
|
|
196
|
+
});
|
|
197
|
+
expect(settings.resolve('githubRepository')).toBe('');
|
|
198
|
+
expect(settings.resolve('gitMode')).toBe('');
|
|
199
|
+
expect(phaseRuns()).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it.each([
|
|
203
|
+
[
|
|
204
|
+
'it may read and not write',
|
|
205
|
+
{ outcome: 'read-only', field: 'gitToken', error: 'x', branches: ['main'], defaultBranch: 'main', empty: false } as ConnectionCheck,
|
|
206
|
+
/can read that repository but not write/,
|
|
207
|
+
],
|
|
208
|
+
[
|
|
209
|
+
'GitHub cannot be reached',
|
|
210
|
+
{ outcome: 'rejected', reason: 'unreachable', field: 'kbRepoUrl', error: 'x' } as ConnectionCheck,
|
|
211
|
+
/could not be reached/,
|
|
212
|
+
],
|
|
213
|
+
])('says so when %s', async (_why, github, said) => {
|
|
214
|
+
const { save } = listen({ github });
|
|
215
|
+
const { status, body } = await save(CHOICE);
|
|
216
|
+
expect(status).toBe(400);
|
|
217
|
+
expect(body.problems?.githubRepository).toMatch(said);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('asks for the connection before a repository can be chosen', async () => {
|
|
221
|
+
const { save, probed } = listen({ installed: false });
|
|
222
|
+
const { status, body } = await save(CHOICE);
|
|
223
|
+
expect(status).toBe(400);
|
|
224
|
+
expect(body.problems).toEqual({ githubRepository: 'Connect GitHub before choosing a repository.' });
|
|
225
|
+
expect(probed).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('asks for the repository when only the way was chosen', async () => {
|
|
229
|
+
const { save } = listen();
|
|
230
|
+
const { status, body } = await save({ gitMode: 'github-app' });
|
|
231
|
+
expect(status).toBe(400);
|
|
232
|
+
expect(body.problems).toEqual({ githubRepository: 'Choose the repository the knowledge base lives in.' });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('says the app may have been uninstalled when GitHub gives no token', async () => {
|
|
236
|
+
const { save, probed } = listen({ tokenGiven: false });
|
|
237
|
+
const { status, body } = await save(CHOICE);
|
|
238
|
+
expect(status).toBe(400);
|
|
239
|
+
expect(body.problems?.gitMode).toMatch(/may have been uninstalled/);
|
|
240
|
+
expect(probed).toEqual([]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('refuses the way on a deployment that does not offer it', async () => {
|
|
244
|
+
const { save } = listen({ offered: false });
|
|
245
|
+
const { status, body } = await save(CHOICE);
|
|
246
|
+
expect(status).toBe(400);
|
|
247
|
+
expect(body.problems?.gitMode).toMatch(/cannot connect to GitHub/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('refuses a name that is not a repository', async () => {
|
|
251
|
+
const { save, probed } = listen();
|
|
252
|
+
for (const name of ['acme', 'acme/kb.git', '../acme/kb', 'https://github.com/acme/kb', 'acme/kb/extra']) {
|
|
253
|
+
const { status, body } = await save({ gitMode: 'github-app', githubRepository: name });
|
|
254
|
+
expect(status, name).toBe(400);
|
|
255
|
+
expect(body.problems?.githubRepository, name).toBe('Name the repository as owner/name.');
|
|
256
|
+
}
|
|
257
|
+
expect(probed).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('a deployment on GitHub that is serving', () => {
|
|
262
|
+
async function serving() {
|
|
263
|
+
const mounted = listen({ kb: testKbContext() });
|
|
264
|
+
await mounted.save({ ...CHOICE, defaultBranch: 'main', protectedBranches: 'main' });
|
|
265
|
+
return mounted;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it('is not asked about its repository by a save that changes something else', async () => {
|
|
269
|
+
const { save, probed } = await serving();
|
|
270
|
+
const asked = probed.length;
|
|
271
|
+
expect((await save({ kbSyncSecret: 'a-secret-of-sixteen-or-more' })).status).toBe(200);
|
|
272
|
+
expect(probed).toHaveLength(asked);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Another repository on GitHub is a move like any other: GitHub is asked
|
|
277
|
+
* about the new one, the admin is asked about the move, and confirmed it
|
|
278
|
+
* happens on the save.
|
|
279
|
+
*/
|
|
280
|
+
it('proves the new repository, asks about the move, and moves on the save once confirmed', async () => {
|
|
281
|
+
const { save, probed, source, phaseRuns } = await serving();
|
|
282
|
+
const before = probed.length;
|
|
283
|
+
const asked = await save({ githubRepository: 'acme/another' });
|
|
284
|
+
expect(asked.status).toBe(409);
|
|
285
|
+
expect(asked.body).toMatchObject({ repositoryChange: { from: 'github-app', to: 'github-app' } });
|
|
286
|
+
// The connection is proven before the question is put: nobody agrees to
|
|
287
|
+
// a move only to be told the repository cannot be reached.
|
|
288
|
+
expect(probed).toHaveLength(before + 1);
|
|
289
|
+
expect(probed.at(-1)!.url).toBe('https://github.com/acme/another.git');
|
|
290
|
+
expect(source.url()).toBe('https://github.com/acme/kb.git');
|
|
291
|
+
|
|
292
|
+
const runs = phaseRuns();
|
|
293
|
+
const moved = await save({ githubRepository: 'acme/another' }, 'keep');
|
|
294
|
+
expect(moved.status, JSON.stringify(moved.body)).toBe(200);
|
|
295
|
+
expect(moved.body).toMatchObject({ ok: true, restartRequired: false, repositoryChange: { choice: 'keep' } });
|
|
296
|
+
expect(source.url()).toBe('https://github.com/acme/another.git');
|
|
297
|
+
expect(phaseRuns()).toBe(runs + 1);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import type { Server as HttpServer } from 'node:http';
|
|
4
|
+
import type { AddressInfo } from 'node:net';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import express from 'express';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
10
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
11
|
+
import { createSetupRoutes } from '../setup.routes.js';
|
|
12
|
+
import { DeploymentSettingsService } from '../deployment-settings.service.js';
|
|
13
|
+
import { ManagedRepository } from '../managed-repository.js';
|
|
14
|
+
import { RepositorySource } from '../repository-source.js';
|
|
15
|
+
import { KbStartupRunner } from '../../workspace/startup/kb-startup-runner.js';
|
|
16
|
+
import { NodeGitRunner } from '../../workflow/git/node-git-runner.js';
|
|
17
|
+
import { TemplateFilesStep } from '../../workspace/startup/steps/template-files.step.js';
|
|
18
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
19
|
+
import { defaultKbTemplateDir } from '../../../assets.js';
|
|
20
|
+
import type { Database } from '../../database/connection.js';
|
|
21
|
+
import type { IAdminAccessService } from '../../admin/admin.interface.js';
|
|
22
|
+
import type { KbContext } from '../../../shared/kb-context.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A repository the deployment keeps, and the REAL startup phase, wired the
|
|
26
|
+
* way the composition root wires them: the phase asks the repository source
|
|
27
|
+
* where the repository is, and git runs with what the source presents.
|
|
28
|
+
*
|
|
29
|
+
* A fake phase proves the routes chose the right address. It cannot see
|
|
30
|
+
* whether a bare repository on a disk is something the phase can seed,
|
|
31
|
+
* clone and push to, or what happens to the working copies of a repository
|
|
32
|
+
* the deployment has moved away from. This looks at the disk afterwards.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const execFileAsync = promisify(execFile);
|
|
36
|
+
const ENC_KEY = 'kToAi8FXWDpDn3A6yQ/60O39bv05N7XzVOIu/0CJrFc=';
|
|
37
|
+
const KB_ENV = ['KB_REPO_URL', 'GIT_TOKEN', 'GIT_USERNAME', 'GIT_MODE', 'GITHUB_TOKEN', 'DEFAULT_BRANCH', 'PROTECTED_BRANCHES'] as const;
|
|
38
|
+
|
|
39
|
+
const gitEnv = {
|
|
40
|
+
...process.env,
|
|
41
|
+
GIT_AUTHOR_NAME: 'Test',
|
|
42
|
+
GIT_AUTHOR_EMAIL: 't@x.com',
|
|
43
|
+
GIT_COMMITTER_NAME: 'Test',
|
|
44
|
+
GIT_COMMITTER_EMAIL: 't@x.com',
|
|
45
|
+
};
|
|
46
|
+
const git = async (cwd: string, args: string[]) =>
|
|
47
|
+
(await execFileAsync('git', args, { cwd, env: gitEnv })).stdout.toString().trim();
|
|
48
|
+
|
|
49
|
+
let root: string;
|
|
50
|
+
let server: HttpServer | null = null;
|
|
51
|
+
let savedEnv: Partial<Record<(typeof KB_ENV)[number], string | undefined>> = {};
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
savedEnv = {};
|
|
55
|
+
for (const k of KB_ENV) {
|
|
56
|
+
savedEnv[k] = process.env[k];
|
|
57
|
+
delete process.env[k];
|
|
58
|
+
}
|
|
59
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'setup-managed-phase-'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
server?.close();
|
|
64
|
+
server = null;
|
|
65
|
+
for (const k of KB_ENV) {
|
|
66
|
+
const original = savedEnv[k];
|
|
67
|
+
if (original === undefined) delete process.env[k];
|
|
68
|
+
else process.env[k] = original;
|
|
69
|
+
}
|
|
70
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** A deployment: its settings, its repository source, the real phase, and the setup routes over them. */
|
|
74
|
+
function boot(kb: KbContext) {
|
|
75
|
+
const db = {
|
|
76
|
+
select: () => ({ from: () => Promise.resolve([]) }),
|
|
77
|
+
insert: () => ({ values: () => ({ onConflictDoUpdate: () => Promise.resolve() }) }),
|
|
78
|
+
delete: () => ({ where: () => Promise.resolve() }),
|
|
79
|
+
} as unknown as Database;
|
|
80
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
81
|
+
const managed = new ManagedRepository(path.join(root, 'backups', 'managed-repository'));
|
|
82
|
+
// A deployment reaching a repository by its address has one on a host; here
|
|
83
|
+
// it is a folder, which the source hands over as it would hand a URL.
|
|
84
|
+
const hosted = path.join(root, 'hosted.git');
|
|
85
|
+
const source = new RepositorySource({
|
|
86
|
+
read: (key) => (key === 'kbRepoUrl' && settings.resolve('kbRepoUrl') ? hosted : settings.resolve(key)),
|
|
87
|
+
managed,
|
|
88
|
+
});
|
|
89
|
+
const gitRunner = new NodeGitRunner(60_000, source.credentials);
|
|
90
|
+
const workspacesRoot = path.join(root, 'workspaces');
|
|
91
|
+
const setAsideRoot = path.join(root, 'backups', 'replaced-working-copies');
|
|
92
|
+
const runner = new KbStartupRunner({
|
|
93
|
+
gitRunner,
|
|
94
|
+
kbRepoUrl: () => source.url(),
|
|
95
|
+
workspacesRoot,
|
|
96
|
+
setAsideRoot,
|
|
97
|
+
kbDirName: 'knowledge-base',
|
|
98
|
+
templateDir: defaultKbTemplateDir(),
|
|
99
|
+
defaultBranch: () => kb.defaultBranch,
|
|
100
|
+
protectedBranches: () => [...kb.protectedBranches],
|
|
101
|
+
seedAdminEmails: ['admin@example.com'],
|
|
102
|
+
// A step that works on every branch, so the phase makes each its working copy.
|
|
103
|
+
steps: [new TemplateFilesStep(new NodeFs(), kb)],
|
|
104
|
+
buildSeedTree: async (dir) => {
|
|
105
|
+
await fs.writeFile(path.join(dir, 'WELCOME.md'), 'seeded\n', 'utf8');
|
|
106
|
+
return ['WELCOME.md'];
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const app = express();
|
|
110
|
+
app.use(express.json());
|
|
111
|
+
app.use((req, _res, next) => {
|
|
112
|
+
req.userEmail = 'root@example.com';
|
|
113
|
+
req.userId = 'user-1';
|
|
114
|
+
next();
|
|
115
|
+
});
|
|
116
|
+
app.use(
|
|
117
|
+
'/api',
|
|
118
|
+
createSetupRoutes(
|
|
119
|
+
settings,
|
|
120
|
+
{ isAdmin: async () => true } as IAdminAccessService,
|
|
121
|
+
runner,
|
|
122
|
+
kb,
|
|
123
|
+
undefined,
|
|
124
|
+
async () => ({ outcome: 'connected', branches: ['main'], defaultBranch: 'main', empty: false }),
|
|
125
|
+
undefined,
|
|
126
|
+
undefined,
|
|
127
|
+
undefined,
|
|
128
|
+
undefined,
|
|
129
|
+
{ source, ensureManaged: (branch) => managed.ensure(gitRunner, branch) },
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
server = app.listen(0);
|
|
133
|
+
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
134
|
+
/** `confirm` is the admin's answer to the question a move is asked. */
|
|
135
|
+
const save = async (entries: Record<string, string>, confirm?: 'keep' | 'close') => {
|
|
136
|
+
const res = await fetch(`${base}/api/setup/settings`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'content-type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ settings: entries, ...(confirm ? { confirmRepositoryChange: confirm } : {}) }),
|
|
140
|
+
});
|
|
141
|
+
return { status: res.status, body: (await res.json()) as Record<string, unknown> };
|
|
142
|
+
};
|
|
143
|
+
const workingCopy = (branch: string) => path.join(workspacesRoot, encodeURIComponent(branch), 'knowledge-base');
|
|
144
|
+
return { save, managed, hosted, runner, source, workingCopy, setAsideRoot };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe('the save that chooses a repository the deployment keeps', () => {
|
|
148
|
+
it('creates it, seeds it and clones from it, asking for nothing else', async () => {
|
|
149
|
+
const { save, managed, workingCopy } = boot(testKbContext({ branchModel: null }));
|
|
150
|
+
const { status, body } = await save({ gitMode: 'managed' });
|
|
151
|
+
expect(status, JSON.stringify(body)).toBe(200);
|
|
152
|
+
expect(body).toMatchObject({ complete: true, restartRequired: false });
|
|
153
|
+
|
|
154
|
+
// The repository is where the source says, bare, and holds the seed on the branch named for it.
|
|
155
|
+
expect(await git(managed.path, ['rev-parse', '--is-bare-repository'])).toBe('true');
|
|
156
|
+
expect(await git(managed.path, ['for-each-ref', '--format=%(refname)'])).toBe('refs/heads/main');
|
|
157
|
+
expect(await git(managed.path, ['ls-tree', '-r', '--name-only', 'main'])).toContain('WELCOME.md');
|
|
158
|
+
|
|
159
|
+
// The working copy is a clone of it, and carries no credential helper: there is no credential.
|
|
160
|
+
const copy = workingCopy('main');
|
|
161
|
+
expect(await git(copy, ['config', '--get', 'remote.origin.url'])).toBe(managed.path);
|
|
162
|
+
expect((await fs.readFile(path.join(copy, 'WELCOME.md'), 'utf8')).replace(/\r\n/g, '\n')).toBe('seeded\n');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('takes a push from a working copy, like any remote', async () => {
|
|
166
|
+
const { save, managed, workingCopy } = boot(testKbContext({ branchModel: null }));
|
|
167
|
+
await save({ gitMode: 'managed' });
|
|
168
|
+
const copy = workingCopy('main');
|
|
169
|
+
await fs.writeFile(path.join(copy, 'note.md'), 'a change\n', 'utf8');
|
|
170
|
+
await git(copy, ['add', '-A']);
|
|
171
|
+
await git(copy, ['commit', '-m', 'a change']);
|
|
172
|
+
await git(copy, ['push', 'origin', 'main']);
|
|
173
|
+
expect(await git(managed.path, ['ls-tree', '-r', '--name-only', 'main'])).toContain('note.md');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('a deployment that moves to a repository it keeps', () => {
|
|
178
|
+
it('leaves the repository it had untouched and sets its working copies aside, unpushed work included', async () => {
|
|
179
|
+
const { save, managed, hosted, source, workingCopy, setAsideRoot } = boot(
|
|
180
|
+
testKbContext({ branchModel: { defaultBranch: 'main', protectedBranches: ['main'] } }),
|
|
181
|
+
);
|
|
182
|
+
// A deployment on a repository of its own, set up and serving. The
|
|
183
|
+
// repository has a history of ITS OWN, written here and not by the
|
|
184
|
+
// startup phase: two empty repositories the phase seeds within the
|
|
185
|
+
// same second get the same first commit (same tree, same author, same
|
|
186
|
+
// time), and the phase would then read them, rightly, as one repository
|
|
187
|
+
// at two addresses, and keep the working copy instead of setting it
|
|
188
|
+
// aside. A fast machine did exactly that.
|
|
189
|
+
await git(root, ['init', '--bare', '-b', 'main', hosted]);
|
|
190
|
+
const theirs = path.join(root, 'theirs');
|
|
191
|
+
await fs.mkdir(theirs);
|
|
192
|
+
await git(theirs, ['init', '-b', 'main']);
|
|
193
|
+
await fs.writeFile(path.join(theirs, 'HANDBOOK.md'), 'what this organisation had before\n', 'utf8');
|
|
194
|
+
await git(theirs, ['add', '-A']);
|
|
195
|
+
await git(theirs, ['commit', '-m', 'the handbook']);
|
|
196
|
+
await git(theirs, ['push', hosted, 'main']);
|
|
197
|
+
const first = await save({ kbRepoUrl: 'https://git.example.com/acme/kb.git', gitToken: 'a-token', defaultBranch: 'main', protectedBranches: 'main' });
|
|
198
|
+
expect(first.status, JSON.stringify(first.body)).toBe(200);
|
|
199
|
+
const copy = workingCopy('main');
|
|
200
|
+
expect(await git(copy, ['config', '--get', 'remote.origin.url'])).toBe(hosted);
|
|
201
|
+
// Work that was committed and never pushed.
|
|
202
|
+
await fs.writeFile(path.join(copy, 'unpushed.md'), 'only here\n', 'utf8');
|
|
203
|
+
await git(copy, ['add', '-A']);
|
|
204
|
+
await git(copy, ['commit', '-m', 'never pushed']);
|
|
205
|
+
const hostedBefore = await git(hosted, ['for-each-ref']);
|
|
206
|
+
|
|
207
|
+
// Asked first: until the admin answers, the working copy is where it
|
|
208
|
+
// was and so is the deployment.
|
|
209
|
+
const asked = await save({ gitMode: 'managed' });
|
|
210
|
+
expect(asked.status).toBe(409);
|
|
211
|
+
expect(await git(copy, ['config', '--get', 'remote.origin.url'])).toBe(hosted);
|
|
212
|
+
expect(source.url()).toBe(hosted);
|
|
213
|
+
|
|
214
|
+
// Confirmed, the move happens on the save, with no restart: the way
|
|
215
|
+
// chosen takes effect and the phase brings the working copies into line.
|
|
216
|
+
const moved = await save({ gitMode: 'managed' }, 'keep');
|
|
217
|
+
expect(moved.status, JSON.stringify(moved.body)).toBe(200);
|
|
218
|
+
expect(moved.body).toMatchObject({ ok: true, complete: true, restartRequired: false });
|
|
219
|
+
expect(source.url()).toBe(managed.path);
|
|
220
|
+
|
|
221
|
+
expect(await git(hosted, ['for-each-ref'])).toBe(hostedBefore);
|
|
222
|
+
expect(await git(copy, ['config', '--get', 'remote.origin.url'])).toBe(managed.path);
|
|
223
|
+
expect(await fs.access(path.join(copy, 'unpushed.md')).then(() => true, () => false)).toBe(false);
|
|
224
|
+
const managedHolds = await git(managed.path, ['ls-tree', '-r', '--name-only', 'main']);
|
|
225
|
+
expect(managedHolds).toContain('WELCOME.md');
|
|
226
|
+
// Nothing of one repository was pushed into the other.
|
|
227
|
+
expect(managedHolds).not.toContain('HANDBOOK.md');
|
|
228
|
+
expect(managedHolds).not.toContain('unpushed.md');
|
|
229
|
+
|
|
230
|
+
// What was set aside is whole: the commit nobody pushed is in it.
|
|
231
|
+
const [stamp] = await fs.readdir(setAsideRoot);
|
|
232
|
+
const kept = path.join(setAsideRoot, stamp!, encodeURIComponent('main'));
|
|
233
|
+
expect(await git(kept, ['log', '-1', '--format=%s'])).toBe('never pushed');
|
|
234
|
+
expect((await fs.readFile(path.join(kept, 'unpushed.md'), 'utf8')).replace(/\r\n/g, '\n')).toBe('only here\n');
|
|
235
|
+
});
|
|
236
|
+
});
|