@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,390 @@
|
|
|
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 { createSetupRoutes, type RepositoryChangeRequests } from '../setup.routes.js';
|
|
7
|
+
import { DeploymentSettingsService } from '../deployment-settings.service.js';
|
|
8
|
+
import type { Database } from '../../database/connection.js';
|
|
9
|
+
import type { IAdminAccessService } from '../../admin/admin.interface.js';
|
|
10
|
+
import type { ConnectionCheck, RepositoryConnection } from '../connection-check.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Saving a DIFFERENT knowledge-base repository.
|
|
14
|
+
*
|
|
15
|
+
* Every working copy on this server fetches through the address stored in its
|
|
16
|
+
* own clone, so pointing the deployment somewhere else means deleting and
|
|
17
|
+
* re-cloning all of them — and losing anything committed here and never
|
|
18
|
+
* pushed. On 2026-09-28 that happened with no warning, no re-clone, and a
|
|
19
|
+
* boot that then crash-looped against the repository that was gone.
|
|
20
|
+
*
|
|
21
|
+
* So: the save is REFUSED until the admin confirms, the confirmation says how
|
|
22
|
+
* many change requests are open and what to do with them, and a confirmed
|
|
23
|
+
* save runs the KB startup phase — which is what replaces the working copies
|
|
24
|
+
* — even though the deployment was complete and healthy.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const ENC_KEY = 'kToAi8FXWDpDn3A6yQ/60O39bv05N7XzVOIu/0CJrFc=';
|
|
28
|
+
const KB_ENV = ['KB_REPO_URL', 'GIT_TOKEN', 'GIT_USERNAME', 'KB_DIR_NAME', 'GITHUB_TOKEN'] as const;
|
|
29
|
+
const REPO = 'https://example.com/acme/kb.git';
|
|
30
|
+
|
|
31
|
+
let server: HttpServer | null = null;
|
|
32
|
+
let savedEnv: Partial<Record<(typeof KB_ENV)[number], string | undefined>> = {};
|
|
33
|
+
/** Set by the one test about a close the database will not do. Reset per test. */
|
|
34
|
+
let failTheClose = false;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
failTheClose = false;
|
|
38
|
+
savedEnv = {};
|
|
39
|
+
for (const k of KB_ENV) {
|
|
40
|
+
savedEnv[k] = process.env[k];
|
|
41
|
+
delete process.env[k];
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
server?.close();
|
|
47
|
+
server = null;
|
|
48
|
+
for (const k of KB_ENV) {
|
|
49
|
+
const original = savedEnv[k];
|
|
50
|
+
if (original === undefined) delete process.env[k];
|
|
51
|
+
else process.env[k] = original;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** The remote answers for whatever it is asked about. */
|
|
56
|
+
const connected = async (connection: RepositoryConnection): Promise<ConnectionCheck> => {
|
|
57
|
+
void connection;
|
|
58
|
+
return { outcome: 'connected', branches: ['main'], defaultBranch: 'main', empty: false };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function listen(openChangeRequests: number) {
|
|
62
|
+
const db = {
|
|
63
|
+
select: () => ({ from: () => Promise.resolve([]) }),
|
|
64
|
+
insert: () => ({ values: () => ({ onConflictDoUpdate: () => Promise.resolve() }) }),
|
|
65
|
+
delete: () => ({ where: () => Promise.resolve() }),
|
|
66
|
+
} as unknown as Database;
|
|
67
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
68
|
+
/** Every run of the KB startup phase — the thing that replaces the clones. */
|
|
69
|
+
const phaseRuns = { count: 0 };
|
|
70
|
+
const closes = { count: 0 };
|
|
71
|
+
/** Set by the one test about a move whose startup phase fails. */
|
|
72
|
+
const phase = { fail: false };
|
|
73
|
+
/** What happened, in order: the commit worker held and let go, and the phase between. */
|
|
74
|
+
const events: string[] = [];
|
|
75
|
+
const changeRequests: RepositoryChangeRequests = {
|
|
76
|
+
async countOpen() {
|
|
77
|
+
return openChangeRequests;
|
|
78
|
+
},
|
|
79
|
+
async closeAsRepositoryReplaced() {
|
|
80
|
+
closes.count += 1;
|
|
81
|
+
if (failTheClose) throw new Error('change_requests is down');
|
|
82
|
+
return openChangeRequests;
|
|
83
|
+
},
|
|
84
|
+
async whileCommitsHeld(work) {
|
|
85
|
+
// What was stored at the moment the hold began: the hold must come first.
|
|
86
|
+
events.push(`commits held on ${settings.resolve('kbRepoUrl')}`);
|
|
87
|
+
try {
|
|
88
|
+
return await work();
|
|
89
|
+
} finally {
|
|
90
|
+
events.push('commits released');
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const app = express();
|
|
95
|
+
app.use(express.json());
|
|
96
|
+
app.use((req, _res, next) => {
|
|
97
|
+
req.userEmail = 'root@example.com';
|
|
98
|
+
req.userId = 'user-1';
|
|
99
|
+
next();
|
|
100
|
+
});
|
|
101
|
+
app.use(
|
|
102
|
+
'/api',
|
|
103
|
+
createSetupRoutes(
|
|
104
|
+
settings,
|
|
105
|
+
{ isAdmin: async () => true } as IAdminAccessService,
|
|
106
|
+
{
|
|
107
|
+
async runAll() {
|
|
108
|
+
phaseRuns.count += 1;
|
|
109
|
+
events.push('phase');
|
|
110
|
+
if (phase.fail) throw new Error('git fetch failed: repository not found');
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
testKbContext(),
|
|
114
|
+
undefined,
|
|
115
|
+
connected,
|
|
116
|
+
undefined,
|
|
117
|
+
undefined,
|
|
118
|
+
undefined,
|
|
119
|
+
undefined,
|
|
120
|
+
// No alternative way of being given a repository: this suite is about the
|
|
121
|
+
// one that has always been there, an address and a token.
|
|
122
|
+
undefined,
|
|
123
|
+
changeRequests,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
server = app.listen(0);
|
|
127
|
+
const { port } = server.address() as AddressInfo;
|
|
128
|
+
return { base: `http://127.0.0.1:${port}`, settings, phaseRuns, closes, events, phase };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const post = (base: string, body: unknown) =>
|
|
132
|
+
fetch(`${base}/api/setup/settings`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'content-type': 'application/json' },
|
|
135
|
+
body: JSON.stringify(body),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/** A deployment that has been running against REPO for a while. */
|
|
139
|
+
async function configured(openChangeRequests = 0) {
|
|
140
|
+
const harness = listen(openChangeRequests);
|
|
141
|
+
await harness.settings.save({ kbRepoUrl: REPO, gitToken: 'ghp_old' }, null);
|
|
142
|
+
// The save above is not what this suite counts.
|
|
143
|
+
harness.phaseRuns.count = 0;
|
|
144
|
+
return harness;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe('a save that changes the knowledge-base repository', () => {
|
|
148
|
+
it('is refused until the admin confirms, and stores nothing meanwhile', async () => {
|
|
149
|
+
const { base, settings, phaseRuns } = await configured(0);
|
|
150
|
+
|
|
151
|
+
const res = await post(base, {
|
|
152
|
+
settings: { kbRepoUrl: 'https://example.com/acme/replacement.git', gitToken: 'ghp_new' },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(res.status).toBe(409);
|
|
156
|
+
expect(await res.json()).toMatchObject({ repositoryChange: { openChangeRequests: 0 } });
|
|
157
|
+
// Nothing saved, nothing replaced: a refused save destroys nothing.
|
|
158
|
+
expect(settings.resolve('kbRepoUrl')).toBe(REPO);
|
|
159
|
+
expect(phaseRuns.count).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('says how many change requests are open, so the choice is about a number', async () => {
|
|
163
|
+
const { base } = await configured(3);
|
|
164
|
+
const res = await post(base, {
|
|
165
|
+
settings: { kbRepoUrl: 'https://example.com/acme/replacement.git', gitToken: 'ghp_new' },
|
|
166
|
+
});
|
|
167
|
+
expect(res.status).toBe(409);
|
|
168
|
+
expect((await res.json()).repositoryChange.openChangeRequests).toBe(3);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('confirmed with "keep": saves, replaces the working copies, and closes nothing', async () => {
|
|
172
|
+
const { base, settings, phaseRuns, closes } = await configured(3);
|
|
173
|
+
const replacement = 'https://example.com/acme/replacement.git';
|
|
174
|
+
|
|
175
|
+
const res = await post(base, {
|
|
176
|
+
settings: { kbRepoUrl: replacement, gitToken: 'ghp_new' },
|
|
177
|
+
confirmRepositoryChange: 'keep',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(res.status).toBe(200);
|
|
181
|
+
expect(await res.json()).toMatchObject({
|
|
182
|
+
repositoryChange: { choice: 'keep', closedChangeRequests: 0 },
|
|
183
|
+
});
|
|
184
|
+
expect(settings.resolve('kbRepoUrl')).toBe(replacement);
|
|
185
|
+
// The phase is what deletes the clones of the old repository and makes new
|
|
186
|
+
// ones — on the SAVE, not only at the next boot.
|
|
187
|
+
expect(phaseRuns.count).toBe(1);
|
|
188
|
+
expect(closes.count).toBe(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('confirmed with "close": closes the open requests as repository replaced', async () => {
|
|
192
|
+
const { base, phaseRuns, closes } = await configured(3);
|
|
193
|
+
|
|
194
|
+
const res = await post(base, {
|
|
195
|
+
settings: { kbRepoUrl: 'https://example.com/acme/replacement.git', gitToken: 'ghp_new' },
|
|
196
|
+
confirmRepositoryChange: 'close',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(res.status).toBe(200);
|
|
200
|
+
expect(await res.json()).toMatchObject({
|
|
201
|
+
repositoryChange: { choice: 'close', closedChangeRequests: 3 },
|
|
202
|
+
});
|
|
203
|
+
expect(closes.count).toBe(1);
|
|
204
|
+
expect(phaseRuns.count).toBe(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A close that cannot be done is a PRECONDITION that failed, not a detail of
|
|
209
|
+
* an otherwise successful save. Stored anyway, the address would now match,
|
|
210
|
+
* so the next save would see no change, ask nothing, and never retry the
|
|
211
|
+
* close — leaving those requests open on branches the new repository does not
|
|
212
|
+
* have, their file locks refusing paths to everyone, with nothing the admin
|
|
213
|
+
* could press to fix it.
|
|
214
|
+
*/
|
|
215
|
+
it('refuses the whole save, saying why, when the requests cannot be closed', async () => {
|
|
216
|
+
const { base, settings, phaseRuns } = await configured(3);
|
|
217
|
+
failTheClose = true;
|
|
218
|
+
|
|
219
|
+
const res = await post(base, {
|
|
220
|
+
settings: { kbRepoUrl: 'https://example.com/acme/replacement.git', gitToken: 'ghp_new' },
|
|
221
|
+
confirmRepositoryChange: 'close',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(res.status).toBe(500);
|
|
225
|
+
expect((await res.json()).error).toMatch(/could not be closed/);
|
|
226
|
+
// Nothing stored, no working copy touched: the admin can press Save again.
|
|
227
|
+
expect(settings.resolve('kbRepoUrl')).toBe(REPO);
|
|
228
|
+
expect(phaseRuns.count).toBe(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* An answer stands for the count it was given about. The screen sends "keep"
|
|
234
|
+
* when there was nothing to choose; a request opened while the question
|
|
235
|
+
* stood must not be decided by that.
|
|
236
|
+
*/
|
|
237
|
+
describe('an answer given about a count that has since changed', () => {
|
|
238
|
+
it('is asked again, with the count as it is now', async () => {
|
|
239
|
+
const { base, settings, phaseRuns } = await configured(1);
|
|
240
|
+
const res = await post(base, {
|
|
241
|
+
settings: { kbRepoUrl: 'https://example.com/acme/replacement.git', gitToken: 'ghp_new' },
|
|
242
|
+
confirmRepositoryChange: 'keep',
|
|
243
|
+
seenOpenChangeRequests: 0,
|
|
244
|
+
});
|
|
245
|
+
expect(res.status).toBe(409);
|
|
246
|
+
expect((await res.json()).repositoryChange.openChangeRequests).toBe(1);
|
|
247
|
+
expect(settings.resolve('kbRepoUrl')).toBe(REPO);
|
|
248
|
+
expect(phaseRuns.count).toBe(0);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('goes through when the count is the one the answer was given about', async () => {
|
|
252
|
+
const { base } = await configured(1);
|
|
253
|
+
const res = await post(base, {
|
|
254
|
+
settings: { kbRepoUrl: 'https://example.com/acme/replacement.git', gitToken: 'ghp_new' },
|
|
255
|
+
confirmRepositoryChange: 'keep',
|
|
256
|
+
seenOpenChangeRequests: 1,
|
|
257
|
+
});
|
|
258
|
+
expect(res.status).toBe(200);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A move that fails part-way leaves the deployment gated with the failure
|
|
264
|
+
* standing. It has served all the same, and its open change requests are the
|
|
265
|
+
* proof: the save that tries again must still ask what becomes of them.
|
|
266
|
+
*/
|
|
267
|
+
describe('a move tried again after one that failed part-way', () => {
|
|
268
|
+
it('still asks, because change requests are open', async () => {
|
|
269
|
+
const { base, phase } = await configured(2);
|
|
270
|
+
phase.fail = true;
|
|
271
|
+
const failed = await post(base, {
|
|
272
|
+
settings: { kbRepoUrl: 'https://example.com/acme/replacement.git', gitToken: 'ghp_new' },
|
|
273
|
+
confirmRepositoryChange: 'keep',
|
|
274
|
+
});
|
|
275
|
+
expect(failed.status).toBe(500);
|
|
276
|
+
expect(await failed.json()).toHaveProperty('kbInit');
|
|
277
|
+
|
|
278
|
+
phase.fail = false;
|
|
279
|
+
const again = await post(base, {
|
|
280
|
+
settings: { kbRepoUrl: 'https://example.com/acme/third.git', gitToken: 'ghp_new' },
|
|
281
|
+
});
|
|
282
|
+
expect(again.status).toBe(409);
|
|
283
|
+
expect((await again.json()).repositoryChange.openChangeRequests).toBe(2);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('a save that does NOT change the repository', () => {
|
|
288
|
+
it('goes straight through, and runs no startup phase', async () => {
|
|
289
|
+
const { base, phaseRuns, closes } = await configured(2);
|
|
290
|
+
|
|
291
|
+
// The same address, with something else edited beside it.
|
|
292
|
+
const res = await post(base, { settings: { kbRepoUrl: REPO, gitUsername: 'oauth2' } });
|
|
293
|
+
|
|
294
|
+
expect(res.status).toBe(200);
|
|
295
|
+
expect(await res.json()).not.toHaveProperty('repositoryChange');
|
|
296
|
+
// A complete, healthy deployment's clones are of the configured
|
|
297
|
+
// repository: re-cloning them would be maintenance over live work.
|
|
298
|
+
expect(phaseRuns.count).toBe(0);
|
|
299
|
+
expect(closes.count).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('treats a trailing slash, a .git suffix and the host’s letter case as the same address', async () => {
|
|
303
|
+
for (const spelling of [
|
|
304
|
+
'https://example.com/acme/kb',
|
|
305
|
+
'https://example.com/acme/kb/',
|
|
306
|
+
'https://EXAMPLE.com/acme/kb.git',
|
|
307
|
+
'https://example.com/acme/kb.git/',
|
|
308
|
+
]) {
|
|
309
|
+
const { base, settings, phaseRuns } = await configured(2);
|
|
310
|
+
const res = await post(base, { settings: { kbRepoUrl: spelling } });
|
|
311
|
+
expect({ spelling, status: res.status }).toEqual({ spelling, status: 200 });
|
|
312
|
+
// Stored as typed — it is the same repository, not a correction.
|
|
313
|
+
expect(settings.resolve('kbRepoUrl')).toBe(spelling);
|
|
314
|
+
expect({ spelling, runs: phaseRuns.count }).toEqual({ spelling, runs: 0 });
|
|
315
|
+
server?.close();
|
|
316
|
+
server = null;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('never asks the token question over a spelling, since it is the same repository', async () => {
|
|
321
|
+
const { base } = await configured(0);
|
|
322
|
+
// No token supplied. A DIFFERENT repository would be refused here — the
|
|
323
|
+
// stored token only ever goes to the repository it was saved for.
|
|
324
|
+
const res = await post(base, { settings: { kbRepoUrl: 'https://example.com/acme/kb/' } });
|
|
325
|
+
expect(res.status).toBe(200);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('first-run setup', () => {
|
|
330
|
+
it('asks nothing: there is no configured address and no working copy to lose', async () => {
|
|
331
|
+
const { base, settings, phaseRuns } = listen(0);
|
|
332
|
+
const res = await post(base, { settings: { kbRepoUrl: REPO, gitToken: 'ghp_new' } });
|
|
333
|
+
expect(res.status).toBe(200);
|
|
334
|
+
expect(settings.resolve('kbRepoUrl')).toBe(REPO);
|
|
335
|
+
// The completion transition runs the phase, as it always has.
|
|
336
|
+
expect(phaseRuns.count).toBe(1);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The move runs while the deployment is serving. A commit worker left
|
|
342
|
+
* running would write into a working copy that is being renamed away, or
|
|
343
|
+
* into the fresh clone of a repository the commit was never meant for.
|
|
344
|
+
*/
|
|
345
|
+
describe('the commit worker, while a deployment is moved', () => {
|
|
346
|
+
/**
|
|
347
|
+
* Held BEFORE the new address is stored, not only around the phase. The
|
|
348
|
+
* way chosen takes effect with the store, and a worker still running in
|
|
349
|
+
* between would push a queued commit from a copy of the repository being
|
|
350
|
+
* left with the credential of the one moved to.
|
|
351
|
+
*/
|
|
352
|
+
it('is held from before the address is stored until the startup phase has run', async () => {
|
|
353
|
+
const { base, events } = await configured(0);
|
|
354
|
+
events.length = 0;
|
|
355
|
+
const res = await post(base, {
|
|
356
|
+
settings: { kbRepoUrl: 'https://example.com/acme/another.git', gitToken: 'ghp_new' },
|
|
357
|
+
confirmRepositoryChange: 'keep',
|
|
358
|
+
});
|
|
359
|
+
expect(res.status).toBe(200);
|
|
360
|
+
expect(events).toEqual([`commits held on ${REPO}`, 'phase', 'commits released']);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('is let go when the confirmed move is refused after all', async () => {
|
|
364
|
+
const { base, events } = await configured(2);
|
|
365
|
+
events.length = 0;
|
|
366
|
+
failTheClose = true;
|
|
367
|
+
const res = await post(base, {
|
|
368
|
+
settings: { kbRepoUrl: 'https://example.com/acme/another.git', gitToken: 'ghp_new' },
|
|
369
|
+
confirmRepositoryChange: 'close',
|
|
370
|
+
});
|
|
371
|
+
expect(res.status).toBe(500);
|
|
372
|
+
expect(events).toEqual([`commits held on ${REPO}`, 'commits released']);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('is not touched by a save that moves nothing', async () => {
|
|
376
|
+
const { base, events } = await configured(0);
|
|
377
|
+
events.length = 0;
|
|
378
|
+
const res = await post(base, { settings: { kbSyncSecret: 'a-secret-of-sixteen-or-more' } });
|
|
379
|
+
expect(res.status).toBe(200);
|
|
380
|
+
expect(events).toEqual([]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('is not held by a move that was only asked about', async () => {
|
|
384
|
+
const { base, events } = await configured(0);
|
|
385
|
+
events.length = 0;
|
|
386
|
+
const res = await post(base, { settings: { kbRepoUrl: 'https://example.com/acme/another.git', gitToken: 'ghp_new' } });
|
|
387
|
+
expect(res.status).toBe(409);
|
|
388
|
+
expect(events).toEqual([]);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -780,8 +780,11 @@ describe('POST /setup/settings — the connection is checked before it is stored
|
|
|
780
780
|
const { base, settings } = listen(true, undefined, remote.check);
|
|
781
781
|
await settings.save({ kbRepoUrl: REPO, gitToken: 'ghp_old' }, null);
|
|
782
782
|
const moved = 'https://example.com/acme/moved.git';
|
|
783
|
+
// A different repository replaces every working copy, so the save is
|
|
784
|
+
// refused until the admin confirms — the address is checked all the same.
|
|
783
785
|
const res = await post(base, '/api/setup/settings', {
|
|
784
786
|
settings: { kbRepoUrl: moved, gitToken: 'ghp_new' },
|
|
787
|
+
confirmRepositoryChange: 'keep',
|
|
785
788
|
});
|
|
786
789
|
expect(res.status).toBe(200);
|
|
787
790
|
expect(remote.asked).toEqual([{ url: moved, token: 'ghp_new', username: 'x-access-token' }]);
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
} from '@bevel-software/platform-shared';
|
|
15
15
|
import { createHmac } from 'node:crypto';
|
|
16
16
|
import { TokenCrypto } from '../../shared/token-crypto.js';
|
|
17
|
-
import { assertKbDirNameFree } from '../kb-fs/repo-path.js';
|
|
17
|
+
import { assertKbDirNameFree, hasControlCharacter } from '../kb-fs/repo-path.js';
|
|
18
18
|
import { parseRetentionWindow } from '../audit/audit.contract.js';
|
|
19
19
|
import { normalizeIssuerUrl } from './oidc-check.js';
|
|
20
|
+
import { GIT_MODES, isGitMode } from './repository-source.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* A setting an admin may set from the setup screen instead of the environment.
|
|
@@ -69,6 +70,17 @@ export interface SettingDef {
|
|
|
69
70
|
* restart for a change that never happened.
|
|
70
71
|
*/
|
|
71
72
|
unsetMeans?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Written by the deployment itself, at the end of a flow that PROVED the
|
|
75
|
+
* value, and never taken from a save. What a GitHub App was given when it
|
|
76
|
+
* was registered, and which installation of it the admin was shown to
|
|
77
|
+
* hold, are facts somebody established; accepted from a form they would
|
|
78
|
+
* be claims, and the claim "installation 42 is mine" is one that reaches
|
|
79
|
+
* another organisation's repositories. A save that names one is refused,
|
|
80
|
+
* the setup screen is not told about it, and its value is read like any
|
|
81
|
+
* other's (the environment first), so an operator can still pin it.
|
|
82
|
+
*/
|
|
83
|
+
internal?: boolean;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
/**
|
|
@@ -99,6 +111,25 @@ export const validateHttpsRemote = (value: string): string | null => {
|
|
|
99
111
|
* The core catalogue. Order is the order the setup screen renders them in.
|
|
100
112
|
*/
|
|
101
113
|
export const CORE_SETTINGS: SettingDef[] = [
|
|
114
|
+
{
|
|
115
|
+
/**
|
|
116
|
+
* Which way the deployment is given its repository (see
|
|
117
|
+
* `RepositorySource`). Unset on a deployment configured before there was
|
|
118
|
+
* a choice, which is read as `token` when it has an address or a token,
|
|
119
|
+
* and as nothing chosen when it has neither.
|
|
120
|
+
*
|
|
121
|
+
* Restart-to-apply on a deployment that is running: its working copies
|
|
122
|
+
* were cloned from the repository it had, and they are brought into line
|
|
123
|
+
* with another one by the startup phase, at a moment when nobody is
|
|
124
|
+
* using them. The save that completes first-run setup runs that phase
|
|
125
|
+
* itself, and owes no restart.
|
|
126
|
+
*/
|
|
127
|
+
key: 'gitMode',
|
|
128
|
+
envVar: 'GIT_MODE',
|
|
129
|
+
section: 'knowledge-base',
|
|
130
|
+
validate: (v) => (isGitMode(v) ? null : `Choose one of: ${GIT_MODES.join(', ')}.`),
|
|
131
|
+
restartToApply: true,
|
|
132
|
+
},
|
|
102
133
|
{
|
|
103
134
|
key: 'kbRepoUrl',
|
|
104
135
|
envVar: 'KB_REPO_URL',
|
|
@@ -121,15 +152,73 @@ export const CORE_SETTINGS: SettingDef[] = [
|
|
|
121
152
|
validate: (v) =>
|
|
122
153
|
/^[A-Za-z0-9._-]+$/.test(v) ? null : 'Use only letters, digits, dot, underscore or hyphen.',
|
|
123
154
|
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A repository on GitHub, reached through a GitHub App (see
|
|
158
|
+
* `modules/github-app/`).
|
|
159
|
+
*
|
|
160
|
+
* THE REPOSITORY is the admin's to choose, among those the installation
|
|
161
|
+
* reaches, and is saved like any setting: a name the installation does not
|
|
162
|
+
* reach fails the connection check, since the only token presented to
|
|
163
|
+
* GitHub is the installation's.
|
|
164
|
+
*
|
|
165
|
+
* EVERYTHING ELSE is internal: the app's identity and keys, as GitHub
|
|
166
|
+
* issued them when the app was registered, and the installation, as
|
|
167
|
+
* GitHub confirmed the admin holds it. Each has a variable, so whoever
|
|
168
|
+
* operates the deployment can supply an app they registered themselves,
|
|
169
|
+
* and a host serving many deployments can supply one app for all of them.
|
|
170
|
+
*/
|
|
171
|
+
{
|
|
172
|
+
key: 'githubRepository',
|
|
173
|
+
envVar: 'GITHUB_APP_REPOSITORY',
|
|
174
|
+
section: 'knowledge-base',
|
|
175
|
+
validate: (v) =>
|
|
176
|
+
/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\/[A-Za-z0-9._-]+$/.test(v) && !v.endsWith('.git')
|
|
177
|
+
? null
|
|
178
|
+
: 'Name the repository as owner/name.',
|
|
179
|
+
},
|
|
180
|
+
{ key: 'githubAppId', envVar: 'GITHUB_APP_ID', section: 'knowledge-base', internal: true, validate: (v) => (/^\d+$/.test(v) ? null : 'The app id is a number.') },
|
|
181
|
+
{ key: 'githubAppSlug', envVar: 'GITHUB_APP_SLUG', section: 'knowledge-base', internal: true, validate: (v) => (/^[A-Za-z0-9-]+$/.test(v) ? null : 'The app slug is letters, digits and hyphens.') },
|
|
182
|
+
{ key: 'githubAppPrivateKey', envVar: 'GITHUB_APP_PRIVATE_KEY', section: 'knowledge-base', internal: true, secret: true },
|
|
183
|
+
{ key: 'githubAppClientId', envVar: 'GITHUB_APP_CLIENT_ID', section: 'knowledge-base', internal: true },
|
|
184
|
+
{ key: 'githubAppClientSecret', envVar: 'GITHUB_APP_CLIENT_SECRET', section: 'knowledge-base', internal: true, secret: true },
|
|
185
|
+
{
|
|
186
|
+
/**
|
|
187
|
+
* Put in front of the state every round trip to GitHub carries, by a
|
|
188
|
+
* host that serves several deployments behind ONE app: GitHub sends the
|
|
189
|
+
* browser back to the one address registered for the app, and the host
|
|
190
|
+
* reads this to know which deployment to hand it to. Never stored.
|
|
191
|
+
*/
|
|
192
|
+
key: 'githubAppStateTag',
|
|
193
|
+
envVar: 'GITHUB_APP_STATE_TAG',
|
|
194
|
+
section: 'knowledge-base',
|
|
195
|
+
internal: true,
|
|
196
|
+
validate: (v) => (/^[a-z0-9-]{1,63}$/.test(v) ? null : 'Lowercase letters, digits and hyphens.'),
|
|
197
|
+
},
|
|
198
|
+
{ key: 'githubInstallationId', envVar: 'GITHUB_APP_INSTALLATION_ID', section: 'knowledge-base', internal: true, validate: (v) => (/^\d+$/.test(v) ? null : 'The installation id is a number.') },
|
|
199
|
+
/** Whose account the installation is on, as GitHub names it: shown, never trusted. */
|
|
200
|
+
{ key: 'githubInstallationAccount', section: 'knowledge-base', internal: true },
|
|
201
|
+
/**
|
|
202
|
+
* The repositories the person who connected the installation could PUSH
|
|
203
|
+
* to with their own GitHub account, one `owner/name` a line, as GitHub
|
|
204
|
+
* listed them when they connected. The deployment may be pointed at one
|
|
205
|
+
* of these and at nothing else the installation reaches.
|
|
206
|
+
*/
|
|
207
|
+
{ key: 'githubRepositoriesPermitted', section: 'knowledge-base', internal: true },
|
|
124
208
|
{
|
|
125
209
|
key: 'kbDirName',
|
|
126
210
|
envVar: 'KB_DIR_NAME',
|
|
127
211
|
section: 'knowledge-base',
|
|
128
212
|
// Joined with workspace paths, so a separator or `..` would let it escape
|
|
129
|
-
// the workspace directory
|
|
213
|
+
// the workspace directory — and a control character would be PREFIXED onto
|
|
214
|
+
// every path, where a line break is a separator to git's `cat-file
|
|
215
|
+
// --batch` and would make the normaliser emit a path it refuses from a
|
|
216
|
+
// caller (see `hasControlCharacter`).
|
|
130
217
|
validate: (v) =>
|
|
131
218
|
v && v !== '.' && v !== '..' && !v.includes('/') && !v.includes('\\')
|
|
132
|
-
?
|
|
219
|
+
? hasControlCharacter(v)
|
|
220
|
+
? 'Use a single folder name — no line breaks or other invisible characters.'
|
|
221
|
+
: null
|
|
133
222
|
: 'Use a single folder name — no slashes.',
|
|
134
223
|
// Copied into a dozen services when they are constructed. A running server
|
|
135
224
|
// keeps the name it started with.
|
|
@@ -583,7 +672,7 @@ export class DeploymentSettingsService {
|
|
|
583
672
|
* replace it, which is all anyone needs to finish setup.
|
|
584
673
|
*/
|
|
585
674
|
describe(): ResolvedSetting[] {
|
|
586
|
-
return this.definitions.map((def) => {
|
|
675
|
+
return this.definitions.filter((def) => !def.internal).map((def) => {
|
|
587
676
|
const source = this.sourceOf(def.key);
|
|
588
677
|
const base = {
|
|
589
678
|
key: def.key,
|
|
@@ -616,8 +705,10 @@ export class DeploymentSettingsService {
|
|
|
616
705
|
async save(
|
|
617
706
|
entries: Record<string, string>,
|
|
618
707
|
updatedBy: string | null,
|
|
708
|
+
/** Set by {@link record} alone: the batch is the deployment's own, and may name internal settings. */
|
|
709
|
+
by: 'admin' | 'deployment' = 'admin',
|
|
619
710
|
): Promise<{ restartRequired: boolean; restartKeys: string[] }> {
|
|
620
|
-
const { toWrite, toClear } = this.plan(entries);
|
|
711
|
+
const { toWrite, toClear } = this.plan(entries, by);
|
|
621
712
|
|
|
622
713
|
/** The settings this save changed that a running server cannot pick up. */
|
|
623
714
|
const restartKeys: string[] = [];
|
|
@@ -662,8 +753,22 @@ export class DeploymentSettingsService {
|
|
|
662
753
|
toClear.includes(key) ? '' : (toWrite.find((w) => w.key === key)?.value ?? this.resolve(key));
|
|
663
754
|
}
|
|
664
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Store what the DEPLOYMENT established, internal settings included: the
|
|
758
|
+
* end of a flow that proved each value (see `SettingDef.internal`). The
|
|
759
|
+
* same validation, the same encryption, the same refusal of a setting the
|
|
760
|
+
* environment pins. Never reachable from a request body: the setup routes
|
|
761
|
+
* call {@link save}.
|
|
762
|
+
*/
|
|
763
|
+
async record(entries: Record<string, string>, updatedBy: string | null): Promise<void> {
|
|
764
|
+
await this.save(entries, updatedBy, 'deployment');
|
|
765
|
+
}
|
|
766
|
+
|
|
665
767
|
/** Validate a batch and return the writes (and the clears) it amounts to; throws on any problem. */
|
|
666
|
-
private plan(
|
|
768
|
+
private plan(
|
|
769
|
+
entries: Record<string, string>,
|
|
770
|
+
by: 'admin' | 'deployment' = 'admin',
|
|
771
|
+
): {
|
|
667
772
|
toWrite: { key: string; value: string; def: SettingDef }[];
|
|
668
773
|
toClear: string[];
|
|
669
774
|
} {
|
|
@@ -677,6 +782,12 @@ export class DeploymentSettingsService {
|
|
|
677
782
|
problems[key] = 'Unknown setting.';
|
|
678
783
|
continue;
|
|
679
784
|
}
|
|
785
|
+
if (def.internal && by !== 'deployment') {
|
|
786
|
+
// Said the way an unknown setting is: what the deployment keeps for
|
|
787
|
+
// itself is not the form's to name, or to learn the names of.
|
|
788
|
+
problems[key] = 'Unknown setting.';
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
680
791
|
if (this.sourceOf(key) === 'env') {
|
|
681
792
|
problems[key] = `Set by the ${def.envVar} environment variable — change it there.`;
|
|
682
793
|
continue;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { IGitRunner } from '../../shared/git.contract.js';
|
|
4
|
+
|
|
5
|
+
/** The folder the managed repository is kept in, under the root it is given. */
|
|
6
|
+
export const MANAGED_REPOSITORY_DIR = 'knowledge-base.git';
|
|
7
|
+
|
|
8
|
+
/** The branch a managed repository starts on when the deployment names none. */
|
|
9
|
+
export const MANAGED_DEFAULT_BRANCH = 'main';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The repository a deployment keeps FOR ITSELF, when its admin chose not to
|
|
13
|
+
* bring one: a bare git repository on the deployment's own disk.
|
|
14
|
+
*
|
|
15
|
+
* Nothing else about the deployment changes. Working copies are cloned from
|
|
16
|
+
* it, pushed to it and fetched from it exactly as they are from a
|
|
17
|
+
* repository on a host; it happens to be reached by a path, and to need no
|
|
18
|
+
* credential. An empty one is seeded by the startup phase like any empty
|
|
19
|
+
* remote.
|
|
20
|
+
*
|
|
21
|
+
* WHERE IT LIVES IS THE WHOLE DECISION. It is the only copy of the
|
|
22
|
+
* knowledge base that is not a working copy, so it has to be somewhere that
|
|
23
|
+
* survives the container and that nothing tidies up:
|
|
24
|
+
*
|
|
25
|
+
* - not under the workspaces root, where the workspace service's orphan
|
|
26
|
+
* sweep removes every folder that is not a known branch's;
|
|
27
|
+
* - not in a new folder beside it, which every shipped compose file would
|
|
28
|
+
* have to grow a volume for, and a deployment started from an older file
|
|
29
|
+
* would lose on its next redeploy without a word.
|
|
30
|
+
*
|
|
31
|
+
* The composition root puts it under the BACKUPS root: a volume of its own
|
|
32
|
+
* in every compose file that has ever shipped, and one nothing sweeps.
|
|
33
|
+
*/
|
|
34
|
+
export class ManagedRepository {
|
|
35
|
+
/** The repository's folder: what git is given as the remote. */
|
|
36
|
+
readonly path: string;
|
|
37
|
+
|
|
38
|
+
constructor(private readonly root: string) {
|
|
39
|
+
this.path = path.join(root, MANAGED_REPOSITORY_DIR);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Whether the repository is there. A folder that is not a repository does not count. */
|
|
43
|
+
async exists(): Promise<boolean> {
|
|
44
|
+
return fs.access(path.join(this.path, 'HEAD')).then(
|
|
45
|
+
() => true,
|
|
46
|
+
() => false,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Make sure the repository exists, creating it empty when it does not.
|
|
52
|
+
* One that exists is left exactly as it is: this never re-initialises,
|
|
53
|
+
* and never touches a ref.
|
|
54
|
+
*/
|
|
55
|
+
async ensure(runner: IGitRunner, initialBranch: string = MANAGED_DEFAULT_BRANCH): Promise<void> {
|
|
56
|
+
if (await this.exists()) return;
|
|
57
|
+
await fs.mkdir(this.root, { recursive: true });
|
|
58
|
+
// A folder that is there without being a repository is somebody's, and
|
|
59
|
+
// initialising over it would mix a repository into their files.
|
|
60
|
+
const entries = await fs.readdir(this.path).catch(() => null);
|
|
61
|
+
if (entries && entries.length > 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${this.path} exists and is not a git repository. Move it away, or empty it, before choosing a managed repository.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
await runner.run(this.root, ['init', '--bare', `--initial-branch=${initialBranch}`, this.path]);
|
|
67
|
+
}
|
|
68
|
+
}
|