@bevel-software/platform-core-backend 0.22.0 → 0.24.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 +46 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +12 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +137 -24
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +46 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +99 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +16 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +28 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- 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.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +17 -16
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +7 -3
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +38 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +39 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +2 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +2 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
- package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +4 -2
- package/dist/modules/audit/agent-audit.service.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-erasure.service.d.ts.map +1 -1
- package/dist/modules/auth/account-erasure.service.js +57 -16
- package/dist/modules/auth/account-erasure.service.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 +70 -2
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +197 -18
- 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/code-mode/code-mode.tool.d.ts +20 -2
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +66 -35
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/connection.d.ts +16 -0
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +117 -0
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +337 -120
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +116 -58
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/database/migrate.d.ts +8 -0
- package/dist/modules/database/migrate.d.ts.map +1 -1
- package/dist/modules/database/migrate.js +271 -1
- package/dist/modules/database/migrate.js.map +1 -1
- 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/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/mcp.service.d.ts +8 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +38 -8
- package/dist/modules/mcp/mcp.service.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-request-records.store.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-records.store.js +7 -4
- package/dist/modules/plugins/join-request-records.store.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +8 -3
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +52 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +207 -19
- package/dist/modules/settings/setup.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 +12 -4
- 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-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/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +80 -0
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
- package/dist/modules/tool-registry/description-length.js +108 -0
- package/dist/modules/tool-registry/description-length.js.map +1 -0
- 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 +75 -35
- 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 +94 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +193 -3
- package/dist/modules/workflow/git/git.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 +60 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +7 -3
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow-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.service.d.ts +60 -6
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +115 -5
- 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/agent-upload.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.routes.js +210 -0
- package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.store.js +553 -0
- package/dist/modules/workspace/agent-upload.store.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 +59 -9
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +46 -4
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/upload-limits.d.ts +13 -0
- package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
- package/dist/modules/workspace/upload-limits.js +13 -0
- package/dist/modules/workspace/upload-limits.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +93 -4
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +128 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +316 -58
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -11
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +791 -198
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/workspace/write-denial.d.ts +0 -6
- package/dist/modules/workspace/write-denial.d.ts.map +1 -1
- package/dist/modules/workspace/write-denial.js +0 -6
- package/dist/modules/workspace/write-denial.js.map +1 -1
- package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
- package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
- package/dist/modules/workspace/zip-entry-rules.js +154 -0
- package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
- 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/column-crypto.d.ts +194 -0
- package/dist/shared/column-crypto.d.ts.map +1 -0
- package/dist/shared/column-crypto.js +144 -0
- package/dist/shared/column-crypto.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +25 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +28 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +20 -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/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -1
- package/dist/shared/token-crypto.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 +1 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/dist/tenancy/tenant-secrets.d.ts +5 -1
- package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
- package/dist/tenancy/tenant-secrets.js +4 -0
- package/dist/tenancy/tenant-secrets.js.map +1 -1
- package/kb-template/AGENTS.md +2 -0
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/0016_pii_encryption.sql +20 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/0016_snapshot.json +2327 -0
- package/migrations/meta/_journal.json +21 -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 +241 -5
- 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 +52 -16
- package/src/core/create-core-services.ts +156 -24
- package/src/core/lifecycle.ts +117 -13
- package/src/core-config.ts +31 -14
- package/src/index.ts +30 -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.routes.prospective.test.ts +6 -7
- package/src/modules/access/__tests__/users-db-double.ts +21 -12
- 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.routes.ts +17 -16
- package/src/modules/access/directory-sync-bot.ts +7 -3
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
- package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
- package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
- package/src/modules/agent-instructions/compose.ts +50 -7
- package/src/modules/agent-instructions/index.ts +12 -0
- package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
- package/src/modules/audit/agent-audit.service.ts +4 -2
- package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
- package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
- package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account-erasure.service.ts +69 -18
- 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 +206 -23
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
- package/src/modules/code-mode/code-mode.tool.ts +80 -34
- package/src/modules/database/__tests__/connection.test.ts +12 -0
- package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
- package/src/modules/database/connection.ts +117 -0
- package/src/modules/database/core-schema.ts +116 -58
- package/src/modules/database/migrate.ts +353 -1
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -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/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.e2e.test.ts +4 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/mcp.service.ts +46 -7
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/plugins/join-request-records.store.ts +7 -4
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
- 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 +8 -3
- package/src/modules/settings/setup.routes.ts +263 -19
- 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__/skills.service.test.ts +229 -5
- 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 +12 -4
- 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-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/tool-manuals/tool-manuals.tools.ts +44 -31
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
- package/src/modules/tool-registry/description-length.ts +111 -0
- 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.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
- 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.prFetchFailure.test.ts +170 -0
- package/src/modules/workflow/git/git.service.ts +215 -1
- package/src/modules/workflow/pending-commits.service.ts +64 -2
- package/src/modules/workflow/recovery-bot.ts +7 -3
- package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
- package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.service.ts +125 -5
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -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.any-workspace-credentials.test.ts +156 -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 +402 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/agent-upload.routes.ts +214 -0
- package/src/modules/workspace/agent-upload.store.ts +668 -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 +86 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
- package/src/modules/workspace/startup/steps/template-source.ts +53 -5
- package/src/modules/workspace/upload-limits.ts +12 -0
- package/src/modules/workspace/workspace.routes.ts +96 -5
- package/src/modules/workspace/workspace.service.ts +319 -61
- package/src/modules/workspace/workspace.tools.ts +894 -214
- package/src/modules/workspace/write-denial.ts +0 -8
- package/src/modules/workspace/zip-entry-rules.ts +173 -0
- 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/__tests__/column-crypto.test.ts +217 -0
- package/src/shared/column-crypto.ts +218 -0
- package/src/shared/domain-errors.ts +31 -0
- package/src/shared/git.contract.ts +27 -0
- package/src/shared/token-crypto.ts +28 -1
- package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
- package/src/tenancy/static-tenant-source.ts +1 -3
- package/src/tenancy/tenant-secrets.ts +5 -1
- 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
|
@@ -3,7 +3,7 @@ import express from 'express';
|
|
|
3
3
|
import type { Server } from 'node:http';
|
|
4
4
|
import { createAccountRoutes } from '../account.routes.js';
|
|
5
5
|
import { AuthService } from '../auth.service.js';
|
|
6
|
-
import { AccountAdmissionRefusedError } from '../account-admission.js';
|
|
6
|
+
import { AccountAdmissionRefusedError, AccountChangeRefusedError } from '../account-admission.js';
|
|
7
7
|
import { hashPassword } from '../password-hash.js';
|
|
8
8
|
import type { Database } from '../../database/connection.js';
|
|
9
9
|
import type { IAdminAccessService } from '../../admin/admin.interface.js';
|
|
@@ -13,6 +13,8 @@ const authService = {
|
|
|
13
13
|
{ id: 'u1', email: 'a@example.com', name: 'A', hasPassword: true, createdAt: new Date() },
|
|
14
14
|
]),
|
|
15
15
|
createAccount: vi.fn(async (email: string) => ({ id: 'u2', email, name: 'B' })),
|
|
16
|
+
deactivate: vi.fn(async (userId: string) => userId !== 'missing'),
|
|
17
|
+
reactivate: vi.fn(async (userId: string) => userId !== 'missing'),
|
|
16
18
|
} as unknown as AuthService;
|
|
17
19
|
|
|
18
20
|
// Satisfies the route's narrow Pick<IAccountErasureService, 'eraseUser'>
|
|
@@ -91,9 +93,11 @@ describe('account routes — admin gate', () => {
|
|
|
91
93
|
{ id: 'u2', email: 'b@example.com', name: 'B', passwordHash, createdAt: new Date() },
|
|
92
94
|
];
|
|
93
95
|
// A real AuthService over a stub db — the route's body is what the
|
|
94
|
-
// service produces from full `users` rows, hash column included.
|
|
96
|
+
// service produces from full `users` rows, hash column included. The
|
|
97
|
+
// listing is sorted in-process (the email column is ciphertext in the
|
|
98
|
+
// database), so the read is a bare `select().from()`.
|
|
95
99
|
const db = {
|
|
96
|
-
select: () => ({ from:
|
|
100
|
+
select: () => ({ from: async () => rows }),
|
|
97
101
|
} as unknown as Database;
|
|
98
102
|
const realAuth = new AuthService(db, {
|
|
99
103
|
jwtSecret: 'test-jwt-secret',
|
|
@@ -116,9 +120,10 @@ describe('account routes — admin gate', () => {
|
|
|
116
120
|
const body = JSON.parse(text) as {
|
|
117
121
|
accounts: Array<{ email: string; hasPassword: boolean; isEnvAdmin: boolean }>;
|
|
118
122
|
};
|
|
123
|
+
// Sorted by email in-process, since the column is ciphertext in the database.
|
|
119
124
|
expect(body.accounts.map((a) => [a.email, a.hasPassword, a.isEnvAdmin])).toEqual([
|
|
120
|
-
['root@example.com', false, true],
|
|
121
125
|
['b@example.com', true, false],
|
|
126
|
+
['root@example.com', false, true],
|
|
122
127
|
]);
|
|
123
128
|
expect(text).not.toContain('scrypt:');
|
|
124
129
|
expect(text).not.toContain('passwordHash');
|
|
@@ -172,12 +177,12 @@ describe('account routes — admin gate', () => {
|
|
|
172
177
|
expect(JSON.stringify(body)).not.toContain('relation');
|
|
173
178
|
});
|
|
174
179
|
|
|
175
|
-
it('400s
|
|
180
|
+
it('400s without an email (a password is optional) and surfaces service validation errors', async () => {
|
|
176
181
|
const base = await listen(makeApp({ admin: true }));
|
|
177
182
|
const missing = await fetch(`${base}/api/admin/accounts`, {
|
|
178
183
|
method: 'POST',
|
|
179
184
|
headers: { 'Content-Type': 'application/json' },
|
|
180
|
-
body: JSON.stringify({
|
|
185
|
+
body: JSON.stringify({ name: 'B' }),
|
|
181
186
|
});
|
|
182
187
|
expect(missing.status).toBe(400);
|
|
183
188
|
|
|
@@ -326,3 +331,79 @@ describe('account routes — removing the erased address from access files', ()
|
|
|
326
331
|
expect(accessRemoval.remove).not.toHaveBeenCalled();
|
|
327
332
|
});
|
|
328
333
|
});
|
|
334
|
+
|
|
335
|
+
describe('account routes — switching an account off and on', () => {
|
|
336
|
+
const post = (base: string, path: string) => fetch(`${base}/api/admin/accounts/${path}`, { method: 'POST' });
|
|
337
|
+
|
|
338
|
+
it('switches another account off and back on', async () => {
|
|
339
|
+
const base = await listen(makeApp({ admin: true }));
|
|
340
|
+
expect((await post(base, 'u2/deactivate')).status).toBe(204);
|
|
341
|
+
expect(authService.deactivate).toHaveBeenCalledWith('u2');
|
|
342
|
+
expect((await post(base, 'u2/reactivate')).status).toBe(204);
|
|
343
|
+
expect(authService.reactivate).toHaveBeenCalledWith('u2');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('refuses an admin switching off their own account, so an admin who can sign in always remains', async () => {
|
|
347
|
+
vi.mocked(authService.deactivate).mockClear();
|
|
348
|
+
const base = await listen(makeApp({ admin: true }));
|
|
349
|
+
const res = await post(base, 'u1/deactivate');
|
|
350
|
+
expect(res.status).toBe(400);
|
|
351
|
+
expect(authService.deactivate).not.toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('refuses non-admins', async () => {
|
|
355
|
+
vi.mocked(authService.deactivate).mockClear();
|
|
356
|
+
const base = await listen(makeApp({ admin: false }));
|
|
357
|
+
expect((await post(base, 'u2/deactivate')).status).toBe(403);
|
|
358
|
+
expect((await post(base, 'u2/reactivate')).status).toBe(403);
|
|
359
|
+
expect(authService.deactivate).not.toHaveBeenCalled();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('404s for an account that does not exist', async () => {
|
|
363
|
+
const base = await listen(makeApp({ admin: true }));
|
|
364
|
+
expect((await post(base, 'missing/deactivate')).status).toBe(404);
|
|
365
|
+
expect((await post(base, 'missing/reactivate')).status).toBe(404);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("answers a reactivation the deployment has no room for with 403 and the port's words", async () => {
|
|
369
|
+
vi.mocked(authService.reactivate).mockRejectedValueOnce(new AccountAdmissionRefusedError('All 3 seats are taken'));
|
|
370
|
+
const base = await listen(makeApp({ admin: true }));
|
|
371
|
+
const res = await post(base, 'u2/reactivate');
|
|
372
|
+
expect(res.status).toBe(403);
|
|
373
|
+
expect(((await res.json()) as { error: string }).error).toBe('All 3 seats are taken');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('answers a refused deactivation (the deployment admin, say) with 400 and the reason', async () => {
|
|
377
|
+
vi.mocked(authService.deactivate).mockRejectedValueOnce(new AccountChangeRefusedError('The deployment admin cannot be switched off'));
|
|
378
|
+
const base = await listen(makeApp({ admin: true }));
|
|
379
|
+
const res = await post(base, 'u2/deactivate');
|
|
380
|
+
expect(res.status).toBe(400);
|
|
381
|
+
expect(((await res.json()) as { error: string }).error).toContain('deployment admin');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('answers an unexpected failure with a 500 that names nothing of it', async () => {
|
|
385
|
+
vi.mocked(authService.deactivate).mockRejectedValueOnce(new Error('relation "users" does not exist'));
|
|
386
|
+
vi.mocked(authService.reactivate).mockRejectedValueOnce(new Error('relation "users" does not exist'));
|
|
387
|
+
const base = await listen(makeApp({ admin: true }));
|
|
388
|
+
const off = await post(base, 'u2/deactivate');
|
|
389
|
+
expect(off.status).toBe(500);
|
|
390
|
+
expect(JSON.stringify(await off.json())).not.toContain('relation');
|
|
391
|
+
const on = await post(base, 'u2/reactivate');
|
|
392
|
+
expect(on.status).toBe(500);
|
|
393
|
+
expect(JSON.stringify(await on.json())).not.toContain('relation');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('refuses a password that is not a string, rather than making an account for single sign-on', async () => {
|
|
397
|
+
vi.mocked(authService.createAccount).mockClear();
|
|
398
|
+
const base = await listen(makeApp({ admin: true }));
|
|
399
|
+
for (const password of [false, null, 0]) {
|
|
400
|
+
const res = await fetch(`${base}/api/admin/accounts`, {
|
|
401
|
+
method: 'POST',
|
|
402
|
+
headers: { 'Content-Type': 'application/json' },
|
|
403
|
+
body: JSON.stringify({ email: 'b@example.com', password }),
|
|
404
|
+
});
|
|
405
|
+
expect(res.status).toBe(400);
|
|
406
|
+
}
|
|
407
|
+
expect(authService.createAccount).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import type { Request, Response, NextFunction } from 'express';
|
|
3
3
|
import { createAuthMiddleware, AUTH_COOKIE_NAME } from '../auth.middleware.js';
|
|
4
|
+
import { AccountDeactivatedError, AuthBackendError } from '../account-admission.js';
|
|
4
5
|
import type { AuthService } from '../auth.service.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -11,68 +12,90 @@ import type { AuthService } from '../auth.service.js';
|
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
const GOOD_TOKEN = 'good-token';
|
|
15
|
+
const OFF_TOKEN = 'switched-off-token';
|
|
16
|
+
const DOWN_TOKEN = 'database-down-token';
|
|
14
17
|
const IDENTITY = { userId: 'user-1', email: 'alice@example.com' };
|
|
15
18
|
|
|
16
|
-
function run(headers: Record<string, string>) {
|
|
17
|
-
const
|
|
19
|
+
async function run(headers: Record<string, string>) {
|
|
20
|
+
const resolveSession = vi.fn(async (token: string) => {
|
|
21
|
+
if (token === OFF_TOKEN) throw new AccountDeactivatedError();
|
|
22
|
+
if (token === DOWN_TOKEN) throw new AuthBackendError(new Error('db down'));
|
|
18
23
|
if (token !== GOOD_TOKEN) throw new Error('bad token');
|
|
19
24
|
return IDENTITY;
|
|
20
25
|
});
|
|
21
|
-
const authService = {
|
|
26
|
+
const authService = { resolveSession } as unknown as AuthService;
|
|
22
27
|
const req = { headers } as unknown as Request;
|
|
23
28
|
const json = vi.fn();
|
|
24
29
|
const status = vi.fn(() => ({ json }));
|
|
25
30
|
const res = { status } as unknown as Response;
|
|
26
31
|
const next = vi.fn() as unknown as NextFunction;
|
|
27
|
-
createAuthMiddleware(authService)(req, res, next);
|
|
28
|
-
return { req, next, status, json,
|
|
32
|
+
await createAuthMiddleware(authService)(req, res, next);
|
|
33
|
+
return { req, next, status, json, resolveSession };
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
describe('createAuthMiddleware', () => {
|
|
32
|
-
it('authenticates a request that carries only the auth cookie (an <img>, an EventSource)', () => {
|
|
33
|
-
const { req, next, status } = run({ cookie: `${AUTH_COOKIE_NAME}=${GOOD_TOKEN}` });
|
|
36
|
+
describe('createAuthMiddleware', async () => {
|
|
37
|
+
it('authenticates a request that carries only the auth cookie (an <img>, an EventSource)', async () => {
|
|
38
|
+
const { req, next, status } = await run({ cookie: `${AUTH_COOKIE_NAME}=${GOOD_TOKEN}` });
|
|
34
39
|
expect(next).toHaveBeenCalledTimes(1);
|
|
35
40
|
expect(status).not.toHaveBeenCalled();
|
|
36
41
|
expect(req.userId).toBe(IDENTITY.userId);
|
|
37
42
|
expect(req.userEmail).toBe(IDENTITY.email);
|
|
38
43
|
});
|
|
39
44
|
|
|
40
|
-
it('finds the cookie among others, and decodes a percent-encoded value', () => {
|
|
41
|
-
const { next,
|
|
45
|
+
it('finds the cookie among others, and decodes a percent-encoded value', async () => {
|
|
46
|
+
const { next, resolveSession } = await run({
|
|
42
47
|
cookie: `theme=dark; ${AUTH_COOKIE_NAME}=${encodeURIComponent(GOOD_TOKEN)}; seen=1`,
|
|
43
48
|
});
|
|
44
|
-
expect(
|
|
49
|
+
expect(resolveSession).toHaveBeenCalledWith(GOOD_TOKEN);
|
|
45
50
|
expect(next).toHaveBeenCalledTimes(1);
|
|
46
51
|
});
|
|
47
52
|
|
|
48
|
-
it('prefers the Bearer header when both are present', () => {
|
|
49
|
-
const { next,
|
|
53
|
+
it('prefers the Bearer header when both are present', async () => {
|
|
54
|
+
const { next, resolveSession } = await run({
|
|
50
55
|
authorization: `Bearer ${GOOD_TOKEN}`,
|
|
51
56
|
cookie: `${AUTH_COOKIE_NAME}=stale-cookie`,
|
|
52
57
|
});
|
|
53
|
-
expect(
|
|
54
|
-
expect(
|
|
58
|
+
expect(resolveSession).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(resolveSession).toHaveBeenCalledWith(GOOD_TOKEN);
|
|
55
60
|
expect(next).toHaveBeenCalledTimes(1);
|
|
56
61
|
});
|
|
57
62
|
|
|
58
|
-
it('refuses a request with neither', () => {
|
|
59
|
-
const { next, status, json } = run({});
|
|
63
|
+
it('refuses a request with neither', async () => {
|
|
64
|
+
const { next, status, json } = await run({});
|
|
60
65
|
expect(next).not.toHaveBeenCalled();
|
|
61
66
|
expect(status).toHaveBeenCalledWith(401);
|
|
62
67
|
expect(json.mock.calls[0][0].error).toContain('auth cookie');
|
|
63
68
|
});
|
|
64
69
|
|
|
65
|
-
it('refuses an empty cookie value as missing, not as a token to verify', () => {
|
|
66
|
-
const { next, status,
|
|
67
|
-
expect(
|
|
70
|
+
it('refuses an empty cookie value as missing, not as a token to verify', async () => {
|
|
71
|
+
const { next, status, resolveSession } = await run({ cookie: `${AUTH_COOKIE_NAME}=` });
|
|
72
|
+
expect(resolveSession).not.toHaveBeenCalled();
|
|
68
73
|
expect(next).not.toHaveBeenCalled();
|
|
69
74
|
expect(status).toHaveBeenCalledWith(401);
|
|
70
75
|
});
|
|
71
76
|
|
|
72
|
-
it('refuses a cookie the auth service rejects', () => {
|
|
73
|
-
const { next, status, json } = run({ cookie: `${AUTH_COOKIE_NAME}=forged` });
|
|
77
|
+
it('refuses a cookie the auth service rejects', async () => {
|
|
78
|
+
const { next, status, json } = await run({ cookie: `${AUTH_COOKIE_NAME}=forged` });
|
|
74
79
|
expect(next).not.toHaveBeenCalled();
|
|
75
80
|
expect(status).toHaveBeenCalledWith(401);
|
|
76
81
|
expect(json.mock.calls[0][0].error).toBe('Invalid or expired token');
|
|
77
82
|
});
|
|
78
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('createAuthMiddleware — a switched-off account', () => {
|
|
86
|
+
it('refuses a valid session whose account an admin switched off, and says why', async () => {
|
|
87
|
+
const { next, status, json } = await run({ authorization: `Bearer ${OFF_TOKEN}` });
|
|
88
|
+
expect(next).not.toHaveBeenCalled();
|
|
89
|
+
expect(status).toHaveBeenCalledWith(401);
|
|
90
|
+
expect(json.mock.calls[0][0].error).toContain('switched off');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('createAuthMiddleware — the account cannot be looked up', () => {
|
|
95
|
+
it('500s rather than signing a valid session out during an outage', async () => {
|
|
96
|
+
const { next, status, json } = await run({ authorization: `Bearer ${DOWN_TOKEN}` });
|
|
97
|
+
expect(next).not.toHaveBeenCalled();
|
|
98
|
+
expect(status).toHaveBeenCalledWith(500);
|
|
99
|
+
expect(json.mock.calls[0][0].error).toBe('Authentication backend unavailable');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -521,9 +521,11 @@ describe('AuthService.listAccounts', () => {
|
|
|
521
521
|
const config = makeConfig({ adminEmail: 'root@example.com', adminPassword: 'sup3r-secret' });
|
|
522
522
|
|
|
523
523
|
const withHash = await new AuthService(makeFakeDb([rows]).db, config).listAccounts();
|
|
524
|
+
// Sorted by email in-process: the email column is ciphertext in the
|
|
525
|
+
// database, so the listing orders itself after decrypting.
|
|
524
526
|
expect(withHash.map((a) => [a.email, a.hasPassword, a.isEnvAdmin])).toEqual([
|
|
525
|
-
['root@example.com', true, true],
|
|
526
527
|
['bob@example.com', true, false],
|
|
528
|
+
['root@example.com', true, true],
|
|
527
529
|
['sso@example.com', false, false],
|
|
528
530
|
]);
|
|
529
531
|
|
|
@@ -537,7 +539,7 @@ describe('AuthService.listAccounts', () => {
|
|
|
537
539
|
expect(json).not.toContain('sup3r-secret');
|
|
538
540
|
for (const account of [...withHash, ...withoutHash]) {
|
|
539
541
|
expect(Object.keys(account).sort()).toEqual(
|
|
540
|
-
['createdAt', 'email', 'hasPassword', 'id', 'isEnvAdmin', 'name'],
|
|
542
|
+
['createdAt', 'deactivatedAt', 'email', 'hasPassword', 'id', 'isEnvAdmin', 'isSystem', 'name'],
|
|
541
543
|
);
|
|
542
544
|
}
|
|
543
545
|
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Whether a NEW account may be provisioned on this deployment, asked before
|
|
3
|
-
* the row is inserted
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* the row is inserted — and whether a deactivated one may be switched back
|
|
4
|
+
* on, which takes up the same place. The one seam a host that sells seats
|
|
5
|
+
* needs: core admits everyone, and a cloud overlay answers from its plan
|
|
6
|
+
* without forking any sign-in path.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
* is
|
|
9
|
-
* already in
|
|
8
|
+
* Never asked for an account that is already on. An existing user's sign-in
|
|
9
|
+
* is not a provisioning, so a refusal can never lock out someone who is
|
|
10
|
+
* already in; taking someone out is the admin's act (deactivation), not the
|
|
11
|
+
* port's.
|
|
10
12
|
*/
|
|
11
13
|
export type AccountProvisionReason =
|
|
12
14
|
/** A first sign-in through single sign-on, which provisions on its own. */
|
|
@@ -16,9 +18,29 @@ export type AccountProvisionReason =
|
|
|
16
18
|
/** The deployment owner's first password sign-in, which creates their row. */
|
|
17
19
|
| 'bootstrap'
|
|
18
20
|
/** A verified identity from an embedding surface that needs a row without a session. */
|
|
19
|
-
| 'embed'
|
|
21
|
+
| 'embed'
|
|
22
|
+
/** An admin switching a deactivated account back on. */
|
|
23
|
+
| 'reactivate';
|
|
20
24
|
|
|
21
|
-
export type AccountAdmissionVerdict =
|
|
25
|
+
export type AccountAdmissionVerdict =
|
|
26
|
+
| { ok: true }
|
|
27
|
+
| {
|
|
28
|
+
ok: false;
|
|
29
|
+
message: string;
|
|
30
|
+
/**
|
|
31
|
+
* Keep the person on file instead of turning them away: the account is
|
|
32
|
+
* created DEACTIVATED, the sign-in is still refused with `message`, and
|
|
33
|
+
* an admin finds them on the accounts list, ready to be switched on.
|
|
34
|
+
* For a host whose rule is "whoever is on our domain may join while
|
|
35
|
+
* there is room" — the one past the room waits instead of bouncing.
|
|
36
|
+
*
|
|
37
|
+
* Honoured only for a sign-in through single sign-on, the one
|
|
38
|
+
* provisioning nobody asked for. Everywhere else a verdict carrying it
|
|
39
|
+
* is a plain refusal: an admin creating an account would be told no,
|
|
40
|
+
* and a deactivated account is never reactivated into one.
|
|
41
|
+
*/
|
|
42
|
+
waitForAdmin?: boolean;
|
|
43
|
+
};
|
|
22
44
|
|
|
23
45
|
export interface IAccountAdmission {
|
|
24
46
|
canProvision(email: string, reason: AccountProvisionReason): Promise<AccountAdmissionVerdict>;
|
|
@@ -36,9 +58,56 @@ export const admitEveryone: IAccountAdmission = {
|
|
|
36
58
|
*/
|
|
37
59
|
export class AccountAdmissionRefusedError extends Error {
|
|
38
60
|
readonly status = 403;
|
|
61
|
+
/** The person was put on file, switched off, for an admin to switch on (see `waitForAdmin`). */
|
|
62
|
+
readonly waitingForAdmin: boolean;
|
|
39
63
|
|
|
40
|
-
constructor(message: string) {
|
|
64
|
+
constructor(message: string, opts: { waitingForAdmin?: boolean } = {}) {
|
|
41
65
|
super(message);
|
|
42
66
|
this.name = 'AccountAdmissionRefusedError';
|
|
67
|
+
this.waitingForAdmin = opts.waitingForAdmin ?? false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The account exists and an admin switched it off — at sign-in, or for a
|
|
73
|
+
* credential it still holds. A 403 like a refusal: the identity is fine,
|
|
74
|
+
* the deployment no longer lets it in.
|
|
75
|
+
*/
|
|
76
|
+
export class AccountDeactivatedError extends Error {
|
|
77
|
+
readonly status = 403;
|
|
78
|
+
|
|
79
|
+
constructor(message = ACCOUNT_DEACTIVATED_MESSAGE) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = 'AccountDeactivatedError';
|
|
43
82
|
}
|
|
44
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether the account behind a credential is on could not be asked — the
|
|
87
|
+
* database failed. Not the caller's fault and not a verdict on them: a
|
|
88
|
+
* route answers it with a 500, never with the 401 that would sign a valid
|
|
89
|
+
* caller out during an outage.
|
|
90
|
+
*/
|
|
91
|
+
export class AuthBackendError extends Error {
|
|
92
|
+
constructor(readonly cause: unknown) {
|
|
93
|
+
super('The account could not be looked up');
|
|
94
|
+
this.name = 'AuthBackendError';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* An admin's change to an account that the deployment refuses on purpose
|
|
100
|
+
* (switching off the deployment admin, or an account the platform runs its
|
|
101
|
+
* own work as). Its message is for the admin; a route answers it with 400.
|
|
102
|
+
*/
|
|
103
|
+
export class AccountChangeRefusedError extends Error {
|
|
104
|
+
readonly status = 400;
|
|
105
|
+
|
|
106
|
+
constructor(message: string) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = 'AccountChangeRefusedError';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** What a person with a deactivated account is told, wherever they knock. */
|
|
113
|
+
export const ACCOUNT_DEACTIVATED_MESSAGE = 'Your account on this workspace is switched off. Ask its admin to turn it back on.';
|
|
@@ -3,6 +3,7 @@ import { logger } from '../../shared/logging.js';
|
|
|
3
3
|
|
|
4
4
|
const log = logger('account-erasure');
|
|
5
5
|
import { and, eq, notExists } from 'drizzle-orm';
|
|
6
|
+
import { isEncryptedBlob } from '../../shared/column-crypto.js';
|
|
6
7
|
import type { Database } from '../database/connection.js';
|
|
7
8
|
import type { IReviewWorkflowService } from '../workflow/review-workflow/review-workflow.interface.js';
|
|
8
9
|
import {
|
|
@@ -123,17 +124,34 @@ export class AccountErasureService implements IAccountErasureService {
|
|
|
123
124
|
) {}
|
|
124
125
|
|
|
125
126
|
async listUsers(): Promise<AdminUserView[]> {
|
|
127
|
+
// Sorted in-process: `email` is ciphertext in the database, so ORDER BY
|
|
128
|
+
// would sort by IV noise. One row per team member.
|
|
126
129
|
const rows = await this.db
|
|
127
130
|
.select({ id: users.id, email: users.email, name: users.name, createdAt: users.createdAt })
|
|
128
|
-
.from(users)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
.from(users);
|
|
132
|
+
return rows
|
|
133
|
+
.map((r) => ({ ...r, createdAt: r.createdAt.getTime() }))
|
|
134
|
+
.sort((a, b) => a.email.localeCompare(b.email));
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
async eraseUser(userId: string, opts: { erasureId?: string } = {}): Promise<boolean> {
|
|
134
138
|
const [user] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
|
|
135
139
|
if (!user) return false;
|
|
136
140
|
|
|
141
|
+
// Fail CLOSED on an undecryptable email. The encrypted column's read
|
|
142
|
+
// fallback returns the raw ciphertext when the configured key cannot open
|
|
143
|
+
// it (a rotated or wrong SECRETS_ENC_KEY); a blind index of that blob
|
|
144
|
+
// would match zero audit rows while the user delete still committed — an
|
|
145
|
+
// erasure that silently left the personal data behind. Nothing is
|
|
146
|
+
// deleted until the key is fixed.
|
|
147
|
+
if (isEncryptedBlob(user.email)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`account-erasure: cannot decrypt the stored email for user id=${userId} with the ` +
|
|
150
|
+
'configured SECRETS_ENC_KEY — refusing to erase, because the email-keyed audit rows ' +
|
|
151
|
+
'could not be anonymised. Restore the correct key, then retry.',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
const target: ErasureTarget = {
|
|
138
156
|
userId,
|
|
139
157
|
email: user.email.toLowerCase(),
|
|
@@ -149,6 +167,14 @@ export class AccountErasureService implements IAccountErasureService {
|
|
|
149
167
|
if (p.before) await p.before(target);
|
|
150
168
|
}
|
|
151
169
|
|
|
170
|
+
// Every email-keyed row below is matched through its blind index — the
|
|
171
|
+
// email columns are randomized ciphertext — and the index is rewritten to
|
|
172
|
+
// the placeholder's too, so no value keyed to the erased address survives.
|
|
173
|
+
// An index column is compared and written with the address it is the
|
|
174
|
+
// index of; the handle the statement runs on makes the index.
|
|
175
|
+
const emailBidx = target.email;
|
|
176
|
+
const erasedBidx = target.erasedEmail;
|
|
177
|
+
|
|
152
178
|
const postCommit: Array<() => Promise<void>> = [];
|
|
153
179
|
await this.db.transaction(async (tx) => {
|
|
154
180
|
// Token-shaped rows (all hashed, but they key to the user). Dependents
|
|
@@ -180,7 +206,7 @@ export class AccountErasureService implements IAccountErasureService {
|
|
|
180
206
|
// writing anything back — so no row is resurrected here either.
|
|
181
207
|
await tx
|
|
182
208
|
.delete(pluginJoinRequests)
|
|
183
|
-
.where(eq(pluginJoinRequests.
|
|
209
|
+
.where(eq(pluginJoinRequests.requesterEmailBidx, emailBidx));
|
|
184
210
|
|
|
185
211
|
// Audit rows: anonymize in place (no user FK on these; they key by email).
|
|
186
212
|
//
|
|
@@ -197,23 +223,34 @@ export class AccountErasureService implements IAccountErasureService {
|
|
|
197
223
|
});
|
|
198
224
|
await tx
|
|
199
225
|
.update(prMergeLog)
|
|
200
|
-
.set({ triggeredByEmail: target.erasedEmail, triggeredByName: target.erasedName })
|
|
201
|
-
.where(eq(prMergeLog.
|
|
226
|
+
.set({ triggeredByEmail: target.erasedEmail, triggeredByEmailBidx: erasedBidx, triggeredByName: target.erasedName })
|
|
227
|
+
.where(eq(prMergeLog.triggeredByEmailBidx, emailBidx));
|
|
202
228
|
await tx
|
|
203
229
|
.update(prComments)
|
|
204
|
-
.set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
|
|
205
|
-
.where(eq(prComments.
|
|
230
|
+
.set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
|
|
231
|
+
.where(eq(prComments.authorEmailBidx, emailBidx));
|
|
206
232
|
await tx
|
|
207
233
|
.update(changeRequests)
|
|
208
|
-
.set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
|
|
209
|
-
.where(eq(changeRequests.
|
|
234
|
+
.set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
|
|
235
|
+
.where(eq(changeRequests.authorEmailBidx, emailBidx));
|
|
236
|
+
// The refusal a still-open request shows ("<name> could not apply
|
|
237
|
+
// this"), on requests of any author — found by the index of the address
|
|
238
|
+
// it was recorded with, like every other row. One recorded before the
|
|
239
|
+
// encryption release kept no address, and has no name to find either:
|
|
240
|
+
// the first start on this release took it off (`migrate.ts`,
|
|
241
|
+
// `clearUnindexedRefusalNames`), because matching such a row by display
|
|
242
|
+
// name missed a person who had been renamed and hit their namesake.
|
|
243
|
+
await tx
|
|
244
|
+
.update(changeRequests)
|
|
245
|
+
.set({ applyFailedByName: target.erasedName, applyFailedByEmailBidx: erasedBidx })
|
|
246
|
+
.where(eq(changeRequests.applyFailedByEmailBidx, emailBidx));
|
|
210
247
|
// Queued-but-uncommitted saves: the eventual git commit is authored with
|
|
211
248
|
// the placeholder instead of the erased identity. The file content still
|
|
212
249
|
// lands — erasing an account must not lose other people's KB state.
|
|
213
250
|
await tx
|
|
214
251
|
.update(pendingCommits)
|
|
215
|
-
.set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
|
|
216
|
-
.where(eq(pendingCommits.
|
|
252
|
+
.set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
|
|
253
|
+
.where(eq(pendingCommits.authorEmailBidx, emailBidx));
|
|
217
254
|
|
|
218
255
|
// Module-owned rows, before the users delete so FKs onto users are
|
|
219
256
|
// still satisfiable — and so a participant that MISSES rows makes the
|
|
@@ -251,11 +288,11 @@ export class AccountErasureService implements IAccountErasureService {
|
|
|
251
288
|
// approvals they make are their own.
|
|
252
289
|
await this.db
|
|
253
290
|
.update(prFileApprovals)
|
|
254
|
-
.set({ approverEmail: target.erasedEmail, approverName: target.erasedName })
|
|
291
|
+
.set({ approverEmail: target.erasedEmail, approverEmailBidx: erasedBidx, approverName: target.erasedName })
|
|
255
292
|
.where(
|
|
256
293
|
and(
|
|
257
|
-
eq(prFileApprovals.
|
|
258
|
-
notExists(this.db.select({ id: users.id }).from(users).where(eq(users.
|
|
294
|
+
eq(prFileApprovals.approverEmailBidx, emailBidx),
|
|
295
|
+
notExists(this.db.select({ id: users.id }).from(users).where(eq(users.emailBidx, emailBidx))),
|
|
259
296
|
),
|
|
260
297
|
);
|
|
261
298
|
|
|
@@ -272,11 +309,25 @@ export class AccountErasureService implements IAccountErasureService {
|
|
|
272
309
|
// person, and their requests are their own.
|
|
273
310
|
await this.db
|
|
274
311
|
.update(changeRequests)
|
|
275
|
-
.set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
|
|
312
|
+
.set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
|
|
313
|
+
.where(
|
|
314
|
+
and(
|
|
315
|
+
eq(changeRequests.authorEmailBidx, emailBidx),
|
|
316
|
+
notExists(this.db.select({ id: users.id }).from(users).where(eq(users.emailBidx, emailBidx))),
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// And the refusal note, for the apply this person was inside when the
|
|
321
|
+
// commit landed: recording it takes no lock and names no user row, so it
|
|
322
|
+
// can write their name after the rewrite above. Same statement, same
|
|
323
|
+
// guard, same reason.
|
|
324
|
+
await this.db
|
|
325
|
+
.update(changeRequests)
|
|
326
|
+
.set({ applyFailedByName: target.erasedName, applyFailedByEmailBidx: erasedBidx })
|
|
276
327
|
.where(
|
|
277
328
|
and(
|
|
278
|
-
eq(changeRequests.
|
|
279
|
-
notExists(this.db.select({ id: users.id }).from(users).where(eq(users.
|
|
329
|
+
eq(changeRequests.applyFailedByEmailBidx, emailBidx),
|
|
330
|
+
notExists(this.db.select({ id: users.id }).from(users).where(eq(users.emailBidx, emailBidx))),
|
|
280
331
|
),
|
|
281
332
|
);
|
|
282
333
|
|
|
@@ -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
|