@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
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { afterAll, afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
import { eq, inArray, sql } from 'drizzle-orm';
|
|
5
|
+
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
6
|
+
import { closeDb, createDb, type Database } from '../connection.js';
|
|
7
|
+
import { runCoreMigrations } from '../migrate.js';
|
|
8
|
+
import { prFileApprovals, users } from '../core-schema.js';
|
|
9
|
+
import { coreMigrationsDir } from '../../../assets.js';
|
|
10
|
+
import { derivePiiKeys, isEncryptedBlob } from '../../../shared/column-crypto.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Personal data in a real Postgres: what a database handle stores under its
|
|
14
|
+
* key, and the backfill that seals what an older version left in clear. The
|
|
15
|
+
* backfill rewrites every personal-data row of a deployment once, with no
|
|
16
|
+
* second chance, and deletes rows on the way (the duplicate collapse) — so
|
|
17
|
+
* what it does is pinned here through the one entry point a start uses,
|
|
18
|
+
* `runCoreMigrations`, and read back over a plain connection that holds no
|
|
19
|
+
* key, which sees what a dump would.
|
|
20
|
+
*
|
|
21
|
+
* Runs where `TEST_DATABASE_URL` names a server this role may create
|
|
22
|
+
* databases on (CI carries one as a service container); skipped elsewhere.
|
|
23
|
+
* Every test works in a scratch database of its own and drops it: the
|
|
24
|
+
* database the URL names is only connected to, never written.
|
|
25
|
+
*/
|
|
26
|
+
const ADMIN_URL = process.env.TEST_DATABASE_URL;
|
|
27
|
+
const TIMEOUT = 120_000;
|
|
28
|
+
const KEY = randomBytes(32).toString('base64');
|
|
29
|
+
const keys = derivePiiKeys(KEY);
|
|
30
|
+
|
|
31
|
+
const TABLES = [
|
|
32
|
+
'users',
|
|
33
|
+
'pr_file_approvals',
|
|
34
|
+
'pr_merge_log',
|
|
35
|
+
'pr_comments',
|
|
36
|
+
'change_requests',
|
|
37
|
+
'pending_commits',
|
|
38
|
+
'file_locks',
|
|
39
|
+
'plugin_join_requests',
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
type Row = Record<string, unknown>;
|
|
43
|
+
type Query = (text: string) => Promise<Row[]>;
|
|
44
|
+
|
|
45
|
+
const created: string[] = [];
|
|
46
|
+
const open: Database[] = [];
|
|
47
|
+
const plain: pg.Pool[] = [];
|
|
48
|
+
|
|
49
|
+
async function withAdmin<T>(fn: (admin: pg.Client) => Promise<T>): Promise<T> {
|
|
50
|
+
const admin = new pg.Client({ connectionString: ADMIN_URL });
|
|
51
|
+
await admin.connect();
|
|
52
|
+
try {
|
|
53
|
+
return await fn(admin);
|
|
54
|
+
} finally {
|
|
55
|
+
await admin.end();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A scratch database of this test's own; dropped in `afterAll`. */
|
|
60
|
+
async function scratchDatabase(): Promise<string> {
|
|
61
|
+
const name = `hexis_pii_${randomBytes(6).toString('hex')}`;
|
|
62
|
+
await withAdmin((admin) => admin.query(`create database ${name}`));
|
|
63
|
+
created.push(name);
|
|
64
|
+
const url = new URL(ADMIN_URL!);
|
|
65
|
+
url.pathname = `/${name}`;
|
|
66
|
+
return url.toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The application's handle on a schema, holding `key`. */
|
|
70
|
+
function handle(url: string, opts: { schema?: string; key?: string | null } = {}): Database {
|
|
71
|
+
const piiKey = opts.key === null ? undefined : (opts.key ?? KEY);
|
|
72
|
+
const db = createDb(url, { ...(opts.schema ? { schema: opts.schema } : {}), ...(piiKey ? { piiKey } : {}) });
|
|
73
|
+
open.push(db);
|
|
74
|
+
return db;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** What is STORED, read over a connection that holds no key — as a dump would show it. */
|
|
78
|
+
function stored(url: string, schema?: string): Query {
|
|
79
|
+
const pool = new pg.Pool({ connectionString: url, ...(schema ? { options: `-c search_path=${schema}` } : {}) });
|
|
80
|
+
plain.push(pool);
|
|
81
|
+
return async (text) => (await pool.query(text)).rows as Row[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A schema as an upgrade finds it: the SQL history applied — migration
|
|
86
|
+
* 0016's nullable blind-index columns included — and nothing sealed yet. That
|
|
87
|
+
* is the state between the history and the backfill, and the one rows of an
|
|
88
|
+
* older version can be planted in.
|
|
89
|
+
*/
|
|
90
|
+
async function beforeTheBackfill(
|
|
91
|
+
url: string,
|
|
92
|
+
opts: { schema?: string; key?: string } = {},
|
|
93
|
+
): Promise<{ db: Database; q: Query }> {
|
|
94
|
+
const db = handle(url, opts);
|
|
95
|
+
if (opts.schema) await db.execute(sql.raw(`create schema if not exists "${opts.schema}"`));
|
|
96
|
+
await migrate(db, {
|
|
97
|
+
migrationsFolder: coreMigrationsDir(),
|
|
98
|
+
migrationsTable: '__drizzle_migrations_core',
|
|
99
|
+
migrationsSchema: opts.schema,
|
|
100
|
+
});
|
|
101
|
+
return { db, q: stored(url, opts.schema) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Rows as an older version wrote them: plaintext, no blind index. */
|
|
105
|
+
async function plantLegacyRows(q: Query): Promise<void> {
|
|
106
|
+
await q(`insert into users (email, name, avatar_url) values
|
|
107
|
+
('Ada@Example.com', 'Ada', null),
|
|
108
|
+
('bo@example.com', 'Bo', 'https://img.example.com/bo.png')`);
|
|
109
|
+
await q(`insert into change_requests
|
|
110
|
+
(source_branch, target_branch, title, author_email, author_name, apply_failure_reason, apply_failed_by_name, apply_failed_at)
|
|
111
|
+
values ('d', 'main', 'A title', 'ada@example.com', 'Ada', 'bo@example.com is not an approver', 'Bo', now())`);
|
|
112
|
+
await q(`insert into pr_merge_log (pr_number, triggered_by_email, triggered_by_name, head_sha_at_merge, merge_method, succeeded, error)
|
|
113
|
+
values (1, 'ada@example.com', 'Ada', 'abc', 'merge', true, null),
|
|
114
|
+
(1, 'bo@example.com', 'Bo', 'abc', 'merge', false, 'Bo has no push right')`);
|
|
115
|
+
await q(`insert into pr_comments (pr_number, author_email, author_name, head_sha, body)
|
|
116
|
+
values (1, 'bo@example.com', 'Bo', 'abc', 'Looks right to me')`);
|
|
117
|
+
await q(`insert into pending_commits (workspace_id, branch, path, author_email, author_name, last_error)
|
|
118
|
+
values ('w', 'main', 'a.md', 'ada@example.com', 'Ada', 'rejected for Ada')`);
|
|
119
|
+
await q(`insert into file_locks (workspace_id, branch, path, holder_user_id, holder_name, expires_at)
|
|
120
|
+
select 'w', 'main', 'a.md', id, 'Ada', now() + interval '1 hour' from users where email = 'Ada@Example.com'`);
|
|
121
|
+
// One person's approval and join request, twice, under two spellings of
|
|
122
|
+
// the address: distinct rows to the old unique index, one row to the new.
|
|
123
|
+
await q(`insert into pr_file_approvals (pr_number, path, approver_email, approver_name, head_sha, approved_at)
|
|
124
|
+
values (1, 'a.md', 'ada@example.com', 'Ada', 'abc', now() - interval '2 hours'),
|
|
125
|
+
(1, 'a.md', 'ADA@example.com', 'Ada later', 'abc', now() - interval '1 hour')`);
|
|
126
|
+
await q(`insert into plugin_join_requests (requester_email, requester_name, plugin_key, failure_reason, created_at)
|
|
127
|
+
values ('ada@example.com', 'Ada', 'gtm', 'git refused Ada', now() - interval '2 hours'),
|
|
128
|
+
('Ada@example.com', 'Ada later', 'gtm', null, now() - interval '1 hour')`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Every row of every table the backfill touches, as stored. */
|
|
132
|
+
async function everything(q: Query): Promise<string> {
|
|
133
|
+
const out: Record<string, unknown> = {};
|
|
134
|
+
for (const table of TABLES) {
|
|
135
|
+
out[table] = (await q(`select to_jsonb(t) as row from "${table}" t order by to_jsonb(t)::text`)).map((r) => r.row);
|
|
136
|
+
}
|
|
137
|
+
return JSON.stringify(out);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** {@link everything} with the blobs masked: random base64 spells a planted name by chance often enough. */
|
|
141
|
+
const everythingReadable = async (q: Query) => (await everything(q)).replace(/"pii:v1:[^"]*"/g, '"sealed"');
|
|
142
|
+
|
|
143
|
+
const opened = (value: unknown) => keys.read(String(value));
|
|
144
|
+
const sealed = (value: unknown) => typeof value === 'string' && isEncryptedBlob(value);
|
|
145
|
+
|
|
146
|
+
describe.skipIf(!ADMIN_URL)('personal data, on a real Postgres', () => {
|
|
147
|
+
afterEach(async () => {
|
|
148
|
+
await Promise.all(open.splice(0).map((db) => closeDb(db)));
|
|
149
|
+
await Promise.all(plain.splice(0).map((pool) => pool.end()));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
afterAll(async () => {
|
|
153
|
+
await withAdmin(async (admin) => {
|
|
154
|
+
for (const name of created) await admin.query(`drop database if exists ${name} with (force)`);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('what a handle stores', () => {
|
|
159
|
+
it(
|
|
160
|
+
'seals and indexes what the application writes, and reads it back, a transaction included',
|
|
161
|
+
async () => {
|
|
162
|
+
const url = await scratchDatabase();
|
|
163
|
+
const db = handle(url);
|
|
164
|
+
const q = stored(url);
|
|
165
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
166
|
+
|
|
167
|
+
await db.insert(users).values({ email: 'Ada@Example.com', emailBidx: 'Ada@Example.com', name: 'Ada' });
|
|
168
|
+
await db.transaction(async (tx) => {
|
|
169
|
+
await tx.insert(users).values({ email: 'bo@example.com', emailBidx: 'bo@example.com', name: 'Bo', avatarUrl: '' });
|
|
170
|
+
// Inside the transaction the handle's key is the one at work too.
|
|
171
|
+
const [bo] = await tx.select().from(users).where(eq(users.emailBidx, ' BO@example.com'));
|
|
172
|
+
expect(bo).toMatchObject({ email: 'bo@example.com', name: 'Bo', avatarUrl: '' });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const rows = await q(`select email, email_bidx, name, avatar_url from users order by created_at`);
|
|
176
|
+
expect(rows.every((r) => sealed(r.email) && sealed(r.name))).toBe(true);
|
|
177
|
+
expect(rows.map((r) => r.email_bidx)).toEqual([keys.index('ada@example.com'), keys.index('bo@example.com')]);
|
|
178
|
+
// NULL stays NULL and the empty string stays itself.
|
|
179
|
+
expect(rows.map((r) => r.avatar_url)).toEqual([null, '']);
|
|
180
|
+
|
|
181
|
+
const found = await db.select().from(users).where(inArray(users.emailBidx, ['ADA@example.com', 'nobody@x.co']));
|
|
182
|
+
expect(found).toHaveLength(1);
|
|
183
|
+
expect(found[0]).toMatchObject({ email: 'Ada@Example.com', name: 'Ada', avatarUrl: null });
|
|
184
|
+
|
|
185
|
+
await db.update(users).set({ name: 'Ada L.' }).where(eq(users.emailBidx, 'ada@example.com'));
|
|
186
|
+
const [renamed] = await q(`select name from users where email_bidx = '${keys.index('ada@example.com')}'`);
|
|
187
|
+
expect(sealed(renamed!.name)).toBe(true);
|
|
188
|
+
expect(opened(renamed!.name)).toBe('Ada L.');
|
|
189
|
+
|
|
190
|
+
// A copied row carries the address its index is made from; the stored
|
|
191
|
+
// index read back beside it is refused as a value to write.
|
|
192
|
+
await db.insert(prFileApprovals).values({
|
|
193
|
+
prNumber: 1, path: 'a.md', approverEmail: 'ada@example.com', approverEmailBidx: 'ada@example.com', approverName: 'Ada', headSha: 'abc',
|
|
194
|
+
});
|
|
195
|
+
const [approval] = await db.select().from(prFileApprovals);
|
|
196
|
+
expect(approval!.approverEmailBidx).toBe(keys.index('ada@example.com'));
|
|
197
|
+
await expect(async () => {
|
|
198
|
+
await db.insert(prFileApprovals).values({ ...approval!, id: undefined, headSha: 'def' });
|
|
199
|
+
}).rejects.toThrow(/stored index/);
|
|
200
|
+
|
|
201
|
+
// Raw SQL through the handle reads plaintext as well; the stored form
|
|
202
|
+
// is there for whoever asks for the bytes.
|
|
203
|
+
const [viaSql] = (
|
|
204
|
+
await db.execute(sql`select email, convert_to(email, 'UTF8') as kept from users where email_bidx = ${keys.index('bo@example.com')}`)
|
|
205
|
+
).rows as Array<{ email: string; kept: Buffer }>;
|
|
206
|
+
expect(viaSql!.email).toBe('bo@example.com');
|
|
207
|
+
expect(sealed(viaSql!.kept.toString('utf8'))).toBe(true);
|
|
208
|
+
},
|
|
209
|
+
TIMEOUT,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
it(
|
|
213
|
+
'a handle that holds no key writes no personal data, and is refused a start',
|
|
214
|
+
async () => {
|
|
215
|
+
const url = await scratchDatabase();
|
|
216
|
+
await runCoreMigrations(handle(url), coreMigrationsDir());
|
|
217
|
+
const keyless = handle(url, { key: null });
|
|
218
|
+
const q = stored(url);
|
|
219
|
+
|
|
220
|
+
// drizzle reports the statement that failed; the reason is its cause.
|
|
221
|
+
const refused = { cause: { message: expect.stringMatching(/holds no key/) } };
|
|
222
|
+
await expect(
|
|
223
|
+
keyless.insert(users).values({ email: 'ada@example.com', emailBidx: 'ada@example.com', name: 'Ada' }),
|
|
224
|
+
).rejects.toMatchObject(refused);
|
|
225
|
+
await expect(
|
|
226
|
+
keyless.select().from(users).where(eq(users.emailBidx, 'ada@example.com')),
|
|
227
|
+
).rejects.toMatchObject(refused);
|
|
228
|
+
expect(await q(`select 1 from users`)).toHaveLength(0);
|
|
229
|
+
await expect(runCoreMigrations(keyless, coreMigrationsDir())).rejects.toThrow(/holds no personal-data key/);
|
|
230
|
+
// "No key" is the key left out. A blank or malformed one was meant to
|
|
231
|
+
// be a key, and no handle is built with it.
|
|
232
|
+
expect(() => createDb(url, { piiKey: '' })).toThrow(/SECRETS_ENC_KEY/);
|
|
233
|
+
expect(() => createDb(url, { piiKey: 'not-a-key' })).toThrow(/SECRETS_ENC_KEY/);
|
|
234
|
+
},
|
|
235
|
+
TIMEOUT,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
it(
|
|
239
|
+
'two knowledge bases in one database are sealed under their own keys',
|
|
240
|
+
async () => {
|
|
241
|
+
const url = await scratchDatabase();
|
|
242
|
+
const acmeKey = randomBytes(32).toString('base64');
|
|
243
|
+
const zetaKey = randomBytes(32).toString('base64');
|
|
244
|
+
const acme = handle(url, { schema: 't_acme', key: acmeKey });
|
|
245
|
+
const zeta = handle(url, { schema: 't_zeta', key: zetaKey });
|
|
246
|
+
for (const [db, schema] of [[acme, 't_acme'], [zeta, 't_zeta']] as const) {
|
|
247
|
+
await db.execute(sql.raw(`create schema if not exists "${schema}"`));
|
|
248
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
249
|
+
await db.insert(users).values({ email: 'ada@example.com', emailBidx: 'ada@example.com', name: `Ada of ${schema}` });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The same address, a different index in each: knowing one tenant's
|
|
253
|
+
// says nothing about the other's.
|
|
254
|
+
const q = stored(url);
|
|
255
|
+
const [a] = await q(`select email, email_bidx from t_acme.users`);
|
|
256
|
+
const [z] = await q(`select email, email_bidx from t_zeta.users`);
|
|
257
|
+
expect(a!.email_bidx).toBe(derivePiiKeys(acmeKey).index('ada@example.com'));
|
|
258
|
+
expect(z!.email_bidx).toBe(derivePiiKeys(zetaKey).index('ada@example.com'));
|
|
259
|
+
expect(a!.email_bidx).not.toBe(z!.email_bidx);
|
|
260
|
+
|
|
261
|
+
// Each reads its own, in the same process, at the same time.
|
|
262
|
+
const [[mine], [theirs]] = await Promise.all([
|
|
263
|
+
acme.select().from(users).where(eq(users.emailBidx, 'ada@example.com')),
|
|
264
|
+
zeta.select().from(users).where(eq(users.emailBidx, 'ada@example.com')),
|
|
265
|
+
]);
|
|
266
|
+
expect(mine!.name).toBe('Ada of t_acme');
|
|
267
|
+
expect(theirs!.name).toBe('Ada of t_zeta');
|
|
268
|
+
|
|
269
|
+
// And neither opens the other's, even handed the row.
|
|
270
|
+
const [crossed] = (await acme.execute(sql`select name from t_zeta.users`)).rows as Array<{ name: string }>;
|
|
271
|
+
expect(sealed(crossed!.name)).toBe(true);
|
|
272
|
+
// A tenant's dump opens with the tenant's key alone.
|
|
273
|
+
expect(derivePiiKeys(zetaKey).read(String(z!.email))).toBe('ada@example.com');
|
|
274
|
+
},
|
|
275
|
+
TIMEOUT,
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('the backfill at start', () => {
|
|
280
|
+
it(
|
|
281
|
+
'seals what an older version wrote, indexes it, and moves the unique constraints',
|
|
282
|
+
async () => {
|
|
283
|
+
const { db, q } = await beforeTheBackfill(await scratchDatabase());
|
|
284
|
+
await plantLegacyRows(q);
|
|
285
|
+
|
|
286
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
287
|
+
|
|
288
|
+
// users: ciphertext with the spelling kept, the index over the canonical
|
|
289
|
+
// address, and a NULL left a NULL.
|
|
290
|
+
const people = await q(`select email, email_bidx, name, avatar_url from users`);
|
|
291
|
+
expect(people.every((u) => sealed(u.email) && sealed(u.name))).toBe(true);
|
|
292
|
+
const ada = people.find((u) => opened(u.email) === 'Ada@Example.com')!;
|
|
293
|
+
const bo = people.find((u) => opened(u.email) === 'bo@example.com')!;
|
|
294
|
+
expect(ada.email_bidx).toBe(keys.index('ada@example.com'));
|
|
295
|
+
expect(ada.avatar_url).toBeNull();
|
|
296
|
+
expect(opened(bo.avatar_url)).toBe('https://img.example.com/bo.png');
|
|
297
|
+
expect(sealed(bo.avatar_url)).toBe(true);
|
|
298
|
+
|
|
299
|
+
// change_requests: the text, the author, the recorded refusal; the
|
|
300
|
+
// body the database defaulted to '' stays ''. The NAME on the refusal
|
|
301
|
+
// goes: an older version kept no address beside it, so nothing could
|
|
302
|
+
// ever find it again for the person it names.
|
|
303
|
+
const [cr] = await q(`select * from change_requests`);
|
|
304
|
+
expect([cr!.title, cr!.author_email, cr!.author_name, cr!.apply_failure_reason].every(sealed)).toBe(true);
|
|
305
|
+
expect(cr!.apply_failed_by_name).toBeNull();
|
|
306
|
+
expect(cr!.apply_failed_by_email_bidx).toBeNull();
|
|
307
|
+
expect(cr!.apply_failed_at).not.toBeNull();
|
|
308
|
+
expect(cr!.body).toBe('');
|
|
309
|
+
expect(opened(cr!.apply_failure_reason)).toBe('bo@example.com is not an approver');
|
|
310
|
+
expect(cr!.author_email_bidx).toBe(keys.index('ada@example.com'));
|
|
311
|
+
|
|
312
|
+
const log = await q(`select * from pr_merge_log`);
|
|
313
|
+
expect(log.every((r) => sealed(r.triggered_by_email) && sealed(r.triggered_by_name))).toBe(true);
|
|
314
|
+
expect(log.map((r) => (r.error === null ? null : opened(r.error))).sort()).toEqual(['Bo has no push right', null]);
|
|
315
|
+
expect(log.every((r) => r.error === null || sealed(r.error))).toBe(true);
|
|
316
|
+
expect(log.every((r) => r.triggered_by_email_bidx === keys.index(opened(r.triggered_by_email)))).toBe(true);
|
|
317
|
+
|
|
318
|
+
const [comment] = await q(`select * from pr_comments`);
|
|
319
|
+
expect([comment!.author_email, comment!.author_name, comment!.body].every(sealed)).toBe(true);
|
|
320
|
+
expect(comment!.author_email_bidx).toBe(keys.index('bo@example.com'));
|
|
321
|
+
|
|
322
|
+
const [queued] = await q(`select * from pending_commits`);
|
|
323
|
+
expect([queued!.author_email, queued!.author_name, queued!.last_error].every(sealed)).toBe(true);
|
|
324
|
+
expect(queued!.author_email_bidx).toBe(keys.index('ada@example.com'));
|
|
325
|
+
|
|
326
|
+
const [lock] = await q(`select * from file_locks`);
|
|
327
|
+
expect(sealed(lock!.holder_name)).toBe(true);
|
|
328
|
+
expect(opened(lock!.holder_name)).toBe('Ada');
|
|
329
|
+
|
|
330
|
+
// The two spellings were one person's: the earliest row of each stays.
|
|
331
|
+
const approvals = await q(`select * from pr_file_approvals`);
|
|
332
|
+
expect(approvals).toHaveLength(1);
|
|
333
|
+
expect(opened(approvals[0]!.approver_name)).toBe('Ada');
|
|
334
|
+
expect(approvals[0]!.approver_email_bidx).toBe(keys.index('ada@example.com'));
|
|
335
|
+
const requests = await q(`select * from plugin_join_requests`);
|
|
336
|
+
expect(requests).toHaveLength(1);
|
|
337
|
+
expect(opened(requests[0]!.requester_name)).toBe('Ada');
|
|
338
|
+
expect(opened(requests[0]!.failure_reason)).toBe('git refused Ada');
|
|
339
|
+
expect(requests[0]!.requester_email_bidx).toBe(keys.index('ada@example.com'));
|
|
340
|
+
|
|
341
|
+
// Nothing a dump would show names anyone.
|
|
342
|
+
expect(await everythingReadable(q)).not.toMatch(/example\.com|Ada|Bo /);
|
|
343
|
+
|
|
344
|
+
// Uniqueness moved from the raw columns to the blind indexes.
|
|
345
|
+
const indexes = (await q(`select indexname from pg_indexes where schemaname = current_schema()`)).map(
|
|
346
|
+
(r) => r.indexname,
|
|
347
|
+
);
|
|
348
|
+
expect(indexes).toEqual(
|
|
349
|
+
expect.arrayContaining([
|
|
350
|
+
'users_email_bidx_unq',
|
|
351
|
+
'pr_file_approvals_bidx_unq',
|
|
352
|
+
'plugin_join_requests_requester_bidx_plugin_unq',
|
|
353
|
+
]),
|
|
354
|
+
);
|
|
355
|
+
expect(indexes).not.toContain('users_email_unique');
|
|
356
|
+
expect(indexes).not.toContain('pr_file_approvals_unq');
|
|
357
|
+
expect(indexes).not.toContain('plugin_join_requests_requester_plugin_unq');
|
|
358
|
+
await expect(
|
|
359
|
+
q(`insert into users (email, email_bidx, name) values ('x', '${keys.index('ADA@example.com')}', 'x')`),
|
|
360
|
+
).rejects.toThrow();
|
|
361
|
+
await expect(q(`insert into users (email, name) values ('x', 'x')`)).rejects.toThrow();
|
|
362
|
+
|
|
363
|
+
// And the application reads it back as it was written.
|
|
364
|
+
const [read] = await db.select().from(users).where(eq(users.emailBidx, 'ADA@EXAMPLE.COM '));
|
|
365
|
+
expect(read).toMatchObject({ email: 'Ada@Example.com', name: 'Ada', avatarUrl: null });
|
|
366
|
+
},
|
|
367
|
+
TIMEOUT,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* A blob is recognised by its shape, and a title or a name is text a
|
|
372
|
+
* person chose. Before the first backfill nothing is sealed, so a value in
|
|
373
|
+
* that shape is plaintext: trusted, it stayed in clear for good, and the
|
|
374
|
+
* one the key check sampled refused the start under the right key.
|
|
375
|
+
*/
|
|
376
|
+
it(
|
|
377
|
+
'seals a value an older version stored in the very shape of a sealed one, and starts',
|
|
378
|
+
async () => {
|
|
379
|
+
const { db, q } = await beforeTheBackfill(await scratchDatabase());
|
|
380
|
+
await plantLegacyRows(q);
|
|
381
|
+
// Shaped exactly like a blob, under no key at all — and the same with
|
|
382
|
+
// its tag written unpadded, which the two predicates used to disagree
|
|
383
|
+
// about.
|
|
384
|
+
const exact = `pii:v1:${'A'.repeat(16)}:${'B'.repeat(22)}==:Q2xlYXI=`;
|
|
385
|
+
const unpadded = `pii:v1:${'A'.repeat(16)}:${'B'.repeat(22)}:Q2xlYXI=`;
|
|
386
|
+
await q(`update users set name = '${exact}' where email = 'bo@example.com'`);
|
|
387
|
+
await q(`update pr_comments set body = '${unpadded}'`);
|
|
388
|
+
await q(`update file_locks set holder_name = '${exact}'`);
|
|
389
|
+
|
|
390
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
391
|
+
|
|
392
|
+
const [bo] = await q(`select name from users where email_bidx = '${keys.index('bo@example.com')}'`);
|
|
393
|
+
expect(bo!.name).not.toBe(exact);
|
|
394
|
+
expect(opened(bo!.name)).toBe(exact);
|
|
395
|
+
const [comment] = await q(`select body from pr_comments`);
|
|
396
|
+
expect(comment!.body).not.toBe(unpadded);
|
|
397
|
+
expect(opened(comment!.body)).toBe(unpadded);
|
|
398
|
+
const [lock] = await q(`select holder_name from file_locks`);
|
|
399
|
+
expect(opened(lock!.holder_name)).toBe(exact);
|
|
400
|
+
// And what the application reads back is what was typed.
|
|
401
|
+
const [read] = await db.select().from(users).where(eq(users.emailBidx, 'bo@example.com'));
|
|
402
|
+
expect(read!.name).toBe(exact);
|
|
403
|
+
// A second start finds nothing left to do, and is not refused.
|
|
404
|
+
const after = await everything(q);
|
|
405
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
406
|
+
expect(await everything(q)).toBe(after);
|
|
407
|
+
},
|
|
408
|
+
TIMEOUT,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
it(
|
|
412
|
+
'seals a table larger than one batch, every row of it',
|
|
413
|
+
async () => {
|
|
414
|
+
const { db, q } = await beforeTheBackfill(await scratchDatabase());
|
|
415
|
+
await plantLegacyRows(q);
|
|
416
|
+
await q(`insert into pr_comments (pr_number, author_email, author_name, head_sha, body)
|
|
417
|
+
select n, 'ada@example.com', 'Ada', 'abc', 'comment ' || n from generate_series(2, 1300) n`);
|
|
418
|
+
|
|
419
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
420
|
+
|
|
421
|
+
const [left] = await q(`select count(*)::int as n from pr_comments where body not like 'pii:v1:%' or author_email_bidx is null`);
|
|
422
|
+
expect(left!.n).toBe(0);
|
|
423
|
+
const [all] = await q(`select count(*)::int as n from pr_comments`);
|
|
424
|
+
expect(all!.n).toBe(1300);
|
|
425
|
+
const [one] = await q(`select body from pr_comments where pr_number = 1300`);
|
|
426
|
+
expect(opened(one!.body)).toBe('comment 1300');
|
|
427
|
+
},
|
|
428
|
+
TIMEOUT,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
it(
|
|
432
|
+
'keeps the name on a refusal that has its index, and takes it off one that has none',
|
|
433
|
+
async () => {
|
|
434
|
+
const { db, q } = await beforeTheBackfill(await scratchDatabase());
|
|
435
|
+
await plantLegacyRows(q);
|
|
436
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
437
|
+
// Recorded on this release: the name with the index of its address.
|
|
438
|
+
await q(`update change_requests set apply_failed_by_name = '${keys.seal('Bo')}', apply_failed_by_email_bidx = '${keys.index('bo@example.com')}'`);
|
|
439
|
+
// And one an older version, still running, wrote beside it: a name alone.
|
|
440
|
+
await q(`insert into change_requests (source_branch, target_branch, title, author_email, author_email_bidx, author_name, apply_failure_reason, apply_failed_by_name, apply_failed_at)
|
|
441
|
+
values ('e', 'main', '${keys.seal('Another')}', '${keys.seal('ada@example.com')}', '${keys.index('ada@example.com')}', '${keys.seal('Ada')}', 'refused', 'Bo', now())`);
|
|
442
|
+
|
|
443
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
444
|
+
|
|
445
|
+
const rows = await q(`select source_branch, apply_failed_by_name, apply_failure_reason from change_requests order by source_branch`);
|
|
446
|
+
expect(opened(rows[0]!.apply_failed_by_name)).toBe('Bo');
|
|
447
|
+
expect(rows[1]!.apply_failed_by_name).toBeNull();
|
|
448
|
+
expect(opened(rows[1]!.apply_failure_reason)).toBe('refused');
|
|
449
|
+
},
|
|
450
|
+
TIMEOUT,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
it(
|
|
454
|
+
'writes nothing on a second start',
|
|
455
|
+
async () => {
|
|
456
|
+
const { db, q } = await beforeTheBackfill(await scratchDatabase());
|
|
457
|
+
await plantLegacyRows(q);
|
|
458
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
459
|
+
const after = await everything(q);
|
|
460
|
+
|
|
461
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
462
|
+
|
|
463
|
+
expect(await everything(q)).toBe(after);
|
|
464
|
+
},
|
|
465
|
+
TIMEOUT,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
it(
|
|
469
|
+
'stops the start on two accounts with one address, names both, and writes nothing',
|
|
470
|
+
async () => {
|
|
471
|
+
const { db, q } = await beforeTheBackfill(await scratchDatabase());
|
|
472
|
+
await plantLegacyRows(q);
|
|
473
|
+
await q(`insert into users (email, name) values ('BO@example.com', 'Bo too')`);
|
|
474
|
+
const before = await everything(q);
|
|
475
|
+
const ids = (await q(`select id from users where lower(email) = 'bo@example.com'`)).map((r) => String(r.id));
|
|
476
|
+
|
|
477
|
+
const refusal = await runCoreMigrations(db, coreMigrationsDir()).then(
|
|
478
|
+
() => '',
|
|
479
|
+
(err: Error) => err.message,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
expect(refusal).toMatch(/share the same email/);
|
|
483
|
+
for (const id of ids) expect(refusal).toContain(id);
|
|
484
|
+
// One transaction: the rows already rewritten when the conflict was
|
|
485
|
+
// found are rolled back with it, the duplicate collapse included.
|
|
486
|
+
expect(await everything(q)).toBe(before);
|
|
487
|
+
},
|
|
488
|
+
TIMEOUT,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
it(
|
|
492
|
+
'refuses a start under a key that does not open what is sealed, and changes nothing',
|
|
493
|
+
async () => {
|
|
494
|
+
const url = await scratchDatabase();
|
|
495
|
+
const { db, q } = await beforeTheBackfill(url);
|
|
496
|
+
await plantLegacyRows(q);
|
|
497
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
498
|
+
const after = await everything(q);
|
|
499
|
+
|
|
500
|
+
const rekeyed = handle(url, { key: randomBytes(32).toString('base64') });
|
|
501
|
+
await expect(runCoreMigrations(rekeyed, coreMigrationsDir())).rejects.toThrow(/does not open/);
|
|
502
|
+
expect(await everything(q)).toBe(after);
|
|
503
|
+
|
|
504
|
+
await expect(runCoreMigrations(db, coreMigrationsDir())).resolves.toBeUndefined();
|
|
505
|
+
},
|
|
506
|
+
TIMEOUT,
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
it(
|
|
510
|
+
'recomputes the index of an address an older writer replaced in clear',
|
|
511
|
+
async () => {
|
|
512
|
+
const { db, q } = await beforeTheBackfill(await scratchDatabase());
|
|
513
|
+
await plantLegacyRows(q);
|
|
514
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
515
|
+
// A version from before the upgrade, still running, renames the account:
|
|
516
|
+
// it knows nothing of the index beside the column.
|
|
517
|
+
await q(`update users set email = 'Bo.New@Example.com' where email_bidx = '${keys.index('bo@example.com')}'`);
|
|
518
|
+
|
|
519
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
520
|
+
|
|
521
|
+
const [row] = await q(`select email, email_bidx from users where email_bidx = '${keys.index('bo.new@example.com')}'`);
|
|
522
|
+
expect(sealed(row?.email)).toBe(true);
|
|
523
|
+
expect(opened(row?.email)).toBe('Bo.New@Example.com');
|
|
524
|
+
expect(await q(`select 1 from users where email_bidx = '${keys.index('bo@example.com')}'`)).toHaveLength(0);
|
|
525
|
+
},
|
|
526
|
+
TIMEOUT,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
it(
|
|
530
|
+
"works in a tenant's schema, under the tenant's key, and leaves the default schema alone",
|
|
531
|
+
async () => {
|
|
532
|
+
const url = await scratchDatabase();
|
|
533
|
+
const main = await beforeTheBackfill(url);
|
|
534
|
+
await plantLegacyRows(main.q);
|
|
535
|
+
const untouched = await everything(main.q);
|
|
536
|
+
const tenantKey = randomBytes(32).toString('base64');
|
|
537
|
+
const tenantKeys = derivePiiKeys(tenantKey);
|
|
538
|
+
const tenant = await beforeTheBackfill(url, { schema: 't_acme', key: tenantKey });
|
|
539
|
+
await plantLegacyRows(tenant.q);
|
|
540
|
+
|
|
541
|
+
await runCoreMigrations(tenant.db, coreMigrationsDir());
|
|
542
|
+
|
|
543
|
+
const people = await tenant.q(`select email, email_bidx from users`);
|
|
544
|
+
expect(people).toHaveLength(2);
|
|
545
|
+
expect(
|
|
546
|
+
people.every((u) => sealed(u.email) && u.email_bidx === tenantKeys.index(tenantKeys.read(String(u.email)))),
|
|
547
|
+
).toBe(true);
|
|
548
|
+
// Under the tenant's key and no other.
|
|
549
|
+
expect(people.every((u) => !keys.open(String(u.email)).ok)).toBe(true);
|
|
550
|
+
expect(await tenant.q(`select 1 from pr_file_approvals`)).toHaveLength(1);
|
|
551
|
+
expect(await everythingReadable(tenant.q)).not.toMatch(/example\.com/);
|
|
552
|
+
const indexes = (await tenant.q(`select indexname from pg_indexes where schemaname = 't_acme'`)).map((r) => r.indexname);
|
|
553
|
+
expect(indexes).toContain('users_email_bidx_unq');
|
|
554
|
+
expect(indexes).not.toContain('users_email_unique');
|
|
555
|
+
|
|
556
|
+
expect(await everything(main.q)).toBe(untouched);
|
|
557
|
+
},
|
|
558
|
+
TIMEOUT,
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
});
|