@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
3
|
+
import { sanitizedPath } from '../../shared/printable.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Workspace-relative paths and the repository folder.
|
|
@@ -20,6 +21,51 @@ import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
|
20
21
|
* rule were allowed to exist.
|
|
21
22
|
*/
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* NUL, the C0 controls (a line break among them) and DEL — refused in a
|
|
26
|
+
* workspace path wherever one enters.
|
|
27
|
+
*
|
|
28
|
+
* Not merely hygiene about names nobody can type. A `\n` is a SEPARATOR in
|
|
29
|
+
* git's own line-oriented stdin protocols: `cat-file --batch` reads one
|
|
30
|
+
* `<ref>:<path>` object name per LINE, so `Docs/secret.md\nzzz` is TWO specs.
|
|
31
|
+
* Everything upstream — the read gate, the download gate, the not-a-directory
|
|
32
|
+
* check — is then asked about the whole string, which names nothing, while git
|
|
33
|
+
* answers about `Docs/secret.md` and hands over its bytes. A file denied by
|
|
34
|
+
* its own frontmatter inside a folder its reader may list would be readable
|
|
35
|
+
* that way, and in a batch of several specs the extra answer shifts every
|
|
36
|
+
* result after it, so the access resolver reads one directory's `access.md` as
|
|
37
|
+
* another's.
|
|
38
|
+
*
|
|
39
|
+
* Refused HERE, at the normaliser every accepted path goes through, rather
|
|
40
|
+
* than at each route: the fence has to be at the mechanism, because the next
|
|
41
|
+
* caller to assemble a git spec from a path will not remember this. The
|
|
42
|
+
* shared `validateFilename` has always refused the same range per segment, so
|
|
43
|
+
* no name any surface can create is lost — this closes the gap between it and
|
|
44
|
+
* the paths that reach git. U+2028/U+2029 are NOT in it: they are nothing to
|
|
45
|
+
* git's protocols (which read bytes, and a line break there is 0x0A), a name
|
|
46
|
+
* carrying one is creatable today, and refusing it here would make an existing
|
|
47
|
+
* file unreadable. They are escaped for display instead, which is what
|
|
48
|
+
* `sanitizedPath` is for.
|
|
49
|
+
*/
|
|
50
|
+
// eslint-disable-next-line no-control-regex
|
|
51
|
+
const CONTROL_CHARACTERS = /[\x00-\x1F\x7F]/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether `value` carries one of them — THE predicate, so the places that
|
|
55
|
+
* accept a NAME enforce the same rule as the normaliser that joins it into a
|
|
56
|
+
* path. The checkout folder name is the one that matters: it is not a caller's
|
|
57
|
+
* path, it is deployment configuration (`KB_DIR_NAME`, and the `kbDirName`
|
|
58
|
+
* setting), and it is PREFIXED onto every workspace-relative path. A name with
|
|
59
|
+
* a line break in it would therefore make the normaliser emit exactly the path
|
|
60
|
+
* this rule exists to exclude, from inputs that were each clean. Refused where
|
|
61
|
+
* the name is set — at boot in `core-config` and in the settings validator —
|
|
62
|
+
* so the deployment fails loudly instead of serving a path nothing downstream
|
|
63
|
+
* can safely read.
|
|
64
|
+
*/
|
|
65
|
+
export function hasControlCharacter(value: string): boolean {
|
|
66
|
+
return CONTROL_CHARACTERS.test(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
23
69
|
/**
|
|
24
70
|
* True when `wsPath` is the repository folder or lies under it.
|
|
25
71
|
*
|
|
@@ -33,6 +79,10 @@ import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
|
33
79
|
*/
|
|
34
80
|
export function isInsideRepo(wsPath: string, kbDirName: string): boolean {
|
|
35
81
|
if (typeof wsPath !== 'string' || wsPath.includes('\\')) return false;
|
|
82
|
+
// A control character is not a spelling of any path under the repository —
|
|
83
|
+
// see {@link CONTROL_CHARACTERS}. Refused here too, so the check AFTER the
|
|
84
|
+
// normaliser cannot pass one that somehow skipped it.
|
|
85
|
+
if (CONTROL_CHARACTERS.test(wsPath)) return false;
|
|
36
86
|
const segments = wsPath.replace(/\/$/, '').split('/');
|
|
37
87
|
if (segments[0] !== kbDirName) return false;
|
|
38
88
|
return segments.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
@@ -52,6 +102,22 @@ export function isInsideRepo(wsPath: string, kbDirName: string): boolean {
|
|
|
52
102
|
*/
|
|
53
103
|
export function assertInsideRepo(wsPath: string, kbDirName: string): void {
|
|
54
104
|
if (isInsideRepo(wsPath, kbDirName)) return;
|
|
105
|
+
// Said as what it is, before the suggestion below: a control character is
|
|
106
|
+
// not a path that landed in the wrong place, and a "use this instead"
|
|
107
|
+
// carrying an invisible character back to the caller would be nonsense.
|
|
108
|
+
if (typeof wsPath === 'string' && CONTROL_CHARACTERS.test(wsPath)) {
|
|
109
|
+
// Through `sanitizedPath`, like every other refusal that names a path: the
|
|
110
|
+
// character that makes this path invalid is also the character that would
|
|
111
|
+
// forge a second line of the answer, in the message and in the `path` a
|
|
112
|
+
// JSON consumer reads.
|
|
113
|
+
const shown = sanitizedPath(wsPath);
|
|
114
|
+
throw new WorkflowValidationError(
|
|
115
|
+
`"${shown}" contains a control character: a line break, a NUL or another invisible byte is not part of any ` +
|
|
116
|
+
'file name, and git reads a line break in a path as the end of one object name and the start of another. ' +
|
|
117
|
+
'Remove it and send the name as it is written.',
|
|
118
|
+
{ kind: 'path-control-character', path: shown, kbDirName },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
55
121
|
// The suggestion collapses what made the path wrong: backslashes, leading
|
|
56
122
|
// `./` or `/`, and any `..` climbing back out (a bare `..` included, so the
|
|
57
123
|
// suggestion is never itself a climb). The agent gets a path it can use.
|
|
@@ -103,6 +169,25 @@ export function normalizeWorkspacePath(wsPath: string, kbDirName: string): strin
|
|
|
103
169
|
// `\` separates segments on Windows, so `foo\..\..` is a climb this cannot
|
|
104
170
|
// read. Refused before anything is collapsed, never rewritten to `/`.
|
|
105
171
|
if (wsPath.includes('\\')) refuse();
|
|
172
|
+
// A line break (or any other control character) is a SEPARATOR to git, not
|
|
173
|
+
// part of a name — see {@link CONTROL_CHARACTERS}. Refused before the path
|
|
174
|
+
// is normalised, so no gate downstream is ever asked about a string that
|
|
175
|
+
// means one thing to it and two things to git.
|
|
176
|
+
if (CONTROL_CHARACTERS.test(wsPath)) refuse();
|
|
177
|
+
// …and the FOLDER NAME, which is prefixed onto the result rather than read
|
|
178
|
+
// from it: a clean path joined to `kb\nname` comes out carrying the line
|
|
179
|
+
// break, so the normaliser would itself emit what the check above refuses.
|
|
180
|
+
// Said as the configuration error it is — the caller's path is fine, and an
|
|
181
|
+
// operator reading a refusal about their own path would have nothing to fix.
|
|
182
|
+
// `core-config` and the `kbDirName` setting validator both refuse this at
|
|
183
|
+
// the point it is SET, so reaching here means the two drifted.
|
|
184
|
+
if (CONTROL_CHARACTERS.test(kbDirName)) {
|
|
185
|
+
throw new WorkflowValidationError(
|
|
186
|
+
`the configured checkout folder name (kbDirName / KB_DIR_NAME) contains a control character: "${sanitizedPath(kbDirName)}". ` +
|
|
187
|
+
'Every workspace path is prefixed with it, so no path can be resolved until the setting is corrected.',
|
|
188
|
+
{ kind: 'kb-dir-name-control-character', kbDirName: sanitizedPath(kbDirName) },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
106
191
|
// The root-anchored form, and ONLY for the repository folder: `/<kbDirName>/…`
|
|
107
192
|
// is what the app's Copy path gives (it is the form a Markdown link resolves
|
|
108
193
|
// from) and names the same workspace path. `/tmp/x` and `//x` are not that
|
|
@@ -4,6 +4,7 @@ import express from 'express';
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { createKbSyncRoutes, isSyncRawBodyPath } from '../kb-sync.routes.js';
|
|
6
6
|
import type { IKbSyncService, SyncResult } from '../kb-sync.interface.js';
|
|
7
|
+
import { AuthBackendError } from '../../auth/account-admission.js';
|
|
7
8
|
|
|
8
9
|
const SECRET = 'a-very-long-and-random-sync-secret';
|
|
9
10
|
|
|
@@ -23,6 +24,8 @@ async function mount(opts: {
|
|
|
23
24
|
secret?: string;
|
|
24
25
|
result?: SyncResult;
|
|
25
26
|
admin?: boolean;
|
|
27
|
+
/** No account can be looked up: every session check fails as the database being down. */
|
|
28
|
+
outage?: boolean;
|
|
26
29
|
/** Install the production-shaped global JSON parser in front of the router. */
|
|
27
30
|
globalJson?: boolean;
|
|
28
31
|
}): Promise<{ base: string; sync: ReturnType<typeof vi.fn> }> {
|
|
@@ -44,7 +47,8 @@ async function mount(opts: {
|
|
|
44
47
|
kbSync,
|
|
45
48
|
syncSecret: () => opts.secret ?? SECRET,
|
|
46
49
|
authService: {
|
|
47
|
-
|
|
50
|
+
resolveSession: async (token: string) => {
|
|
51
|
+
if (opts.outage) throw new AuthBackendError(new Error('the database is down'));
|
|
48
52
|
if (token !== 'jwt-ok') throw new Error('bad token');
|
|
49
53
|
return { userId: 'u1', email: 'person@example.com' };
|
|
50
54
|
},
|
|
@@ -94,6 +98,27 @@ describe('POST /api/sync — credentials', () => {
|
|
|
94
98
|
expect((await post(person.base, { headers: { authorization: 'Bearer jwt-ok' } })).status).toBe(401);
|
|
95
99
|
});
|
|
96
100
|
|
|
101
|
+
it('500, not 401, when the session’s account cannot be looked up', async () => {
|
|
102
|
+
const { base, sync } = await mount({ admin: true, outage: true });
|
|
103
|
+
const res = await post(base, { headers: { authorization: 'Bearer jwt-ok' } });
|
|
104
|
+
// An outage is not a wrong credential: a 401 would send the admin to sign in again.
|
|
105
|
+
expect(res.status).toBe(500);
|
|
106
|
+
expect(res.headers.get('www-authenticate')).toBeNull();
|
|
107
|
+
expect(sync).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('the shared secret still works while accounts cannot be looked up', async () => {
|
|
111
|
+
const { base, sync } = await mount({ outage: true });
|
|
112
|
+
expect((await post(base, { headers: { authorization: `Bearer ${SECRET}` } })).status).toBe(200);
|
|
113
|
+
expect(sync).toHaveBeenCalledTimes(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a request with no credential is still a 401 during an outage', async () => {
|
|
117
|
+
const { base } = await mount({ outage: true });
|
|
118
|
+
// No bearer at all: nothing was looked up, so nothing failed.
|
|
119
|
+
expect((await post(base)).status).toBe(401);
|
|
120
|
+
});
|
|
121
|
+
|
|
97
122
|
it('accepts the GitLab token header', async () => {
|
|
98
123
|
const { base } = await mount({});
|
|
99
124
|
const res = await post(base, { headers: { 'x-gitlab-token': SECRET } });
|
|
@@ -7,6 +7,7 @@ import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
|
7
7
|
import type { IKbSyncService, SyncResult } from './kb-sync.interface.js';
|
|
8
8
|
import { parseSyncPayload, type SyncPayloadSource } from './sync-payload.js';
|
|
9
9
|
import { verifySyncCredential } from './sync-auth.js';
|
|
10
|
+
import { AuthBackendError } from '../auth/account-admission.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Remote sync — a git host or a pipeline tells Hexis the repository changed.
|
|
@@ -45,7 +46,7 @@ export interface KbSyncRouteDeps {
|
|
|
45
46
|
kbSync: IKbSyncService;
|
|
46
47
|
/** The configured secret, read per request so a setup-screen save applies at once. */
|
|
47
48
|
syncSecret: () => string;
|
|
48
|
-
authService: {
|
|
49
|
+
authService: { resolveSession(token: string): Promise<{ userId: string; email: string }> };
|
|
49
50
|
adminAccess: IAdminAccessService;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -107,6 +108,7 @@ export function createKbSyncRoutes(deps: KbSyncRouteDeps): express.Router {
|
|
|
107
108
|
): Promise<void> {
|
|
108
109
|
res.setHeader(SYNC_RESPONSE_HEADER, SYNC_RESPONSE_MARKER);
|
|
109
110
|
const raw: Buffer | undefined = Buffer.isBuffer(req.body) ? req.body : undefined;
|
|
111
|
+
let lookupFailed: AuthBackendError | null = null as AuthBackendError | null;
|
|
110
112
|
|
|
111
113
|
const auth = await verifySyncCredential(
|
|
112
114
|
{
|
|
@@ -117,10 +119,11 @@ export function createKbSyncRoutes(deps: KbSyncRouteDeps): express.Router {
|
|
|
117
119
|
rawBody: raw,
|
|
118
120
|
},
|
|
119
121
|
{
|
|
120
|
-
verifyJwt: (token) => {
|
|
122
|
+
verifyJwt: async (token) => {
|
|
121
123
|
try {
|
|
122
|
-
return { email: deps.authService.
|
|
123
|
-
} catch {
|
|
124
|
+
return { email: (await deps.authService.resolveSession(token)).email };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err instanceof AuthBackendError) lookupFailed = err;
|
|
124
127
|
return null;
|
|
125
128
|
}
|
|
126
129
|
},
|
|
@@ -128,6 +131,13 @@ export function createKbSyncRoutes(deps: KbSyncRouteDeps): express.Router {
|
|
|
128
131
|
},
|
|
129
132
|
);
|
|
130
133
|
if (!auth.ok) {
|
|
134
|
+
// The session's account could not be looked up: an outage, not a wrong
|
|
135
|
+
// credential, and saying 401 would send an admin to sign in again.
|
|
136
|
+
if (lookupFailed) {
|
|
137
|
+
log.error('session account lookup failed:', { err: lookupFailed.cause });
|
|
138
|
+
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
131
141
|
if (auth.status === 401) res.setHeader('WWW-Authenticate', 'Bearer realm="hexis-sync"');
|
|
132
142
|
res.status(auth.status).json({ error: auth.message });
|
|
133
143
|
return;
|
|
@@ -42,7 +42,7 @@ export interface SyncAuthInput {
|
|
|
42
42
|
|
|
43
43
|
export interface SyncSessionVerifier {
|
|
44
44
|
/** Resolve a browser JWT to its email, or null when it is not one. */
|
|
45
|
-
verifyJwt(token: string): { email: string } | null
|
|
45
|
+
verifyJwt(token: string): { email: string } | null | Promise<{ email: string } | null>;
|
|
46
46
|
isAdmin(email: string): Promise<boolean>;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -98,7 +98,7 @@ export async function verifySyncCredential(
|
|
|
98
98
|
// An admin's own session, carried as the bearer. Checked after the secret
|
|
99
99
|
// so a secret that happens to parse as a JWT is never mistaken for one.
|
|
100
100
|
if (bearer) {
|
|
101
|
-
const claim = session.verifyJwt(bearer);
|
|
101
|
+
const claim = await session.verifyJwt(bearer);
|
|
102
102
|
if (claim && (await session.isAdmin(claim.email))) {
|
|
103
103
|
return { ok: true, credential: { kind: 'admin-session', email: claim.email } };
|
|
104
104
|
}
|
|
@@ -5,6 +5,7 @@ import type { AuthService } from '../../auth/auth.service.js';
|
|
|
5
5
|
import type { IExternalApiKeyService } from '../../tool-auth/external-api-key.interface.js';
|
|
6
6
|
import type { BevelOAuthProvider } from '../oauth/bevel-oauth-provider.js';
|
|
7
7
|
import { InternalTokenService } from '../../tool-auth/internal-token.service.js';
|
|
8
|
+
import { AccountDeactivatedError, AuthBackendError } from '../../auth/account-admission.js';
|
|
8
9
|
|
|
9
10
|
const RESOURCE_METADATA_URL = 'https://bevel.example/.well-known/oauth-protected-resource/api/mcp';
|
|
10
11
|
|
|
@@ -19,10 +20,14 @@ function makeReqRes(authorization?: string) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function makeAuthService(verify?: (t: string) => { userId: string; email: string }) {
|
|
23
|
+
const verifyToken = vi.fn(verify ?? (() => {
|
|
24
|
+
throw new Error('invalid');
|
|
25
|
+
}));
|
|
22
26
|
return {
|
|
23
|
-
verifyToken
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
verifyToken,
|
|
28
|
+
// The session path checks the account is still on; every account here is.
|
|
29
|
+
resolveSession: vi.fn(async (t: string) => verifyToken(t)),
|
|
30
|
+
isActive: vi.fn(async () => true),
|
|
26
31
|
} as unknown as AuthService;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -424,3 +429,55 @@ describe('createMcpAuthMiddleware', () => {
|
|
|
424
429
|
});
|
|
425
430
|
});
|
|
426
431
|
});
|
|
432
|
+
|
|
433
|
+
describe('createMcpAuthMiddleware — a switched-off account', () => {
|
|
434
|
+
const internal = new InternalTokenService({ secret: 'test-secret-32-bytes-long-enough!!' });
|
|
435
|
+
|
|
436
|
+
it('401s a session whose account was switched off, and says why', async () => {
|
|
437
|
+
const auth = makeAuthService();
|
|
438
|
+
(auth.resolveSession as ReturnType<typeof vi.fn>) = vi.fn(async () => {
|
|
439
|
+
throw new AccountDeactivatedError();
|
|
440
|
+
});
|
|
441
|
+
const { req, res, next, status, json } = makeReqRes('Bearer some.jwt.token');
|
|
442
|
+
await makeMw({ auth })(req, res, next);
|
|
443
|
+
expect(next).not.toHaveBeenCalled();
|
|
444
|
+
expect(status).toHaveBeenCalledWith(401);
|
|
445
|
+
expect(json.mock.calls[0][0].error).toContain('switched off');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('500s — not 401s — when the session account cannot be looked up', async () => {
|
|
449
|
+
const auth = makeAuthService();
|
|
450
|
+
(auth.resolveSession as ReturnType<typeof vi.fn>) = vi.fn(async () => {
|
|
451
|
+
throw new AuthBackendError(new Error('db down'));
|
|
452
|
+
});
|
|
453
|
+
const { req, res, next, status } = makeReqRes('Bearer some.jwt.token');
|
|
454
|
+
await makeMw({ auth })(req, res, next);
|
|
455
|
+
expect(next).not.toHaveBeenCalled();
|
|
456
|
+
expect(status).toHaveBeenCalledWith(500);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('401s an internal token whose account was switched off, and says why', async () => {
|
|
460
|
+
const token = internal.mint({ userId: 'user-7', externalProxy: true }, 60_000);
|
|
461
|
+
const auth = makeAuthService();
|
|
462
|
+
(auth.getUserById as ReturnType<typeof vi.fn>) = vi.fn(async () => ({ id: 'user-7', email: 'seven@example.com' }));
|
|
463
|
+
(auth.isActive as ReturnType<typeof vi.fn>) = vi.fn(async () => false);
|
|
464
|
+
const { req, res, next, status, json } = makeReqRes(`Bearer ${token}`);
|
|
465
|
+
await makeMw({ internal, auth })(req, res, next);
|
|
466
|
+
expect(next).not.toHaveBeenCalled();
|
|
467
|
+
expect(status).toHaveBeenCalledWith(401);
|
|
468
|
+
expect(json.mock.calls[0][0].error).toContain('switched off');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('500s — not 401s — when an internal token\'s account cannot be looked up', async () => {
|
|
472
|
+
const token = internal.mint({ userId: 'user-7', externalProxy: true }, 60_000);
|
|
473
|
+
const auth = makeAuthService();
|
|
474
|
+
(auth.getUserById as ReturnType<typeof vi.fn>) = vi.fn(async () => ({ id: 'user-7', email: 'seven@example.com' }));
|
|
475
|
+
(auth.isActive as ReturnType<typeof vi.fn>) = vi.fn(async () => {
|
|
476
|
+
throw new Error('db down');
|
|
477
|
+
});
|
|
478
|
+
const { req, res, next, status } = makeReqRes(`Bearer ${token}`);
|
|
479
|
+
await makeMw({ internal, auth })(req, res, next);
|
|
480
|
+
expect(next).not.toHaveBeenCalled();
|
|
481
|
+
expect(status).toHaveBeenCalledWith(500);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
@@ -16,7 +16,8 @@ import { SpillStore } from '../../workspace/spill-store.js';
|
|
|
16
16
|
import { createManualRoutes } from '../../tool-registry/manual.routes.js';
|
|
17
17
|
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
18
18
|
import { toolDef } from '../../tool-helpers/tool-def.js';
|
|
19
|
-
import {
|
|
19
|
+
import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
|
|
20
|
+
import { platformInstructions } from '../../agent-instructions/index.js';
|
|
20
21
|
import { startFakeDownstreamMcpServer, type FakeDownstreamMcpServer } from './fake-downstream-mcp-server.js';
|
|
21
22
|
import { registerBevelSecretsVariableLoader } from '../../secrets-vault/secrets-variable-loader.js';
|
|
22
23
|
import type { ForcedRefreshOutcome, ISecretsVaultService } from '../../secrets-vault/secrets-vault.contract.js';
|
|
@@ -483,13 +484,13 @@ describe('per-request identity: catalog, metering and continuity', () => {
|
|
|
483
484
|
});
|
|
484
485
|
|
|
485
486
|
describe('agent instructions over the real transport', () => {
|
|
486
|
-
it('the initialize result carries the
|
|
487
|
+
it('the initialize result carries the platform text and the preamble body inline', async () => {
|
|
487
488
|
const { baseUrl } = await startPlatform({
|
|
488
489
|
readAgentPreamble: async () => 'Acme builds solar farms.\n\n<!-- private -->Look in Projects/ first.',
|
|
489
490
|
});
|
|
490
491
|
const { client } = await connectSdkClient(baseUrl);
|
|
491
492
|
const instructions = client.getInstructions();
|
|
492
|
-
expect(instructions).toBe(`${
|
|
493
|
+
expect(instructions).toBe(`${platformInstructions(DEFAULT_KB_LAYOUT)}\n\nAcme builds solar farms.\n\nLook in Projects/ first.`);
|
|
493
494
|
expect(instructions).not.toContain('private');
|
|
494
495
|
});
|
|
495
496
|
|
|
@@ -11,9 +11,13 @@ import { SpillStore } from '../../workspace/spill-store.js';
|
|
|
11
11
|
import { createManualRoutes } from '../../tool-registry/manual.routes.js';
|
|
12
12
|
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
13
13
|
import { toolDef } from '../../tool-helpers/tool-def.js';
|
|
14
|
-
import {
|
|
14
|
+
import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
|
|
15
|
+
import { TOOL_PREFIX_LINE, platformInstructions, sharedRulesPointer } from '../../agent-instructions/index.js';
|
|
15
16
|
import type { AgentEventInput, IAgentEventRecorder } from '../../audit/audit.contract.js';
|
|
16
17
|
|
|
18
|
+
/** The platform-owned part of the handshake text: the header plus the shared file rules. */
|
|
19
|
+
const PLATFORM = platformInstructions(DEFAULT_KB_LAYOUT);
|
|
20
|
+
|
|
17
21
|
/**
|
|
18
22
|
* End-to-end proxy test: a real express app serving the registry-driven tool
|
|
19
23
|
* surface over loopback, a real per-request `UtcpClient` discovering it, and a
|
|
@@ -24,6 +28,8 @@ import type { AgentEventInput, IAgentEventRecorder } from '../../audit/audit.con
|
|
|
24
28
|
|
|
25
29
|
let httpServer: HttpServer | undefined;
|
|
26
30
|
const cleanups: Array<() => Promise<void>> = [];
|
|
31
|
+
/** Every loopback call the skill routes saw this test, in order — with its body. */
|
|
32
|
+
let skillRequests: { tool: string; body: Record<string, unknown> }[] = [];
|
|
27
33
|
|
|
28
34
|
// Module-hosted tool endpoints + their registered defs: a stand-in echo `ask`
|
|
29
35
|
// (mirrors the real `{text, sessionId}` contract) and an erroring `boom`.
|
|
@@ -49,6 +55,7 @@ async function setup(deps?: {
|
|
|
49
55
|
/** The agent connection an OAuth caller arrived through (with `tokenId: null`). */
|
|
50
56
|
connectionId?: string | null;
|
|
51
57
|
}) {
|
|
58
|
+
skillRequests = [];
|
|
52
59
|
const registry = new ToolRegistry();
|
|
53
60
|
registry.registerExternalTool(
|
|
54
61
|
toolDef({
|
|
@@ -99,9 +106,47 @@ async function setup(deps?: {
|
|
|
99
106
|
// The two the Audit log's skill classification reads through: a catalog of
|
|
100
107
|
// one skill, and a file read that answers for any path (registered as
|
|
101
108
|
// tools only by the tests that need them, via `extraTools`).
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
109
|
+
//
|
|
110
|
+
// Both skill routes answer like the real ones: a `branch` in the body reads
|
|
111
|
+
// that draft (an extra, unmerged skill), no `branch` reads the released
|
|
112
|
+
// catalog. Every call is recorded, so a test can assert which of the two the
|
|
113
|
+
// prompt surface asked for.
|
|
114
|
+
const RFI_BODY = '# RFI\n\nAnswer the RFI.';
|
|
115
|
+
app.post('/api/agent/tools/list_skills', (req, res) => {
|
|
116
|
+
const b = (req.body ?? {}) as Record<string, unknown>;
|
|
117
|
+
skillRequests.push({ tool: 'list_skills', body: b });
|
|
118
|
+
const released = [{ name: 'rfi', description: 'RFI answers', path: 'Plugins/Sales/rfi' }];
|
|
119
|
+
res.json({
|
|
120
|
+
skills:
|
|
121
|
+
typeof b.branch === 'string'
|
|
122
|
+
? [
|
|
123
|
+
...released,
|
|
124
|
+
{ name: 'make-deck', description: 'Decks.', path: 'Plugins/make-deck', unmerged: true, branch: b.branch },
|
|
125
|
+
]
|
|
126
|
+
: released,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
app.post('/api/agent/tools/get_skill', (req, res) => {
|
|
130
|
+
const b = (req.body ?? {}) as Record<string, unknown>;
|
|
131
|
+
skillRequests.push({ tool: 'get_skill', body: b });
|
|
132
|
+
if (b.name !== 'rfi' && typeof b.branch !== 'string') {
|
|
133
|
+
res.json({ ok: false, error: 'not_found' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const unmergedLine =
|
|
137
|
+
typeof b.branch === 'string' ? `This skill is read from the unmerged branch "${b.branch}" and is not approved.\n\n` : '';
|
|
138
|
+
res.json({
|
|
139
|
+
ok: true,
|
|
140
|
+
kind: 'skill',
|
|
141
|
+
skill: {
|
|
142
|
+
name: b.name,
|
|
143
|
+
description: 'RFI answers',
|
|
144
|
+
path: 'Plugins/Sales/rfi',
|
|
145
|
+
body: `${unmergedLine}${RFI_BODY}`,
|
|
146
|
+
files: [],
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
});
|
|
105
150
|
app.post('/api/agent/tools/read_file', (req, res) => {
|
|
106
151
|
const b = (req.body ?? {}) as { path?: string };
|
|
107
152
|
res.json({ content: `contents of ${b.path}` });
|
|
@@ -221,6 +266,25 @@ describe('McpService (UTCP→MCP proxy)', () => {
|
|
|
221
266
|
expect(askSchema.properties.body?.properties?.prompt).toBeDefined();
|
|
222
267
|
});
|
|
223
268
|
|
|
269
|
+
it('ends the served call_tool_chain description with the shared-rules pointer', async () => {
|
|
270
|
+
// What a chained read does to an IMAGE is one of the rules the file tools
|
|
271
|
+
// share, so it is stated once — in the handshake instructions and in the
|
|
272
|
+
// managed guide — and the chain, like every file tool, ends with the one
|
|
273
|
+
// sentence saying where. The clients that drop `instructions` have only
|
|
274
|
+
// descriptions to go on, so that sentence is their way to the rule.
|
|
275
|
+
const client = await setup();
|
|
276
|
+
const { tools } = await client.listTools();
|
|
277
|
+
const chain = tools.find((t) => t.name === 'call_tool_chain')!;
|
|
278
|
+
const pointer = sharedRulesPointer(DEFAULT_KB_LAYOUT);
|
|
279
|
+
expect(chain.description!.endsWith(pointer)).toBe(true);
|
|
280
|
+
// Once, and not on the two meta-tools that describe the registry rather
|
|
281
|
+
// than a file.
|
|
282
|
+
expect(chain.description!.split(pointer)).toHaveLength(2);
|
|
283
|
+
for (const name of ['list_tools', 'tools_info']) {
|
|
284
|
+
expect(tools.find((t) => t.name === name)!.description, name).not.toContain(pointer);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
224
288
|
it('a $defs/$ref tool schema survives tools/list and a real MCP client accepts it', async () => {
|
|
225
289
|
// No tokenId filter for this assertion — an OAuth-style session lists all.
|
|
226
290
|
const client = await setup({ tokenId: null });
|
|
@@ -493,6 +557,23 @@ describe('McpService — per-user credential pre-check', () => {
|
|
|
493
557
|
expect(await toolNames(client)).toEqual(['ask', 'boom', 'call_tool_chain', 'list_tools', 'refy', 'tools_info']);
|
|
494
558
|
});
|
|
495
559
|
|
|
560
|
+
/**
|
|
561
|
+
* The worked example in `call_tool_chain`'s description has to be derived
|
|
562
|
+
* from the tools THIS caller is actually served. Derived from the unfiltered
|
|
563
|
+
* catalog it could name a credential-gated tool the filter just removed —
|
|
564
|
+
* an example a connection-key caller copies and cannot call at all.
|
|
565
|
+
*/
|
|
566
|
+
it('never writes the worked example against a tool it just hid', async () => {
|
|
567
|
+
const hidden = await setup({ secretsVault: vault(false), toolManuals: manualsWithUserVar });
|
|
568
|
+
const chain = (await hidden.listTools()).tools.find((t) => t.name === 'call_tool_chain')!;
|
|
569
|
+
expect(chain.description).not.toMatch(/return KNOWLEDGE_BASE\.\w+\(/);
|
|
570
|
+
// The same catalog, with the credential set, does print one — so the
|
|
571
|
+
// assertion above is the filter at work, not an example that never exists.
|
|
572
|
+
const served = await setup({ secretsVault: vault(true), toolManuals: manualsWithUserVar });
|
|
573
|
+
const shown = (await served.listTools()).tools.find((t) => t.name === 'call_tool_chain')!;
|
|
574
|
+
expect(shown.description).toMatch(/return KNOWLEDGE_BASE\.\w+\(/);
|
|
575
|
+
});
|
|
576
|
+
|
|
496
577
|
it('keeps the full listing for an OAuth/JWT session (tokenId null) — the caller configures interactively', async () => {
|
|
497
578
|
const client = await setup({
|
|
498
579
|
secretsVault: vault(false),
|
|
@@ -507,17 +588,17 @@ describe('McpService — per-user credential pre-check', () => {
|
|
|
507
588
|
describe('McpService — agent instructions', () => {
|
|
508
589
|
const KB_TOOLS = ['start_session', 'grep', 'list_files', 'read_file'];
|
|
509
590
|
|
|
510
|
-
it(
|
|
591
|
+
it("sends the platform text and the preamble as the session's instructions", async () => {
|
|
511
592
|
const client = await setup({ readAgentPreamble: async () => 'Acme builds solar farms.\n\nProjects live in Projects/.' });
|
|
512
|
-
expect(client.getInstructions()).toBe(`${
|
|
593
|
+
expect(client.getInstructions()).toBe(`${PLATFORM}\n\nAcme builds solar farms.\n\nProjects live in Projects/.`);
|
|
513
594
|
});
|
|
514
595
|
|
|
515
|
-
it('sends the
|
|
596
|
+
it('sends the platform text alone when no reader is wired', async () => {
|
|
516
597
|
const client = await setup();
|
|
517
|
-
expect(client.getInstructions()).toBe(
|
|
598
|
+
expect(client.getInstructions()).toBe(PLATFORM);
|
|
518
599
|
});
|
|
519
600
|
|
|
520
|
-
it('a throwing reader still yields a session, with the
|
|
601
|
+
it('a throwing reader still yields a session, with the platform text as its instructions and a warning', async () => {
|
|
521
602
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
522
603
|
const client = await setup({
|
|
523
604
|
readAgentPreamble: async () => {
|
|
@@ -525,7 +606,7 @@ describe('McpService — agent instructions', () => {
|
|
|
525
606
|
},
|
|
526
607
|
extraTools: KB_TOOLS,
|
|
527
608
|
});
|
|
528
|
-
expect(client.getInstructions()).toBe(
|
|
609
|
+
expect(client.getInstructions()).toBe(PLATFORM);
|
|
529
610
|
// The error itself rides along, so a terminal shows its stack.
|
|
530
611
|
expect(warn).toHaveBeenCalledWith(
|
|
531
612
|
expect.stringContaining('mcp-description.md'),
|
|
@@ -773,10 +854,31 @@ describe('McpService — the Audit log records what an agent calls', () => {
|
|
|
773
854
|
it('records a prompt read as a skill, with an error outcome when the skill is unknown', async () => {
|
|
774
855
|
const recorder = spyRecorder();
|
|
775
856
|
const client = await setup({ auditRecorder: recorder });
|
|
776
|
-
//
|
|
777
|
-
await expect(client.getPrompt({ name: '
|
|
857
|
+
// A name the harness's catalog does not have: get_skill answers not_found.
|
|
858
|
+
await expect(client.getPrompt({ name: 'nope' })).rejects.toThrow(/Unknown skill/);
|
|
778
859
|
await settle();
|
|
779
|
-
expect(recorder.events[0]).toMatchObject({ kind: 'skill', name: '
|
|
860
|
+
expect(recorder.events[0]).toMatchObject({ kind: 'skill', name: 'nope', outcome: 'error' });
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Prompts ARE skills, and a skill offered as a prompt is one the
|
|
865
|
+
* organisation released: the two prompt handlers name no branch, so a draft's
|
|
866
|
+
* skills never reach a client's slash-command menu.
|
|
867
|
+
*/
|
|
868
|
+
it('serves prompts from the default branch only, never a draft', async () => {
|
|
869
|
+
const client = await setup();
|
|
870
|
+
const { prompts } = await client.listPrompts();
|
|
871
|
+
expect(prompts.map((p) => p.name)).toEqual(['rfi']);
|
|
872
|
+
|
|
873
|
+
const got = await client.getPrompt({ name: 'rfi' });
|
|
874
|
+
const text = got.messages[0].content.text as string;
|
|
875
|
+
expect(text).toContain('Answer the RFI.');
|
|
876
|
+
expect(text).not.toContain('unmerged');
|
|
877
|
+
|
|
878
|
+
// The loopback bodies prove it: had either handler passed a branch, the
|
|
879
|
+
// harness would have answered with the draft's skill and its notice.
|
|
880
|
+
expect(skillRequests.map((r) => r.tool)).toEqual(['list_skills', 'get_skill']);
|
|
881
|
+
expect(skillRequests.every((r) => r.body.branch === undefined)).toBe(true);
|
|
780
882
|
});
|
|
781
883
|
|
|
782
884
|
it('records a call refused for a missing sign-in as denied, without running the tool', async () => {
|
|
@@ -4,6 +4,7 @@ import { logger } from '../../shared/logging.js';
|
|
|
4
4
|
const log = logger('mcp-auth');
|
|
5
5
|
import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
6
6
|
import type { AuthService } from '../auth/auth.service.js';
|
|
7
|
+
import { ACCOUNT_DEACTIVATED_MESSAGE, AccountDeactivatedError, AuthBackendError } from '../auth/account-admission.js';
|
|
7
8
|
import type { IExternalApiKeyService } from '../tool-auth/external-api-key.interface.js';
|
|
8
9
|
import type { InternalTokenService } from '../tool-auth/internal-token.service.js';
|
|
9
10
|
import { rejectConnectionKey } from '../tool-auth/connection-key-rejection.js';
|
|
@@ -170,8 +171,10 @@ export function createMcpAuthMiddleware(
|
|
|
170
171
|
return;
|
|
171
172
|
}
|
|
172
173
|
let user;
|
|
174
|
+
let active = false;
|
|
173
175
|
try {
|
|
174
176
|
user = await authService.getUserById(claim.userId);
|
|
177
|
+
if (user) active = await authService.isActive(user.id);
|
|
175
178
|
} catch (err) {
|
|
176
179
|
log.error('internal-token user lookup failed:', { err });
|
|
177
180
|
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
@@ -181,6 +184,10 @@ export function createMcpAuthMiddleware(
|
|
|
181
184
|
unauthorized(res, 'Invalid or expired internal token');
|
|
182
185
|
return;
|
|
183
186
|
}
|
|
187
|
+
if (!active) {
|
|
188
|
+
unauthorized(res, ACCOUNT_DEACTIVATED_MESSAGE);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
184
191
|
req.userId = user.id;
|
|
185
192
|
req.userEmail = user.email;
|
|
186
193
|
// The local server's exchanged grant names its agent connection (see
|
|
@@ -214,13 +221,20 @@ export function createMcpAuthMiddleware(
|
|
|
214
221
|
// JWT path — same logic as `createAuthMiddleware` in modules/auth, kept
|
|
215
222
|
// duplicated rather than imported because that one writes its own 401
|
|
216
223
|
// body shape and we want the WWW-Authenticate header set.
|
|
224
|
+
let session: { userId: string; email: string };
|
|
217
225
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
session = await authService.resolveSession(token);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (err instanceof AuthBackendError) {
|
|
229
|
+
log.error('session account lookup failed:', { err: err.cause });
|
|
230
|
+
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
unauthorized(res, err instanceof AccountDeactivatedError ? err.message : 'Invalid or expired token');
|
|
234
|
+
return;
|
|
224
235
|
}
|
|
236
|
+
req.userId = session.userId;
|
|
237
|
+
req.userEmail = session.email;
|
|
238
|
+
next();
|
|
225
239
|
};
|
|
226
240
|
}
|