@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
|
@@ -6,7 +6,7 @@ import { printable } from '../../shared/printable.js';
|
|
|
6
6
|
const log = logger('accounts');
|
|
7
7
|
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
8
8
|
import type { AuthService } from './auth.service.js';
|
|
9
|
-
import { AccountAdmissionRefusedError } from './account-admission.js';
|
|
9
|
+
import { AccountAdmissionRefusedError, AccountChangeRefusedError } from './account-admission.js';
|
|
10
10
|
import { erasedAccountId, type IAccountErasureService } from './account-erasure.service.js';
|
|
11
11
|
import type { IAdminAccessService } from '../admin/admin.interface.js';
|
|
12
12
|
import type { UserAccessRemovalService } from '../access/user-access-removal.service.js';
|
|
@@ -26,7 +26,7 @@ import './auth.middleware.js'; // Express Request augmentation
|
|
|
26
26
|
* bootstrap admin (always-admin, see AdminAccessService).
|
|
27
27
|
*/
|
|
28
28
|
export function createAccountRoutes(
|
|
29
|
-
authService: Pick<AuthService, 'listAccounts' | 'createAccount' | 'getUserById'>,
|
|
29
|
+
authService: Pick<AuthService, 'listAccounts' | 'createAccount' | 'getUserById' | 'deactivate' | 'reactivate'>,
|
|
30
30
|
adminAccess: IAdminAccessService,
|
|
31
31
|
accountErasure: Pick<IAccountErasureService, 'eraseUser'>,
|
|
32
32
|
accessRemoval?: Pick<UserAccessRemovalService, 'report' | 'assertRemovable' | 'remove' | 'filesNaming'>,
|
|
@@ -47,17 +47,23 @@ export function createAccountRoutes(
|
|
|
47
47
|
res.json({ accounts: await authService.listAccounts() });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// POST /api/admin/accounts { email, name?, password } — create an account
|
|
50
|
+
// POST /api/admin/accounts { email, name?, password? } — create an account
|
|
51
51
|
// (or reset the password of an existing one; upsert-by-email is deliberate,
|
|
52
|
-
// see AuthService.createAccount).
|
|
52
|
+
// see AuthService.createAccount). Without a password the account is for
|
|
53
|
+
// single sign-on: the person finds it waiting when they first sign in.
|
|
53
54
|
router.post('/admin/accounts', requireAdmin, async (req, res) => {
|
|
54
55
|
const { email, name, password } = req.body as {
|
|
55
56
|
email?: string;
|
|
56
57
|
name?: string;
|
|
57
|
-
password?:
|
|
58
|
+
password?: unknown;
|
|
58
59
|
};
|
|
59
|
-
if (!email
|
|
60
|
-
res.status(400).json({ error: 'email
|
|
60
|
+
if (!email) {
|
|
61
|
+
res.status(400).json({ error: 'email is required' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Absent means single sign-on; anything else that is not text is a mistake, not that.
|
|
65
|
+
if (password !== undefined && typeof password !== 'string') {
|
|
66
|
+
res.status(400).json({ error: 'password must be a string' });
|
|
61
67
|
return;
|
|
62
68
|
}
|
|
63
69
|
try {
|
|
@@ -71,6 +77,56 @@ export function createAccountRoutes(
|
|
|
71
77
|
}
|
|
72
78
|
});
|
|
73
79
|
|
|
80
|
+
// POST /api/admin/accounts/:userId/deactivate — switch an account off. It
|
|
81
|
+
// keeps its row, history and place in roles and groups; nothing it holds
|
|
82
|
+
// signs in any more (see AuthService.isActive). An admin's own account is
|
|
83
|
+
// refused, so whoever does this is still an admin who is on afterwards —
|
|
84
|
+
// the deployment is never left with no admin able to sign in.
|
|
85
|
+
router.post('/admin/accounts/:userId/deactivate', requireAdmin, async (req, res) => {
|
|
86
|
+
const userId = String(req.params.userId);
|
|
87
|
+
if (userId === req.userId) {
|
|
88
|
+
res.status(400).json({ error: 'You cannot switch off your own account. Ask another admin.' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
if (!(await authService.deactivate(userId))) {
|
|
93
|
+
res.status(404).json({ error: 'No such user' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
log.info('account audit:', { action: 'deactivate', actorUserId: req.userId, targetUserId: userId });
|
|
97
|
+
res.status(204).end();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof AccountChangeRefusedError) {
|
|
100
|
+
res.status(400).json({ error: error.message });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
log.error('switching an account off failed:', { err: error });
|
|
104
|
+
res.status(500).json({ error: 'Could not switch this account off' });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// POST /api/admin/accounts/:userId/reactivate — switch it back on, if the
|
|
109
|
+
// deployment has room: the admission port is asked, as for a new account
|
|
110
|
+
// (a host that sells seats answers from its plan; 403 with its words).
|
|
111
|
+
router.post('/admin/accounts/:userId/reactivate', requireAdmin, async (req, res) => {
|
|
112
|
+
const userId = String(req.params.userId);
|
|
113
|
+
try {
|
|
114
|
+
if (!(await authService.reactivate(userId))) {
|
|
115
|
+
res.status(404).json({ error: 'No such user' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
log.info('account audit:', { action: 'reactivate', actorUserId: req.userId, targetUserId: userId });
|
|
119
|
+
res.status(204).end();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof AccountAdmissionRefusedError) {
|
|
122
|
+
res.status(403).json({ error: error.message });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
log.error('switching an account on failed:', { err: error });
|
|
126
|
+
res.status(500).json({ error: 'Could not switch this account on' });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
74
130
|
// GET /api/admin/accounts/:userId/references — how many places in the
|
|
75
131
|
// knowledge base name this account's address (roles, groups, access rules,
|
|
76
132
|
// file grants) and whether removing them is allowed. Feeds the delete
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import type { AuthService } from './auth.service.js';
|
|
3
|
+
import { AccountDeactivatedError, AuthBackendError } from './account-admission.js';
|
|
3
4
|
|
|
4
5
|
// Augment Express Request so userId/userEmail are available after auth middleware
|
|
5
6
|
declare global {
|
|
@@ -46,8 +47,8 @@ function readAuthCookie(req: Request): string | null {
|
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export function createAuthMiddleware(authService: AuthService) {
|
|
50
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
50
|
+
export function createAuthMiddleware(authService: Pick<AuthService, 'resolveSession'>) {
|
|
51
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
51
52
|
// Bearer is the primary path — every JSON API call in the frontend
|
|
52
53
|
// attaches it via `authFetch`. Cookie is the fallback for transports
|
|
53
54
|
// that can't carry headers (EventSource, image tags), set at login by
|
|
@@ -66,13 +67,23 @@ export function createAuthMiddleware(authService: AuthService) {
|
|
|
66
67
|
return;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
let session: { userId: string; email: string };
|
|
69
71
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
session = await authService.resolveSession(token);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// The account could not be looked up: not the caller's doing, and a
|
|
75
|
+
// 401 here would sign every valid session out during an outage.
|
|
76
|
+
if (err instanceof AuthBackendError) {
|
|
77
|
+
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// A 401 otherwise, so the app drops the session and shows the login
|
|
81
|
+
// page; a switched-off account is told why there.
|
|
82
|
+
res.status(401).json({ error: err instanceof AccountDeactivatedError ? err.message : 'Invalid or expired token' });
|
|
83
|
+
return;
|
|
76
84
|
}
|
|
85
|
+
req.userId = session.userId;
|
|
86
|
+
req.userEmail = session.email;
|
|
87
|
+
next();
|
|
77
88
|
};
|
|
78
89
|
}
|
|
@@ -5,6 +5,7 @@ const log = logger('auth');
|
|
|
5
5
|
import type { AuthService } from './auth.service.js';
|
|
6
6
|
import { AUTH_COOKIE_NAME } from './auth.middleware.js'; // also imports Express Request augmentation
|
|
7
7
|
import { FixedWindowRateLimiter } from './rate-limit.js';
|
|
8
|
+
import { AccountDeactivatedError } from './account-admission.js';
|
|
8
9
|
import { canonicalEmail } from '../../shared/email-identity.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -113,6 +114,13 @@ export function createAuthRoutes(
|
|
|
113
114
|
});
|
|
114
115
|
res.json(result);
|
|
115
116
|
} catch (error) {
|
|
117
|
+
// The password was right and an admin switched the account off: said
|
|
118
|
+
// as such (only ever after the password is proven, see
|
|
119
|
+
// `loginWithPassword`), so the person knows whom to ask.
|
|
120
|
+
if (error instanceof AccountDeactivatedError) {
|
|
121
|
+
res.status(403).json({ error: error.message });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
116
124
|
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
117
125
|
log.error('Login error:', { detail: msg });
|
|
118
126
|
res.status(401).json({ error: 'Authentication failed' });
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
|
-
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { and, eq, isNotNull, isNull } from 'drizzle-orm';
|
|
3
3
|
import type { Database } from '../database/connection.js';
|
|
4
4
|
import { users } from '../database/schema.js';
|
|
5
5
|
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
6
6
|
import { canonicalEmail, hashEmail } from '../../shared/email-identity.js';
|
|
7
7
|
import {
|
|
8
8
|
AccountAdmissionRefusedError,
|
|
9
|
+
AccountChangeRefusedError,
|
|
10
|
+
AccountDeactivatedError,
|
|
11
|
+
AuthBackendError,
|
|
9
12
|
admitEveryone,
|
|
10
13
|
type AccountProvisionReason,
|
|
11
14
|
type IAccountAdmission,
|
|
@@ -16,9 +19,22 @@ import {
|
|
|
16
19
|
timingSafeStringEqual,
|
|
17
20
|
MIN_PASSWORD_LENGTH,
|
|
18
21
|
} from './password-hash.js';
|
|
22
|
+
import { RECOVERY_BOT_EMAIL } from '../workflow/recovery-bot.js';
|
|
23
|
+
import { DIRECTORY_SYNC_BOT_EMAIL } from '../access/directory-sync-bot.js';
|
|
19
24
|
|
|
20
25
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
21
26
|
|
|
27
|
+
/**
|
|
28
|
+
* How long {@link AuthService.isActive} trusts what it last read. Every
|
|
29
|
+
* request carrying a credential asks, so the answer is kept briefly rather
|
|
30
|
+
* than read each time; switching an account off or on in THIS process
|
|
31
|
+
* forgets it at once, and another replica sees the change within this long.
|
|
32
|
+
*/
|
|
33
|
+
const ACTIVE_CACHE_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
/** The accounts core runs its own work as. Nobody signs in with them, and nobody switches them off. */
|
|
36
|
+
const SYSTEM_ACCOUNT_EMAILS: readonly string[] = [RECOVERY_BOT_EMAIL, DIRECTORY_SYNC_BOT_EMAIL];
|
|
37
|
+
|
|
22
38
|
/**
|
|
23
39
|
* What every caller is told when it tries to give the deployment admin a
|
|
24
40
|
* stored password — the Account page's own change and an admin's "Set
|
|
@@ -85,6 +101,24 @@ export interface AuthConfig {
|
|
|
85
101
|
loginPasswordEnabled?: boolean;
|
|
86
102
|
}
|
|
87
103
|
|
|
104
|
+
/** What a sign-in provider tells {@link AuthService.loginWithSso} about the sign-in it hands over. */
|
|
105
|
+
export interface SsoLoginOptions {
|
|
106
|
+
/**
|
|
107
|
+
* The provider has itself decided that this person may enter: they were
|
|
108
|
+
* invited, their address is on a domain the deployment claimed, whatever
|
|
109
|
+
* its rule is. The domain allow-list (`ALLOWED_EMAIL_DOMAINS`) is then
|
|
110
|
+
* not applied.
|
|
111
|
+
*
|
|
112
|
+
* The allow-list exists for a provider that decides nothing: it is what
|
|
113
|
+
* stands between "the issuer knows this person" and "this person has an
|
|
114
|
+
* account here". Applied on top of a provider's own decision it is a
|
|
115
|
+
* second rule the admin set somewhere else, and the person it refuses was
|
|
116
|
+
* let in by the first. Default false, so every provider that does not say
|
|
117
|
+
* otherwise is governed by the allow-list as before.
|
|
118
|
+
*/
|
|
119
|
+
admittedByProvider?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
88
122
|
export class AuthService {
|
|
89
123
|
constructor(
|
|
90
124
|
private readonly db: Database,
|
|
@@ -111,7 +145,19 @@ export class AuthService {
|
|
|
111
145
|
.limit(1);
|
|
112
146
|
if (existing) return;
|
|
113
147
|
const verdict = await this.admission.canProvision(normalizedEmail, reason);
|
|
114
|
-
if (
|
|
148
|
+
if (verdict.ok) return;
|
|
149
|
+
// A first sign-in the port would rather keep than turn away: the person
|
|
150
|
+
// is put on file, switched off, for an admin to switch on — and is still
|
|
151
|
+
// refused now, with the port's words. Only for single sign-on, the one
|
|
152
|
+
// provisioning nobody asked for (see `AccountAdmissionVerdict`).
|
|
153
|
+
if (verdict.waitForAdmin && reason === 'sso') {
|
|
154
|
+
await this.db
|
|
155
|
+
.insert(users)
|
|
156
|
+
.values({ email: normalizedEmail, name: normalizedEmail.split('@')[0] || normalizedEmail, deactivatedAt: new Date() })
|
|
157
|
+
.onConflictDoNothing({ target: users.email });
|
|
158
|
+
throw new AccountAdmissionRefusedError(verdict.message, { waitingForAdmin: true });
|
|
159
|
+
}
|
|
160
|
+
throw new AccountAdmissionRefusedError(verdict.message);
|
|
115
161
|
}
|
|
116
162
|
|
|
117
163
|
/**
|
|
@@ -182,6 +228,9 @@ export class AuthService {
|
|
|
182
228
|
await verifyPassword(provided, await decoyHash());
|
|
183
229
|
throw new Error('Invalid credentials');
|
|
184
230
|
}
|
|
231
|
+
// Never refused for being switched off: the environment password is
|
|
232
|
+
// the deployment's way back in, and {@link deactivate} refuses this
|
|
233
|
+
// account for that reason.
|
|
185
234
|
const defaultName = normalizedEmail.split('@')[0] || normalizedEmail;
|
|
186
235
|
await this.assertAdmitted(normalizedEmail, 'bootstrap');
|
|
187
236
|
const admin = await this.upsertUserByEmail(normalizedEmail, defaultName);
|
|
@@ -197,6 +246,9 @@ export class AuthService {
|
|
|
197
246
|
if (!user?.passwordHash || !matches) {
|
|
198
247
|
throw new Error('Invalid credentials');
|
|
199
248
|
}
|
|
249
|
+
// Only now, with the password proven: telling a stranger that an address
|
|
250
|
+
// is switched off would tell them it has an account.
|
|
251
|
+
if (user.deactivatedAt) throw new AccountDeactivatedError();
|
|
200
252
|
return { token: this.signToken(user.id, user.email), user: this.toClientUser(user) };
|
|
201
253
|
}
|
|
202
254
|
|
|
@@ -206,19 +258,29 @@ export class AuthService {
|
|
|
206
258
|
* verified. Same upsert-by-email + JWT path as password login. Email is the
|
|
207
259
|
* idempotency key, so a user who first used password login and later signs
|
|
208
260
|
* in via SSO (same email) keeps the same account/id.
|
|
261
|
+
*
|
|
262
|
+
* WHO MAY ENTER is decided once per sign-in, by whoever is in a position
|
|
263
|
+
* to decide it. For the deployment's own provider that is the domain
|
|
264
|
+
* allow-list: the provider signs in whoever its issuer knows, and nobody
|
|
265
|
+
* has approved the person. A provider that HAS decided (see
|
|
266
|
+
* {@link SsoLoginOptions.admittedByProvider}) is governed by its own rule
|
|
267
|
+
* alone; the allow-list is not laid on top of it. The admission port is
|
|
268
|
+
* asked either way: that is the plan's answer, not a sign-in rule.
|
|
209
269
|
*/
|
|
210
270
|
async loginWithSso(
|
|
211
271
|
email: string,
|
|
212
272
|
name: string,
|
|
273
|
+
opts: SsoLoginOptions = {},
|
|
213
274
|
): Promise<{ token: string; user: AuthUser }> {
|
|
214
275
|
const normalizedEmail = canonicalEmail(email ?? '');
|
|
215
276
|
if (!EMAIL_REGEX.test(normalizedEmail)) {
|
|
216
277
|
throw new Error('Sign-in returned an invalid email');
|
|
217
278
|
}
|
|
218
|
-
this.assertAllowedDomain(normalizedEmail);
|
|
279
|
+
if (!opts.admittedByProvider) this.assertAllowedDomain(normalizedEmail);
|
|
219
280
|
await this.assertAdmitted(normalizedEmail, 'sso');
|
|
220
281
|
const displayName = (name ?? '').trim() || normalizedEmail.split('@')[0] || normalizedEmail;
|
|
221
282
|
const user = await this.upsertUserByEmail(normalizedEmail, displayName);
|
|
283
|
+
if (user.deactivatedAt) throw new AccountDeactivatedError();
|
|
222
284
|
return { token: this.signToken(user.id, user.email), user: this.toClientUser(user) };
|
|
223
285
|
}
|
|
224
286
|
|
|
@@ -228,7 +290,13 @@ export class AuthService {
|
|
|
228
290
|
* account (e.g. one that first arrived via SSO, or a reset for a locked-out
|
|
229
291
|
* user) is deliberate admin behavior, not an error.
|
|
230
292
|
*
|
|
231
|
-
*
|
|
293
|
+
* WITHOUT a password the account is made for single sign-on: the person
|
|
294
|
+
* signs in through the deployment's provider and finds their account
|
|
295
|
+
* waiting, already holding its place (a seat, on a host that sells them).
|
|
296
|
+
* Password sign-in refuses it until a password is set. For an address that
|
|
297
|
+
* already has an account it changes nothing but an explicitly given name.
|
|
298
|
+
*
|
|
299
|
+
* WITH a password, the deployment admin is the one target this refuses, for the reason
|
|
232
300
|
* {@link changePassword} refuses it: that account's password is the
|
|
233
301
|
* environment's, so a stored hash would not replace it but ADD a second
|
|
234
302
|
* credential — one that keeps signing in after `ADMIN_PASSWORD` is rotated,
|
|
@@ -239,12 +307,26 @@ export class AuthService {
|
|
|
239
307
|
async createAccount(
|
|
240
308
|
email: string,
|
|
241
309
|
name: string | undefined,
|
|
242
|
-
password
|
|
310
|
+
password?: string,
|
|
243
311
|
): Promise<AuthUser> {
|
|
244
312
|
const normalizedEmail = canonicalEmail(email ?? '');
|
|
245
313
|
if (!EMAIL_REGEX.test(normalizedEmail)) {
|
|
246
314
|
throw new Error('Invalid email');
|
|
247
315
|
}
|
|
316
|
+
const suppliedName = (name ?? '').trim();
|
|
317
|
+
const displayName = suppliedName || normalizedEmail.split('@')[0] || normalizedEmail;
|
|
318
|
+
if (!password) {
|
|
319
|
+
await this.assertAdmitted(normalizedEmail, 'admin-create');
|
|
320
|
+
const [created] = await this.db
|
|
321
|
+
.insert(users)
|
|
322
|
+
.values({ email: normalizedEmail, name: displayName })
|
|
323
|
+
.onConflictDoUpdate({
|
|
324
|
+
target: users.email,
|
|
325
|
+
set: suppliedName ? { name: suppliedName, updatedAt: new Date() } : { updatedAt: new Date() },
|
|
326
|
+
})
|
|
327
|
+
.returning();
|
|
328
|
+
return this.toClientUser(created);
|
|
329
|
+
}
|
|
248
330
|
// Before the policy check, so the refusal names the real reason rather
|
|
249
331
|
// than sending the admin off to pick a longer password first.
|
|
250
332
|
if (this.isEnvAdminEmail(normalizedEmail)) {
|
|
@@ -252,8 +334,6 @@ export class AuthService {
|
|
|
252
334
|
}
|
|
253
335
|
this.assertPasswordPolicy(password);
|
|
254
336
|
await this.assertAdmitted(normalizedEmail, 'admin-create');
|
|
255
|
-
const suppliedName = (name ?? '').trim();
|
|
256
|
-
const displayName = suppliedName || normalizedEmail.split('@')[0] || normalizedEmail;
|
|
257
337
|
const passwordHash = await hashPassword(password);
|
|
258
338
|
// One atomic upsert. On conflict (re-provisioning an existing account) an
|
|
259
339
|
// EXPLICITLY supplied name is persisted too; a blank name keeps the
|
|
@@ -375,6 +455,10 @@ export class AuthService {
|
|
|
375
455
|
name: string;
|
|
376
456
|
hasPassword: boolean;
|
|
377
457
|
isEnvAdmin: boolean;
|
|
458
|
+
/** When an admin switched the account off; null while it is on. */
|
|
459
|
+
deactivatedAt: Date | null;
|
|
460
|
+
/** One of the accounts the platform runs its own work as: never switched off, nobody signs in with it. */
|
|
461
|
+
isSystem: boolean;
|
|
378
462
|
createdAt: Date;
|
|
379
463
|
}>
|
|
380
464
|
> {
|
|
@@ -385,6 +469,8 @@ export class AuthService {
|
|
|
385
469
|
name: row.name,
|
|
386
470
|
hasPassword: row.passwordHash != null,
|
|
387
471
|
isEnvAdmin: this.reportsAsEnvAdmin(row.email),
|
|
472
|
+
deactivatedAt: row.deactivatedAt,
|
|
473
|
+
isSystem: SYSTEM_ACCOUNT_EMAILS.includes(row.email),
|
|
388
474
|
createdAt: row.createdAt,
|
|
389
475
|
}));
|
|
390
476
|
}
|
|
@@ -416,6 +502,7 @@ export class AuthService {
|
|
|
416
502
|
await this.assertAdmitted(normalizedEmail, 'embed');
|
|
417
503
|
const displayName = (name ?? '').trim() || normalizedEmail.split('@')[0] || normalizedEmail;
|
|
418
504
|
const user = await this.upsertUserByEmail(normalizedEmail, displayName);
|
|
505
|
+
if (user.deactivatedAt) throw new AccountDeactivatedError();
|
|
419
506
|
return { id: user.id, email: user.email, name: user.name };
|
|
420
507
|
}
|
|
421
508
|
|
|
@@ -428,6 +515,11 @@ export class AuthService {
|
|
|
428
515
|
return user;
|
|
429
516
|
}
|
|
430
517
|
|
|
518
|
+
/**
|
|
519
|
+
* The signature and expiry of a session token, and nothing else — whether
|
|
520
|
+
* its account is still on is {@link resolveSession}'s question. Every
|
|
521
|
+
* place a session is ACCEPTED goes through that one.
|
|
522
|
+
*/
|
|
431
523
|
verifyToken(token: string): { userId: string; email: string } {
|
|
432
524
|
const decoded = jwt.verify(token, this.config.jwtSecret) as {
|
|
433
525
|
userId: string;
|
|
@@ -436,6 +528,114 @@ export class AuthService {
|
|
|
436
528
|
return { userId: decoded.userId, email: decoded.email };
|
|
437
529
|
}
|
|
438
530
|
|
|
531
|
+
/**
|
|
532
|
+
* A session token that may still be used: signed by us, not expired, and
|
|
533
|
+
* its account still on. A token outlives a deactivation by up to seven
|
|
534
|
+
* days, so the signature alone no longer says the bearer may enter.
|
|
535
|
+
* Throws for an invalid token, {@link AccountDeactivatedError} for one
|
|
536
|
+
* whose account was switched off.
|
|
537
|
+
*/
|
|
538
|
+
async resolveSession(token: string): Promise<{ userId: string; email: string }> {
|
|
539
|
+
const claim = this.verifyToken(token);
|
|
540
|
+
let active: boolean;
|
|
541
|
+
try {
|
|
542
|
+
active = await this.isActive(claim.userId);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
// The token is fine; the database is not. Said apart from a refusal, so
|
|
545
|
+
// an outage is a 500 and not a sign-out (see `AuthBackendError`).
|
|
546
|
+
throw new AuthBackendError(err);
|
|
547
|
+
}
|
|
548
|
+
if (!active) throw new AccountDeactivatedError();
|
|
549
|
+
return claim;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private readonly activeCache = new Map<string, { active: boolean; at: number }>();
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Whether the account behind a credential is on: it exists and no admin
|
|
556
|
+
* switched it off. Asked for every credential presented that does not
|
|
557
|
+
* already read the account (a session token, an internal token), so it is
|
|
558
|
+
* cached for {@link ACTIVE_CACHE_MS}; switching an account off or on in
|
|
559
|
+
* this process forgets its entry at once.
|
|
560
|
+
*
|
|
561
|
+
* A missing row is not on either — an erased account's tokens die with it.
|
|
562
|
+
*
|
|
563
|
+
* The deployment admin's account is always on, whatever its row says: its
|
|
564
|
+
* environment password signs it in regardless (see `loginWithPassword`),
|
|
565
|
+
* and a session that sign-in mints must then be usable too, or the way
|
|
566
|
+
* back into a deployment would lead nowhere. {@link deactivate} refuses
|
|
567
|
+
* the account, but a host may write the column itself.
|
|
568
|
+
*/
|
|
569
|
+
async isActive(userId: string): Promise<boolean> {
|
|
570
|
+
const now = Date.now();
|
|
571
|
+
const hit = this.activeCache.get(userId);
|
|
572
|
+
if (hit && now - hit.at < ACTIVE_CACHE_MS) return hit.active;
|
|
573
|
+
const [row] = await this.db
|
|
574
|
+
.select({ email: users.email, deactivatedAt: users.deactivatedAt })
|
|
575
|
+
.from(users)
|
|
576
|
+
.where(eq(users.id, userId))
|
|
577
|
+
.limit(1);
|
|
578
|
+
const active = row !== undefined && (!row.deactivatedAt || this.isEnvAdminEmail(row.email ?? ''));
|
|
579
|
+
this.activeCache.set(userId, { active, at: now });
|
|
580
|
+
return active;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Switch an account off. Its row, history and place in roles and groups
|
|
585
|
+
* stay; every credential it holds stops being honoured (see
|
|
586
|
+
* {@link isActive}). Idempotent: an account already off keeps the moment
|
|
587
|
+
* it was first switched off.
|
|
588
|
+
*
|
|
589
|
+
* The deployment admin is refused: the environment password is the way
|
|
590
|
+
* back into a deployment whose every other admin is gone, and switching
|
|
591
|
+
* it off would close that door from inside. So are the machine accounts
|
|
592
|
+
* core runs its own work as. Refusing an admin's own account is the
|
|
593
|
+
* route's concern, since it knows who is asking.
|
|
594
|
+
*
|
|
595
|
+
* Returns false when there is no such account.
|
|
596
|
+
*/
|
|
597
|
+
async deactivate(userId: string): Promise<boolean> {
|
|
598
|
+
const [row] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
|
|
599
|
+
if (!row) return false;
|
|
600
|
+
if (this.isEnvAdminEmail(row.email)) {
|
|
601
|
+
throw new AccountChangeRefusedError('The deployment admin cannot be switched off: its password in the environment is the way back in.');
|
|
602
|
+
}
|
|
603
|
+
if (SYSTEM_ACCOUNT_EMAILS.includes(row.email)) {
|
|
604
|
+
throw new AccountChangeRefusedError('This account belongs to the platform itself and cannot be switched off.');
|
|
605
|
+
}
|
|
606
|
+
await this.db
|
|
607
|
+
.update(users)
|
|
608
|
+
.set({ deactivatedAt: new Date(), updatedAt: new Date() })
|
|
609
|
+
.where(and(eq(users.id, userId), isNull(users.deactivatedAt)));
|
|
610
|
+
this.activeCache.delete(userId);
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Switch a deactivated account back on, if the deployment has room for it:
|
|
616
|
+
* the admission port is asked, as for a new account, because an account
|
|
617
|
+
* that is on takes the same place. A refusal throws
|
|
618
|
+
* {@link AccountAdmissionRefusedError} with the port's words. An account
|
|
619
|
+
* already on is not asked about.
|
|
620
|
+
*
|
|
621
|
+
* Returns false when there is no such account.
|
|
622
|
+
*/
|
|
623
|
+
async reactivate(userId: string): Promise<boolean> {
|
|
624
|
+
const [row] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
|
|
625
|
+
if (!row) return false;
|
|
626
|
+
if (!row.deactivatedAt) return true;
|
|
627
|
+
if (this.admission !== admitEveryone) {
|
|
628
|
+
const verdict = await this.admission.canProvision(row.email, 'reactivate');
|
|
629
|
+
if (!verdict.ok) throw new AccountAdmissionRefusedError(verdict.message);
|
|
630
|
+
}
|
|
631
|
+
await this.db
|
|
632
|
+
.update(users)
|
|
633
|
+
.set({ deactivatedAt: null, updatedAt: new Date() })
|
|
634
|
+
.where(and(eq(users.id, userId), isNotNull(users.deactivatedAt)));
|
|
635
|
+
this.activeCache.delete(userId);
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
|
|
439
639
|
/**
|
|
440
640
|
* Resolve a set of `authorId` hashes back to the app users that produced
|
|
441
641
|
* them. Used by the PR list to display the human who triggered each PR
|
|
@@ -7,7 +7,7 @@ import type { AuthProviderPlugin } from './auth.routes.js';
|
|
|
7
7
|
import { AUTH_COOKIE_MAX_AGE_S } from './auth.routes.js';
|
|
8
8
|
import { AUTH_COOKIE_NAME } from './auth.middleware.js';
|
|
9
9
|
import type { AuthService } from './auth.service.js';
|
|
10
|
-
import { AccountAdmissionRefusedError } from './account-admission.js';
|
|
10
|
+
import { AccountAdmissionRefusedError, AccountDeactivatedError } from './account-admission.js';
|
|
11
11
|
import { normalizeIssuerUrl } from '../settings/oidc-check.js';
|
|
12
12
|
|
|
13
13
|
// Short-lived CSRF state + PKCE verifier for the OAuth round-trip: set before
|
|
@@ -325,7 +325,12 @@ export class OidcAuthProvider implements AuthProviderPlugin {
|
|
|
325
325
|
// screen can say so instead of "try again".
|
|
326
326
|
if (error instanceof AccountAdmissionRefusedError) {
|
|
327
327
|
log.warn('OIDC sign-in refused by the account admission port:', { detail: error.message });
|
|
328
|
-
fail('admission');
|
|
328
|
+
fail(error.waitingForAdmin ? 'waiting' : 'admission');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (error instanceof AccountDeactivatedError) {
|
|
332
|
+
log.warn('OIDC sign-in refused: the account is switched off');
|
|
333
|
+
fail('deactivated');
|
|
329
334
|
return;
|
|
330
335
|
}
|
|
331
336
|
log.error('OIDC callback error:', { err: error });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CORE platform tables — the git-backed workspace/workflow, auth, access,
|
|
3
|
-
* change requests, locks, the pending-commits queue,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* change requests, locks, the pending-commits queue, connection keys, MCP
|
|
4
|
+
* OAuth, and the Secrets Vault. A core-only deployment migrates and runs on
|
|
5
|
+
* exactly these tables.
|
|
6
6
|
*
|
|
7
7
|
* Enterprise-only tables (chat, routines, watchlist, connectors, LLM config,
|
|
8
8
|
* SharePoint/Atlassian links, feedback, upload, kb-revalidation) live in
|
|
@@ -37,6 +37,19 @@ export const users = pgTable('users', {
|
|
|
37
37
|
* backfill would need, and being shown the setup once costs a click.
|
|
38
38
|
*/
|
|
39
39
|
onboardingDone: boolean('onboarding_done').default(false).notNull(),
|
|
40
|
+
/**
|
|
41
|
+
* When an admin switched this account off; NULL while it is on. A
|
|
42
|
+
* deactivated account keeps its row, its history and its place in roles
|
|
43
|
+
* and groups, but nothing it holds is honoured: it cannot sign in, and its
|
|
44
|
+
* session, connection keys, agent tokens and internal tokens are refused
|
|
45
|
+
* the next time they are presented (see `AuthService.isActive`). Turning it
|
|
46
|
+
* back on restores all of them as they were.
|
|
47
|
+
*
|
|
48
|
+
* The off switch a host that sells seats needs — a seat is an account that
|
|
49
|
+
* is on — and the one an admin uses for someone who left without erasing
|
|
50
|
+
* what they did.
|
|
51
|
+
*/
|
|
52
|
+
deactivatedAt: timestamp('deactivated_at'),
|
|
40
53
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
41
54
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
42
55
|
});
|
|
@@ -160,6 +173,18 @@ export const changeRequests = pgTable('change_requests', {
|
|
|
160
173
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
161
174
|
updatedAt: timestamp('updated_at'),
|
|
162
175
|
closedAt: timestamp('closed_at'),
|
|
176
|
+
/**
|
|
177
|
+
* Why a closed request was closed, when something other than a person
|
|
178
|
+
* closed it. Today the one value is 'repository-replaced': the admin
|
|
179
|
+
* pointed the deployment at a different knowledge-base repository and chose
|
|
180
|
+
* to close the requests rather than keep them. Null for every request
|
|
181
|
+
* closed the ordinary way — by its author, or by the deleted-branch sweep —
|
|
182
|
+
* so the column reads as "nobody recorded a reason", not as a default.
|
|
183
|
+
*
|
|
184
|
+
* The row is never deleted: a replaced repository loses the branches, not
|
|
185
|
+
* the record of what people asked for.
|
|
186
|
+
*/
|
|
187
|
+
closedReason: text('closed_reason'),
|
|
163
188
|
}, (t) => ({
|
|
164
189
|
numberUnq: uniqueIndex('change_requests_number_unq').on(t.number),
|
|
165
190
|
// Enforce the "A→B blocks A→B while open" uniqueness rule at the DB level —
|
|
@@ -382,30 +407,6 @@ export const secrets = pgTable('secrets', {
|
|
|
382
407
|
sharedKeyUnq: uniqueIndex('secrets_shared_key_unq').on(t.key).where(sql`${t.userId} is null`),
|
|
383
408
|
}));
|
|
384
409
|
|
|
385
|
-
/**
|
|
386
|
-
* Ontology-session boundary — the per-session log of which named ontologies a
|
|
387
|
-
* run has TOUCHED (one row per `(session_id, ontology)`). Each agent operation
|
|
388
|
-
* that resolves to a named ontology records a row here (idempotent on the PK).
|
|
389
|
-
*
|
|
390
|
-
* The rule the gate enforces from this set:
|
|
391
|
-
* - READS are always allowed; they just record the ontology they touched.
|
|
392
|
-
* - WRITES are allowed only while the session's touched set is a SINGLE named
|
|
393
|
-
* ontology equal to the write target. Once a session has touched two or
|
|
394
|
-
* more ontologies (e.g. by reading across them), every write is blocked —
|
|
395
|
-
* "read across ontologies → you can't write anymore at all."
|
|
396
|
-
*
|
|
397
|
-
* Postgres is the durable source of truth so the boundary holds across a
|
|
398
|
-
* backend restart (an in-memory set would reset and silently un-poison a run).
|
|
399
|
-
* A write-through in-process cache fronts this table for the chatty read path;
|
|
400
|
-
* a cache miss falls back here.
|
|
401
|
-
*
|
|
402
|
-
* - PK is `(session_id, ontology)` — a set, idempotent on repeat touches.
|
|
403
|
-
* - `ontology` is the resolved ontology id (e.g. `KnowledgeBase/Product`).
|
|
404
|
-
* Neutral touches don't record, so they never write a row.
|
|
405
|
-
* - `touched_at` drives the abandoned-run `sweepOlderThan` backstop; the
|
|
406
|
-
* primary reclamation is an explicit delete of all of a session's rows at
|
|
407
|
-
* run end.
|
|
408
|
-
*/
|
|
409
410
|
/**
|
|
410
411
|
* OAuth clients dynamically registered by MCP clients (RFC 7591 DCR). When an
|
|
411
412
|
* MCP client (claude.ai, Claude Code) connects to `/api/mcp` without a
|
|
@@ -575,6 +576,13 @@ export const agentEvents = pgTable('agent_events', {
|
|
|
575
576
|
),
|
|
576
577
|
}));
|
|
577
578
|
|
|
579
|
+
/**
|
|
580
|
+
* UNUSED IN THIS RELEASE. It backed the per-session boundary that a deployment
|
|
581
|
+
* now owns for itself, behind the agent read/write hooks; nothing in this
|
|
582
|
+
* package reads or writes it any more. The definition stays for ONE release so
|
|
583
|
+
* that no deployment runs a version in which neither side has the table, and
|
|
584
|
+
* the NEXT release drops it (see `TODOS.md`). Do not build on it.
|
|
585
|
+
*/
|
|
578
586
|
export const sessionOntologyTouches = pgTable('session_ontology_touches', {
|
|
579
587
|
sessionId: text('session_id').notNull(),
|
|
580
588
|
ontology: text('ontology').notNull(),
|