@bevel-software/platform-core-backend 0.26.0 → 0.27.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/agent-guide/access-control.md +17 -0
- package/agent-guide/conventions.md +8 -0
- package/agent-guide/html-views.md +83 -0
- package/agent-guide/new-knowledge-base.md +24 -0
- package/agent-guide/tool-manuals.md +10 -0
- package/dist/assets.d.ts +19 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +26 -2
- package/dist/assets.js.map +1 -1
- package/dist/core/catalog-revision.d.ts +19 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +14 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/create-core-server.d.ts +5 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +54 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +26 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +100 -8
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +18 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +21 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +22 -1
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +11 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +1 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.js +8 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -1
- package/dist/modules/agent-guide/index.d.ts +1 -1
- package/dist/modules/agent-guide/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/index.js +1 -1
- package/dist/modules/agent-guide/index.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +5 -0
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +19 -2
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts +21 -2
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +43 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
- package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
- package/dist/modules/audit/agent-connection.routes.js +38 -0
- package/dist/modules/audit/agent-connection.routes.js.map +1 -0
- package/dist/modules/audit/audit.contract.d.ts +24 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/index.d.ts +1 -0
- package/dist/modules/audit/index.d.ts.map +1 -1
- package/dist/modules/audit/index.js +1 -0
- package/dist/modules/audit/index.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +10 -3
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +7 -0
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +1 -1
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +11 -3
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +96 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +32 -3
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/embed/account-link.service.d.ts +42 -0
- package/dist/modules/embed/account-link.service.d.ts.map +1 -0
- package/dist/modules/embed/account-link.service.js +50 -0
- package/dist/modules/embed/account-link.service.js.map +1 -0
- package/dist/modules/embed/embed-link.d.ts +58 -0
- package/dist/modules/embed/embed-link.d.ts.map +1 -0
- package/dist/modules/embed/embed-link.js +160 -0
- package/dist/modules/embed/embed-link.js.map +1 -0
- package/dist/modules/embed/embed.errors.d.ts +30 -0
- package/dist/modules/embed/embed.errors.d.ts.map +1 -0
- package/dist/modules/embed/embed.errors.js +42 -0
- package/dist/modules/embed/embed.errors.js.map +1 -0
- package/dist/modules/embed/embed.interface.d.ts +204 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -0
- package/dist/modules/embed/embed.interface.js +2 -0
- package/dist/modules/embed/embed.interface.js.map +1 -0
- package/dist/modules/embed/embed.routes.d.ts +59 -0
- package/dist/modules/embed/embed.routes.d.ts.map +1 -0
- package/dist/modules/embed/embed.routes.js +343 -0
- package/dist/modules/embed/embed.routes.js.map +1 -0
- package/dist/modules/embed/embed.service.d.ts +169 -0
- package/dist/modules/embed/embed.service.d.ts.map +1 -0
- package/dist/modules/embed/embed.service.js +627 -0
- package/dist/modules/embed/embed.service.js.map +1 -0
- package/dist/modules/embed/embed.tools.d.ts +51 -0
- package/dist/modules/embed/embed.tools.d.ts.map +1 -0
- package/dist/modules/embed/embed.tools.js +170 -0
- package/dist/modules/embed/embed.tools.js.map +1 -0
- package/dist/modules/embed/mcp-app.d.ts +64 -0
- package/dist/modules/embed/mcp-app.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.js +147 -0
- package/dist/modules/embed/mcp-app.js.map +1 -0
- package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
- package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.routes.js +36 -0
- package/dist/modules/embed/mcp-app.routes.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +15 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
- package/dist/modules/github-app/github-app.connection.js +34 -3
- package/dist/modules/github-app/github-app.connection.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +300 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.js +6 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +28 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +64 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.js +7 -0
- package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.js +81 -0
- package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.js +492 -0
- package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
- package/dist/modules/onboarding/starter-packs.d.ts +78 -0
- package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-packs.js +156 -0
- package/dist/modules/onboarding/starter-packs.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +18 -1
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +0 -36
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +31 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +134 -1
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/repository-source.d.ts +1 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -1
- package/dist/modules/settings/repository-source.js +3 -3
- package/dist/modules/settings/repository-source.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +50 -32
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +59 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/index.d.ts +28 -1
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +28 -1
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
- package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-argument-check.js +132 -0
- package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
- package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
- package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +70 -6
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +157 -8
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
- 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-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +6 -2
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +27 -5
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +4 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
- package/dist/modules/tool-registry/guide-first.js +9 -1
- package/dist/modules/tool-registry/guide-first.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +29 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +62 -5
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +172 -36
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +78 -6
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +25 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +23 -6
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +1 -1
- 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 +2 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +16 -0
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +45 -0
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +183 -15
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +805 -106
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
- package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.builder.js +238 -0
- package/dist/modules/workspace/agent-download.builder.js.map +1 -0
- package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.routes.js +156 -0
- package/dist/modules/workspace/agent-download.routes.js.map +1 -0
- package/dist/modules/workspace/agent-download.store.d.ts +225 -0
- package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.store.js +423 -0
- package/dist/modules/workspace/agent-download.store.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
- package/dist/modules/workspace/agent-upload.store.js +16 -6
- package/dist/modules/workspace/agent-upload.store.js.map +1 -1
- package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
- package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
- package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
- package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +5 -8
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/first-run.d.ts +111 -0
- package/dist/modules/workspace/first-run.d.ts.map +1 -0
- package/dist/modules/workspace/first-run.js +181 -0
- package/dist/modules/workspace/first-run.js.map +1 -0
- package/dist/modules/workspace/move-links.d.ts +92 -0
- package/dist/modules/workspace/move-links.d.ts.map +1 -0
- package/dist/modules/workspace/move-links.js +173 -0
- package/dist/modules/workspace/move-links.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +3 -13
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +63 -22
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +75 -2
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +197 -24
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +59 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +443 -65
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts.map +1 -1
- package/dist/modules/write-access/write-access.js +9 -0
- package/dist/modules/write-access/write-access.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +73 -1
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +86 -12
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +10 -1
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/utf8-text.d.ts +15 -0
- package/dist/shared/utf8-text.d.ts.map +1 -0
- package/dist/shared/utf8-text.js +20 -0
- package/dist/shared/utf8-text.js.map +1 -0
- package/dist/tenancy/static-tenant-source.d.ts +2 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +9 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/mcp-app/page.html +349 -0
- package/migrations/0017_atlassian_account_links.sql +37 -0
- package/migrations/meta/0017_snapshot.json +2396 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +5 -3
- package/src/__tests__/packaged-assets-ship.test.ts +16 -4
- package/src/assets.ts +28 -2
- package/src/core/__tests__/catalog-revision.test.ts +40 -0
- package/src/core/__tests__/gated-boot.test.ts +1 -0
- package/src/core/__tests__/lifecycle.test.ts +14 -0
- package/src/core/catalog-revision.ts +15 -2
- package/src/core/create-core-server.ts +67 -3
- package/src/core/create-core-services.ts +140 -6
- package/src/core/lifecycle.ts +28 -2
- package/src/core-config.ts +24 -1
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
- package/src/modules/access/access-control.interface.ts +13 -0
- package/src/modules/access/access-control.service.ts +16 -0
- package/src/modules/access/directory-sync-bot.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
- package/src/modules/agent-guide/agent-guide.ts +9 -0
- package/src/modules/agent-guide/index.ts +1 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
- package/src/modules/agent-instructions/compose.ts +17 -2
- package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
- package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
- package/src/modules/audit/agent-audit.service.ts +58 -3
- package/src/modules/audit/agent-connection.routes.ts +56 -0
- package/src/modules/audit/audit.contract.ts +26 -0
- package/src/modules/audit/index.ts +1 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
- package/src/modules/auth/account.routes.ts +10 -3
- package/src/modules/auth/auth.middleware.ts +1 -1
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/database/core-schema.ts +33 -3
- package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
- package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
- package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
- package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
- package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
- package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
- package/src/modules/embed/account-link.service.ts +76 -0
- package/src/modules/embed/embed-link.ts +180 -0
- package/src/modules/embed/embed.errors.ts +43 -0
- package/src/modules/embed/embed.interface.ts +203 -0
- package/src/modules/embed/embed.routes.ts +363 -0
- package/src/modules/embed/embed.service.ts +705 -0
- package/src/modules/embed/embed.tools.ts +206 -0
- package/src/modules/embed/mcp-app.routes.ts +41 -0
- package/src/modules/embed/mcp-app.ts +173 -0
- package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
- package/src/modules/github-app/github-app.connection.ts +38 -4
- package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
- package/src/modules/kb-fs/locking-filesystem.ts +287 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
- package/src/modules/kb-sync/kb-sync.service.ts +6 -0
- package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
- package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
- package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
- package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
- package/src/modules/mcp/mcp.service.ts +81 -12
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
- package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
- package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
- package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
- package/src/modules/onboarding/onboarding.contract.ts +85 -0
- package/src/modules/onboarding/onboarding.routes.ts +85 -0
- package/src/modules/onboarding/starter-pack.service.ts +580 -0
- package/src/modules/onboarding/starter-packs.ts +182 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
- package/src/modules/plugins/index.ts +1 -0
- package/src/modules/plugins/plugin-provision.service.ts +22 -1
- package/src/modules/plugins/plugins.routes.ts +29 -0
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
- package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
- package/src/modules/settings/deployment-settings.service.ts +131 -1
- package/src/modules/settings/repository-source.ts +4 -4
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
- package/src/modules/skills/skills.tools.ts +55 -35
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
- package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
- package/src/modules/tool-auth/external-api-key.service.ts +61 -4
- package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
- package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
- package/src/modules/tool-helpers/index.ts +34 -1
- package/src/modules/tool-helpers/route-argument-check.ts +149 -0
- package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
- package/src/modules/tool-helpers/tool-def.ts +132 -7
- package/src/modules/tool-helpers/tool-handler.ts +196 -8
- package/src/modules/tool-helpers/tool.contract.ts +9 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
- package/src/modules/tool-registry/description-length.ts +34 -7
- package/src/modules/tool-registry/guide-first.ts +8 -1
- package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
- package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
- package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
- package/src/modules/workflow/file-lock.service.ts +67 -5
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
- package/src/modules/workflow/git/git.service.ts +176 -38
- package/src/modules/workflow/git/node-git-runner.ts +82 -7
- package/src/modules/workflow/pending-commits.service.ts +29 -1
- package/src/modules/workflow/pending-commits.worker.ts +24 -6
- package/src/modules/workflow/recovery-bot.ts +1 -1
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
- package/src/modules/workflow/sanitize-error.ts +50 -0
- package/src/modules/workflow/workflow.service.ts +879 -112
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
- package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
- package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
- package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
- package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
- package/src/modules/workspace/agent-download.builder.ts +308 -0
- package/src/modules/workspace/agent-download.routes.ts +183 -0
- package/src/modules/workspace/agent-download.store.ts +533 -0
- package/src/modules/workspace/agent-upload.store.ts +23 -7
- package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
- package/src/modules/workspace/file-readers/text-reader.ts +5 -8
- package/src/modules/workspace/first-run.ts +222 -0
- package/src/modules/workspace/move-links.ts +237 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
- package/src/modules/workspace/workspace.routes.ts +64 -22
- package/src/modules/workspace/workspace.service.ts +202 -25
- package/src/modules/workspace/workspace.tools.ts +492 -63
- package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
- package/src/modules/write-access/write-access.ts +9 -0
- package/src/shared/domain-errors.ts +108 -11
- package/src/shared/git.contract.ts +8 -1
- package/src/shared/utf8-text.ts +21 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
- package/src/tenancy/static-tenant-source.ts +10 -2
- package/starter-packs/README.md +103 -0
- package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
- package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
- package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
- package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
- package/starter-packs/engineering/pack.yaml +11 -0
- package/starter-packs/general/KnowledgeBase/About us.md +9 -0
- package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
- package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
- package/starter-packs/general/Plugins/general-starter/access.md +19 -0
- package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
- package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
- package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
- package/starter-packs/general/pack.yaml +10 -0
- package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
- package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
- package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
- package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
- package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
- package/starter-packs/operations/pack.yaml +12 -0
- package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
- package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
- package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
- package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
- package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
- package/starter-packs/sales/pack.yaml +13 -0
- package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
- package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
- package/starter-packs/support/KnowledgeBase/Products.md +9 -0
- package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
- package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
- package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
- package/starter-packs/support/Plugins/support-starter/access.md +19 -0
- package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
- package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
- package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
- package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
- package/starter-packs/support/pack.yaml +12 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { timingSafeStringEqual } from '../auth/password-hash.js';
|
|
3
|
+
import { ACCOUNT_DEACTIVATED_MESSAGE } from '../auth/account-admission.js';
|
|
4
|
+
import jwt from 'jsonwebtoken';
|
|
5
|
+
import { suggestionsBranchPrefixFor } from '@bevel-software/platform-shared';
|
|
6
|
+
import { logger } from '../../shared/logging.js';
|
|
7
|
+
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
8
|
+
import { changeRequestLink, changeRequestLinkBase } from '../workflow/git/change-request-link.js';
|
|
9
|
+
import { EmbedAccessError, EmbedLockedError, EmbedNodeNotFoundError, EmbedTokenError, } from './embed.errors.js';
|
|
10
|
+
import { parseEmbedRef, EmbedRefParseError, isSafeRepoRelativeEmbedPath } from './embed-link.js';
|
|
11
|
+
const log = logger('embed');
|
|
12
|
+
/**
|
|
13
|
+
* Token lifetime — long enough for a view left open in a chat, short enough
|
|
14
|
+
* that a token copied out of a transcript stops working on its own. The
|
|
15
|
+
* token rides in `open_page`'s result, so it sits in the chat transcript, and
|
|
16
|
+
* whoever holds it reads and edits that one file as the token's user until
|
|
17
|
+
* it expires. One hour (Razvan, 2026-10-09; the embed used two before): a
|
|
18
|
+
* page left open in a chat is still covered, and the window is halved. A
|
|
19
|
+
* view older than that shows the expired sentence and the agent opens the
|
|
20
|
+
* page again.
|
|
21
|
+
*/
|
|
22
|
+
export const EMBED_TOKEN_TTL_SECONDS = 60 * 60;
|
|
23
|
+
export class EmbedService {
|
|
24
|
+
config;
|
|
25
|
+
kb;
|
|
26
|
+
workspaceService;
|
|
27
|
+
accessControl;
|
|
28
|
+
authService;
|
|
29
|
+
workflowService;
|
|
30
|
+
gitService;
|
|
31
|
+
accountLinks;
|
|
32
|
+
readers;
|
|
33
|
+
validateWrite;
|
|
34
|
+
resolveNodeId;
|
|
35
|
+
/**
|
|
36
|
+
* Embed tokens are signed with a key DERIVED from the JWT secret (HMAC with
|
|
37
|
+
* a fixed label) rather than the secret itself — domain separation, so an
|
|
38
|
+
* embed token can never be replayed as a session JWT, nor the reverse. The
|
|
39
|
+
* label is part of the token format: changing it invalidates every live
|
|
40
|
+
* token, so it does not change.
|
|
41
|
+
*/
|
|
42
|
+
signingKey;
|
|
43
|
+
constructor(config, kb, workspaceService, accessControl, authService, workflowService, gitService, accountLinks, readers,
|
|
44
|
+
/**
|
|
45
|
+
* The pre-disk write gate every other write surface runs — the file
|
|
46
|
+
* editor's and the agent tools' `roles.yaml` validity check. An embed
|
|
47
|
+
* save or proposal writes through `workspaceService.writeFile`, which
|
|
48
|
+
* puts bytes on disk without one, so the gate is asked here first.
|
|
49
|
+
*/
|
|
50
|
+
validateWrite,
|
|
51
|
+
/** See {@link EmbedNodeIdResolver} — core has none, so core refuses an id reference. */
|
|
52
|
+
resolveNodeId = null) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.kb = kb;
|
|
55
|
+
this.workspaceService = workspaceService;
|
|
56
|
+
this.accessControl = accessControl;
|
|
57
|
+
this.authService = authService;
|
|
58
|
+
this.workflowService = workflowService;
|
|
59
|
+
this.gitService = gitService;
|
|
60
|
+
this.accountLinks = accountLinks;
|
|
61
|
+
this.readers = readers;
|
|
62
|
+
this.validateWrite = validateWrite;
|
|
63
|
+
this.resolveNodeId = resolveNodeId;
|
|
64
|
+
this.signingKey = createHmac('sha256', config.jwtSecret)
|
|
65
|
+
.update('bevel-embed-token-v1')
|
|
66
|
+
.digest('hex');
|
|
67
|
+
}
|
|
68
|
+
// ── mint ───────────────────────────────────────────────────────────────────
|
|
69
|
+
sharedSecretConfigured() {
|
|
70
|
+
return this.config.embedSharedSecret.length > 0;
|
|
71
|
+
}
|
|
72
|
+
verifySharedSecret(secret) {
|
|
73
|
+
const expected = this.config.embedSharedSecret;
|
|
74
|
+
if (!expected || !secret)
|
|
75
|
+
return false;
|
|
76
|
+
// The one constant-time comparison the platform has (the password hash's).
|
|
77
|
+
return timingSafeStringEqual(secret, expected);
|
|
78
|
+
}
|
|
79
|
+
async mintForUser(input) {
|
|
80
|
+
if (!input.userId)
|
|
81
|
+
throw new EmbedAccessError('Missing user id');
|
|
82
|
+
return this.mint({ kind: 'user', userId: input.userId }, input.reference);
|
|
83
|
+
}
|
|
84
|
+
async mintToken(input) {
|
|
85
|
+
if (!input.accountId)
|
|
86
|
+
throw new EmbedAccessError('Missing Atlassian account id');
|
|
87
|
+
// The optional allowed-email-domains guard: a viewer whose domain may not
|
|
88
|
+
// reach the knowledge base must not receive a token either. With a guard
|
|
89
|
+
// configured and no email sent we fail CLOSED — we then cannot confirm the
|
|
90
|
+
// viewer is allowed. (Unchanged from the connector's contract.)
|
|
91
|
+
const email = input.email?.trim().toLowerCase() || undefined;
|
|
92
|
+
if (!this.authService.isEmailDomainAllowed(email ?? '')) {
|
|
93
|
+
throw new EmbedAccessError('Your email domain is not permitted to view this content');
|
|
94
|
+
}
|
|
95
|
+
return this.mint({ kind: 'atlassian', accountId: input.accountId }, input.reference);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The one mint. Resolves the reference to a path on the default branch,
|
|
99
|
+
* confirms the FILE EXISTS, and signs the claims.
|
|
100
|
+
*
|
|
101
|
+
* Access is NOT checked here and must not be: the two callers check it
|
|
102
|
+
* their own way — `open_page` has already answered the caller the way
|
|
103
|
+
* `read_file` would (refusal and all) before it asks for a token, and the
|
|
104
|
+
* connector's mint is for an account that may not be linked to anyone yet.
|
|
105
|
+
* The load path gates content on the resolved identity's read access, which
|
|
106
|
+
* is the gate that decides what anybody sees.
|
|
107
|
+
*/
|
|
108
|
+
async mint(subject, reference) {
|
|
109
|
+
const ref = parseEmbedRef(reference, this.config.kbDirName);
|
|
110
|
+
let repoRelative;
|
|
111
|
+
if ('nodeId' in ref) {
|
|
112
|
+
// One dot-less segment is an id by the copy-link's shape — and ALSO a
|
|
113
|
+
// legal root-level file name (`readme`, `LICENSE`). A file that is
|
|
114
|
+
// actually there wins: it is what the caller named, and `open_page`
|
|
115
|
+
// has just read it by that very path.
|
|
116
|
+
if (isSafeRepoRelativeEmbedPath(ref.nodeId) && (await this.fileExists(ref.nodeId))) {
|
|
117
|
+
repoRelative = ref.nodeId;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// A bare id is the app's copy-link form. Resolving it needs a node
|
|
121
|
+
// graph, which core does not have; a deployment that does registers a
|
|
122
|
+
// resolver (see `EmbedNodeIdResolver`).
|
|
123
|
+
const resolved = this.resolveNodeId ? await this.resolveNodeId(ref.nodeId) : null;
|
|
124
|
+
if (!resolved)
|
|
125
|
+
throw new EmbedRefParseError(`No file at the reference '${ref.nodeId}'`);
|
|
126
|
+
repoRelative = resolved;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
repoRelative = ref.repoRelative;
|
|
131
|
+
}
|
|
132
|
+
// Existence at mint time, so a dead reference fails where the caller can
|
|
133
|
+
// still say something about it rather than inside a rendered iframe. A
|
|
134
|
+
// stat, not a read: a deck or a workbook can be hundreds of megabytes,
|
|
135
|
+
// and the view fetches its bytes later, from `/raw`, if at all.
|
|
136
|
+
await this.assertExists(repoRelative);
|
|
137
|
+
const claims = {
|
|
138
|
+
scope: 'embed',
|
|
139
|
+
kind: subject.kind,
|
|
140
|
+
sub: subject.kind === 'user' ? subject.userId : subject.accountId,
|
|
141
|
+
repoRelative,
|
|
142
|
+
slug: ref.slug,
|
|
143
|
+
};
|
|
144
|
+
const token = jwt.sign(claims, this.signingKey, { expiresIn: EMBED_TOKEN_TTL_SECONDS });
|
|
145
|
+
return { token, embedUrl: this.embedUrlFor(token) };
|
|
146
|
+
}
|
|
147
|
+
/** The view's address — what a host frames. */
|
|
148
|
+
embedUrlFor(token) {
|
|
149
|
+
return `${this.config.publicFrontendUrl}/embed?token=${encodeURIComponent(token)}`;
|
|
150
|
+
}
|
|
151
|
+
/** A file's address in the app — what a link opens in a new tab. */
|
|
152
|
+
appUrlFor(repoRelative, slug) {
|
|
153
|
+
const wsPath = [this.config.kbDirName, ...repoRelative.split('/')]
|
|
154
|
+
.map(encodeURIComponent)
|
|
155
|
+
.join('/');
|
|
156
|
+
const branch = encodeURIComponent(this.kb.defaultBranch);
|
|
157
|
+
const anchor = slug ? `#${encodeURIComponent(slug)}` : '';
|
|
158
|
+
return `${this.config.publicFrontendUrl}/workspace/${branch}/${wsPath}${anchor}`;
|
|
159
|
+
}
|
|
160
|
+
// ── read ───────────────────────────────────────────────────────────────────
|
|
161
|
+
async loadFile(token) {
|
|
162
|
+
const claims = this.verifyToken(token);
|
|
163
|
+
const { linked, canRead, canWrite } = await this.resolveIdentity(claims);
|
|
164
|
+
const reader = this.readers.readerFor(claims.repoRelative);
|
|
165
|
+
const base = {
|
|
166
|
+
nodeName: nodeNameFor(claims.repoRelative),
|
|
167
|
+
repoRelative: claims.repoRelative,
|
|
168
|
+
workspacePath: this.wsPathFor(claims.repoRelative),
|
|
169
|
+
kbDirName: this.config.kbDirName,
|
|
170
|
+
branch: this.kb.defaultBranch,
|
|
171
|
+
appUrl: this.appUrlFor(claims.repoRelative, claims.slug),
|
|
172
|
+
...(claims.slug ? { heading: claims.slug } : {}),
|
|
173
|
+
content: '',
|
|
174
|
+
contentIsText: reader.textEditable,
|
|
175
|
+
linked,
|
|
176
|
+
canRead,
|
|
177
|
+
canWrite,
|
|
178
|
+
linkUrl: `${this.config.publicFrontendUrl}/embed/link?token=${encodeURIComponent(token)}`,
|
|
179
|
+
};
|
|
180
|
+
// Default-deny, exactly as the app does: no content leaves the backend
|
|
181
|
+
// until the token's identity resolves AND may read this file. The gated
|
|
182
|
+
// view still carries the status so the page can show the right prompt.
|
|
183
|
+
if (!linked || !canRead)
|
|
184
|
+
return base;
|
|
185
|
+
// A renderer that reads BYTES is handed none here — it fetches them from
|
|
186
|
+
// `/api/embed/raw` under this same token, which keeps a document or an
|
|
187
|
+
// image out of a JSON payload and off the base64 round trip.
|
|
188
|
+
if (!reader.textEditable)
|
|
189
|
+
return base;
|
|
190
|
+
const bytes = await this.readFileBytes(claims.repoRelative);
|
|
191
|
+
const result = await reader.read(bytes, claims.repoRelative);
|
|
192
|
+
// A refusal IS the file's honest textual answer (unreadable binary under
|
|
193
|
+
// an extension the fallback reader took), and the app shows it as text —
|
|
194
|
+
// but it is not the file's text, so the view neither claims it is nor
|
|
195
|
+
// offers to save over the bytes it stands for. `save` holds the same line.
|
|
196
|
+
const isText = result.kind === 'text';
|
|
197
|
+
const content = isText ? result.text : result.kind === 'refusal' ? result.message : '';
|
|
198
|
+
return { ...base, content, contentIsText: isText, canWrite: canWrite && isText };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Refuse to write text over a file whose bytes are not text: the reader
|
|
202
|
+
* that would show it answers a refusal, not the content, and a save would
|
|
203
|
+
* replace a binary with the viewer's draft.
|
|
204
|
+
*/
|
|
205
|
+
/** A file a page draws rather than a page: bytes no reader edits as text, or an SVG. */
|
|
206
|
+
isAsset(repoRelative) {
|
|
207
|
+
return !this.readers.readerFor(repoRelative).textEditable || /\.svg$/i.test(repoRelative);
|
|
208
|
+
}
|
|
209
|
+
async assertTextEditable(repoRelative) {
|
|
210
|
+
const reader = this.readers.readerFor(repoRelative);
|
|
211
|
+
if (!reader.textEditable)
|
|
212
|
+
throw new EmbedAccessError('This file is not editable as text');
|
|
213
|
+
const result = await reader.read(await this.readFileBytes(repoRelative), repoRelative);
|
|
214
|
+
if (result.kind !== 'text')
|
|
215
|
+
throw new EmbedAccessError('This file is not editable as text');
|
|
216
|
+
}
|
|
217
|
+
async readBytes(token, path) {
|
|
218
|
+
const claims = this.verifyToken(token);
|
|
219
|
+
const { linked, user } = await this.resolveIdentity(claims);
|
|
220
|
+
if (!linked || !user)
|
|
221
|
+
throw new EmbedAccessError('This view is not linked to an account');
|
|
222
|
+
// A path beside the embedded file — the pictures a markdown page shows.
|
|
223
|
+
// Resolved relative to the embedded file's own folder, then gated on the
|
|
224
|
+
// viewer's read access for THAT file: the token scopes the view to one
|
|
225
|
+
// page, never to one page's read permissions.
|
|
226
|
+
const target = path ? resolveBeside(claims.repoRelative, path) : claims.repoRelative;
|
|
227
|
+
if (target === null)
|
|
228
|
+
throw new EmbedAccessError('That path is not inside this knowledge base');
|
|
229
|
+
// ONE page, as the token promises: beside the page only an ASSET it draws
|
|
230
|
+
// is served — a file no reader edits as text, or an SVG, the one text
|
|
231
|
+
// format a page shows as a picture. Another page's text never comes
|
|
232
|
+
// through this token; that page opens in the app, under its own view.
|
|
233
|
+
if (target !== claims.repoRelative && !this.isAsset(target)) {
|
|
234
|
+
throw new EmbedAccessError('This view shows one page; another page opens in the app');
|
|
235
|
+
}
|
|
236
|
+
const workspaceId = this.defaultWorkspaceId();
|
|
237
|
+
if (!(await this.accessControl.canRead(workspaceId, user.email, target))) {
|
|
238
|
+
throw new EmbedAccessError(`You don't have permission to read "${target}".`);
|
|
239
|
+
}
|
|
240
|
+
return { bytes: await this.readFileBytes(target), path: target };
|
|
241
|
+
}
|
|
242
|
+
// ── write ──────────────────────────────────────────────────────────────────
|
|
243
|
+
async acquireLock(token) {
|
|
244
|
+
const { user, wsPath } = await this.requireEditor(token);
|
|
245
|
+
const result = await this.workflowService.acquireLock(this.defaultWorkspaceId(), this.kb.defaultBranch, wsPath, user);
|
|
246
|
+
return result.acquired
|
|
247
|
+
? { acquired: true }
|
|
248
|
+
: { acquired: false, holderName: result.lock.holderName };
|
|
249
|
+
}
|
|
250
|
+
async heartbeat(token) {
|
|
251
|
+
const claims = this.verifyToken(token);
|
|
252
|
+
const { user, canWrite } = await this.resolveIdentity(claims);
|
|
253
|
+
if (!user)
|
|
254
|
+
return; // nothing held — nothing to keep alive
|
|
255
|
+
const workspaceId = this.defaultWorkspaceId();
|
|
256
|
+
const wsPath = this.wsPathFor(claims.repoRelative);
|
|
257
|
+
// Write access is asked again on every renewal: the token outlives the
|
|
258
|
+
// permission it was minted under, and a writer who lost it must not keep
|
|
259
|
+
// a lock alive that shuts out the people who still have it. The lock
|
|
260
|
+
// goes HERE, not only when the view hears the refusal: a frame that is
|
|
261
|
+
// gone, or a host that never delivers the 403, would otherwise leave the
|
|
262
|
+
// file shut to the writers who still have access until the TTL. Letting
|
|
263
|
+
// go takes no access — the workflow releases only a lock this user holds.
|
|
264
|
+
if (!canWrite) {
|
|
265
|
+
await this.workflowService.releaseLockNoCommit(workspaceId, this.kb.defaultBranch, wsPath, user);
|
|
266
|
+
throw new EmbedAccessError();
|
|
267
|
+
}
|
|
268
|
+
await this.workflowService.heartbeatLock(workspaceId, this.kb.defaultBranch, wsPath, user);
|
|
269
|
+
}
|
|
270
|
+
async cancel(token) {
|
|
271
|
+
const claims = this.verifyToken(token);
|
|
272
|
+
const { user } = await this.resolveIdentity(claims);
|
|
273
|
+
if (!user)
|
|
274
|
+
return;
|
|
275
|
+
await this.workflowService.releaseLockNoCommit(this.defaultWorkspaceId(), this.kb.defaultBranch, this.wsPathFor(claims.repoRelative), user);
|
|
276
|
+
}
|
|
277
|
+
async save(token, content) {
|
|
278
|
+
const { claims, user, wsPath } = await this.requireEditor(token);
|
|
279
|
+
await this.assertTextEditable(claims.repoRelative);
|
|
280
|
+
// The write gate before the lock: a text the gate refuses (a `roles.yaml`
|
|
281
|
+
// that would not parse) never takes a lock it has nothing to write under.
|
|
282
|
+
await this.validateWrite(wsPath, content);
|
|
283
|
+
const workspaceId = this.defaultWorkspaceId();
|
|
284
|
+
// Bytes reach the disk ONLY under a lock this viewer holds. ASK who holds
|
|
285
|
+
// it rather than acquiring again: `acquire` is strict and refuses a live
|
|
286
|
+
// lock even to its own holder (so an agent sharing the user id cannot
|
|
287
|
+
// steal a human's edit), which turned every Save into a 409 against the
|
|
288
|
+
// writer's own Edit lock. This is the check the app's file routes make
|
|
289
|
+
// (`workspace.routes` `withLock`). A viewer whose lock lapsed — a frame
|
|
290
|
+
// hidden past the TTL — takes it again if it is free, and is refused if
|
|
291
|
+
// somebody else took it: writing first and finding out at the release
|
|
292
|
+
// would leave their text on disk under the other editor's lock.
|
|
293
|
+
await this.ensureHoldsLock(workspaceId, wsPath, user);
|
|
294
|
+
try {
|
|
295
|
+
await this.workspaceService.writeFile(workspaceId, wsPath, content);
|
|
296
|
+
// Release commits + pushes the on-disk bytes through the background
|
|
297
|
+
// queue — the same path the app's file page takes out of edit mode.
|
|
298
|
+
await this.workflowService.releaseLock(workspaceId, this.kb.defaultBranch, wsPath, user);
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
await this.workflowService
|
|
302
|
+
.releaseLockNoCommit(workspaceId, this.kb.defaultBranch, wsPath, user)
|
|
303
|
+
.catch((cleanupErr) => {
|
|
304
|
+
// Best-effort cleanup failed — say so, so a stuck lock is
|
|
305
|
+
// diagnosable, then rethrow the save's own error below.
|
|
306
|
+
log.error(`releaseLockNoCommit failed during save cleanup ` +
|
|
307
|
+
`(workspace=${workspaceId} path=${wsPath} user=${user.id})`, cleanupErr);
|
|
308
|
+
return undefined;
|
|
309
|
+
});
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* File the edit as a change request authored by the viewer, on their own
|
|
315
|
+
* personal suggestions branch — the same branch and the same one-request
|
|
316
|
+
* bundle the app's "Propose changes" uses, so a reader who proposes from a
|
|
317
|
+
* chat and from the app lands in one place.
|
|
318
|
+
*
|
|
319
|
+
* No lock: nothing touches the default branch, and the suggestions branch
|
|
320
|
+
* is this person's own.
|
|
321
|
+
*
|
|
322
|
+
* The write is COMMITTED here, synchronously, before the request is opened.
|
|
323
|
+
* `workspaceService.writeFile` only puts bytes on disk — the app's file
|
|
324
|
+
* routes commit by releasing the lock they took, which enqueues the commit
|
|
325
|
+
* for the background worker. Neither is usable here: without a commit the
|
|
326
|
+
* branch head never moves, and the request opens against a tree identical
|
|
327
|
+
* to its base — `changedFiles: 0`, and a reviewer told "this pull request
|
|
328
|
+
* has no file changes to approve" about a proposal that is sitting
|
|
329
|
+
* uncommitted on disk. An ENQUEUED commit has the same problem with a race
|
|
330
|
+
* on top of it. `commitChanges` commits and pushes before returning, which
|
|
331
|
+
* is what the one other server-side propose flow (the plugin join request)
|
|
332
|
+
* does for exactly this reason.
|
|
333
|
+
*/
|
|
334
|
+
async propose(token, content) {
|
|
335
|
+
const claims = this.verifyToken(token);
|
|
336
|
+
const { user, canRead } = await this.resolveIdentity(claims);
|
|
337
|
+
if (!user)
|
|
338
|
+
throw new EmbedAccessError('Link your account to propose a change');
|
|
339
|
+
// Read access is the floor for proposing: a change request quotes the
|
|
340
|
+
// file's new text beside its old, so proposing on a file you may not read
|
|
341
|
+
// would publish what you were not allowed to see.
|
|
342
|
+
if (!canRead)
|
|
343
|
+
throw new EmbedAccessError(`You don't have permission to read "${claims.repoRelative}".`);
|
|
344
|
+
// The guard a save runs: a proposal is text, so what it proposes over
|
|
345
|
+
// has to be text too — a PDF, an image or bytes under a markdown name
|
|
346
|
+
// are refused here as they are on Save, not committed as text.
|
|
347
|
+
await this.assertTextEditable(claims.repoRelative);
|
|
348
|
+
const wsPath = this.wsPathFor(claims.repoRelative);
|
|
349
|
+
// The same write gate as a save, before a branch is made for the text: a
|
|
350
|
+
// proposal is a commit, and a commit of an unparseable `roles.yaml` is
|
|
351
|
+
// exactly what the gate exists to keep out of the repository.
|
|
352
|
+
await this.validateWrite(wsPath, content);
|
|
353
|
+
const branch = `${suggestionsBranchPrefixFor({ email: user.email, id: user.id })}knowledge`;
|
|
354
|
+
// Create the branch when it is not there yet; an existing one is reused,
|
|
355
|
+
// which is what bundles a person's proposals into one request. Asked of
|
|
356
|
+
// ORIGIN first: a second proposal finds the branch already pushed, and
|
|
357
|
+
// creating it again would fail as a non-fast-forward push, not as
|
|
358
|
+
// "already exists" — the race between the two is still caught below.
|
|
359
|
+
// Created through the workflow service, under its branch-lifecycle lock,
|
|
360
|
+
// as every branch the app creates is: creating one is the one operation
|
|
361
|
+
// that can bring a "gone" branch back under a retirement's feet.
|
|
362
|
+
if (!(await this.gitService.remoteBranchExists(this.defaultWorkspaceId(), branch))) {
|
|
363
|
+
await this.workflowService
|
|
364
|
+
.createBranch(this.defaultWorkspaceId(), branch, this.kb.defaultBranch)
|
|
365
|
+
.catch((err) => {
|
|
366
|
+
if (err instanceof Error && /already exists|non-fast-forward|fetch first/i.test(err.message))
|
|
367
|
+
return;
|
|
368
|
+
throw err;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const workspace = await this.workspaceService.getOrCreateForBranch(branch);
|
|
372
|
+
// One proposal at a time per file on this branch: two from the same
|
|
373
|
+
// person landing together would otherwise write over each other before
|
|
374
|
+
// either commit staged the file, and one would commit the other's text.
|
|
375
|
+
// The workspace service's own per-path turn is that queue — the one the
|
|
376
|
+
// app's file routes take around a write whose decision spans more than
|
|
377
|
+
// one call — and the write inside takes the same turn, re-entrantly.
|
|
378
|
+
await this.workspaceService.withPathTurn(workspace.id, wsPath, async () => {
|
|
379
|
+
await this.workspaceService.writeFile(workspace.id, wsPath, content);
|
|
380
|
+
// Scoped to the one path this proposal is about: the suggestions branch
|
|
381
|
+
// is shared by everything this person has proposed, and a bare commit
|
|
382
|
+
// would sweep in another in-flight write of theirs under this message.
|
|
383
|
+
await this.workflowService.commitChanges(workspace.id, user, `Propose changes to ${claims.repoRelative}`, [wsPath]);
|
|
384
|
+
});
|
|
385
|
+
try {
|
|
386
|
+
const created = await this.workflowService.openChangeRequest(workspace.id, user, {
|
|
387
|
+
sourceBranch: branch,
|
|
388
|
+
targetBranch: this.kb.defaultBranch,
|
|
389
|
+
title: `Changes from ${user.name}. Knowledge`,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
branch,
|
|
393
|
+
...(typeof created?.number === 'number'
|
|
394
|
+
? { number: created.number, url: this.changeRequestUrl(created.number) }
|
|
395
|
+
: {}),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
// The person's one open Knowledge request already covers this branch —
|
|
400
|
+
// which is the state this method is trying to reach, not a failure. The
|
|
401
|
+
// refusal names it, so point at it.
|
|
402
|
+
const existing = duplicateRequestNumber(err);
|
|
403
|
+
if (existing === null)
|
|
404
|
+
throw err;
|
|
405
|
+
return { branch, number: existing, url: this.changeRequestUrl(existing) };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// ── account links ──────────────────────────────────────────────────────────
|
|
409
|
+
async linkAccount(token, userId) {
|
|
410
|
+
const claims = this.verifyToken(token);
|
|
411
|
+
// Only an outside account has anything to link; a token already minted
|
|
412
|
+
// for a Hexis user names its person and must not re-point at another.
|
|
413
|
+
if (claims.kind !== 'atlassian') {
|
|
414
|
+
throw new EmbedAccessError('This view is already signed in');
|
|
415
|
+
}
|
|
416
|
+
await this.accountLinks.link(claims.sub, userId);
|
|
417
|
+
}
|
|
418
|
+
async listLinkedAccounts(userId) {
|
|
419
|
+
const rows = await this.accountLinks.listForUser(userId);
|
|
420
|
+
return rows.map((r) => ({
|
|
421
|
+
atlassianAccountId: r.atlassianAccountId,
|
|
422
|
+
createdAt: r.createdAt.getTime(),
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
async unlinkAccount(userId, accountId) {
|
|
426
|
+
return this.accountLinks.unlink(userId, accountId);
|
|
427
|
+
}
|
|
428
|
+
// ── internals ──────────────────────────────────────────────────────────────
|
|
429
|
+
/**
|
|
430
|
+
* Make sure `user` holds an EDIT lock on `wsPath` on the default branch:
|
|
431
|
+
* keep it when they already do, take it when nobody does (an expired row
|
|
432
|
+
* reads as none), refuse with the holder's name when somebody else does.
|
|
433
|
+
*/
|
|
434
|
+
async ensureHoldsLock(workspaceId, wsPath, user) {
|
|
435
|
+
const branch = this.kb.defaultBranch;
|
|
436
|
+
const current = await this.workflowService.getLock(workspaceId, branch, wsPath);
|
|
437
|
+
if (current) {
|
|
438
|
+
// A coordination hold grants no write authority, so it is not "ours"
|
|
439
|
+
// for a save even when the id matches.
|
|
440
|
+
if (current.holderUserId === user.id && current.mode !== 'coordination')
|
|
441
|
+
return;
|
|
442
|
+
throw new EmbedLockedError(current.holderName);
|
|
443
|
+
}
|
|
444
|
+
const taken = await this.workflowService.acquireLock(workspaceId, branch, wsPath, user);
|
|
445
|
+
if (!taken.acquired)
|
|
446
|
+
throw new EmbedLockedError(taken.lock.holderName);
|
|
447
|
+
}
|
|
448
|
+
defaultWorkspaceId() {
|
|
449
|
+
return workspaceIdForBranch(this.kb.defaultBranch);
|
|
450
|
+
}
|
|
451
|
+
wsPathFor(repoRelative) {
|
|
452
|
+
return `${this.config.kbDirName}/${repoRelative}`;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* A change request's address, through the same builder the workflow tools
|
|
456
|
+
* use — so a proposal made from a chat and one made by an agent point at
|
|
457
|
+
* the same place, path prefix of a proxied deployment included.
|
|
458
|
+
*/
|
|
459
|
+
changeRequestUrl(number) {
|
|
460
|
+
return changeRequestLink(number, changeRequestLinkBase(this.config.publicFrontendUrl)).url;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Resolve, authorize and locate an edit. Throws `EmbedAccessError` when the
|
|
464
|
+
* identity does not resolve or may not write. Returns the user (for lock
|
|
465
|
+
* attribution) and the workspace-relative path.
|
|
466
|
+
*/
|
|
467
|
+
async requireEditor(token) {
|
|
468
|
+
const claims = this.verifyToken(token);
|
|
469
|
+
const { user, canWrite } = await this.resolveIdentity(claims);
|
|
470
|
+
if (!user)
|
|
471
|
+
throw new EmbedAccessError('Link your account to edit');
|
|
472
|
+
if (!canWrite)
|
|
473
|
+
throw new EmbedAccessError();
|
|
474
|
+
await this.workspaceService.getOrCreateForBranch(this.kb.defaultBranch);
|
|
475
|
+
return { claims, user, wsPath: this.wsPathFor(claims.repoRelative) };
|
|
476
|
+
}
|
|
477
|
+
/** Resolve the token's identity to a user + read/write verdicts. */
|
|
478
|
+
async resolveIdentity(claims) {
|
|
479
|
+
const unresolved = { linked: false, user: null, canRead: false, canWrite: false };
|
|
480
|
+
const userId = claims.kind === 'user' ? claims.sub : await this.accountLinks.getUserId(claims.sub);
|
|
481
|
+
if (!userId)
|
|
482
|
+
return unresolved;
|
|
483
|
+
const user = await this.authService.getUserById(userId);
|
|
484
|
+
if (!user)
|
|
485
|
+
return unresolved;
|
|
486
|
+
// The gate every credential passes: a token minted for an account an
|
|
487
|
+
// admin has since switched off reads, saves and proposes nothing, and is
|
|
488
|
+
// told what every other door tells that person.
|
|
489
|
+
if (!(await this.authService.isActive(userId)))
|
|
490
|
+
throw new EmbedAccessError(ACCOUNT_DEACTIVATED_MESSAGE);
|
|
491
|
+
const workspaceId = this.defaultWorkspaceId();
|
|
492
|
+
const canRead = await this.accessControl.canRead(workspaceId, user.email, claims.repoRelative);
|
|
493
|
+
// Write implies read in the access resolver; the `canRead &&` guard pins
|
|
494
|
+
// that here, so no caller can see canWrite without canRead.
|
|
495
|
+
const canWrite = canRead && (await this.accessControl.canWrite(workspaceId, user.email, claims.repoRelative));
|
|
496
|
+
return { linked: true, user, canRead, canWrite };
|
|
497
|
+
}
|
|
498
|
+
/** Whether `repoRelative` is a file on the default branch — a stat, never a read. */
|
|
499
|
+
async fileExists(repoRelative) {
|
|
500
|
+
try {
|
|
501
|
+
await this.assertExists(repoRelative);
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
if (err instanceof EmbedNodeNotFoundError)
|
|
506
|
+
return false;
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/** The typed 404 of {@link readFileBytes}, for a reference, without reading a byte. */
|
|
511
|
+
async assertExists(repoRelative) {
|
|
512
|
+
const workspaceId = this.defaultWorkspaceId();
|
|
513
|
+
await this.workspaceService.getOrCreateForBranch(this.kb.defaultBranch);
|
|
514
|
+
if (!(await this.workspaceService.isFile(workspaceId, this.wsPathFor(repoRelative)))) {
|
|
515
|
+
throw new EmbedNodeNotFoundError(`${repoRelative} doesn't exist on ${this.kb.defaultBranch}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async readFileBytes(repoRelative) {
|
|
519
|
+
const workspaceId = this.defaultWorkspaceId();
|
|
520
|
+
await this.workspaceService.getOrCreateForBranch(this.kb.defaultBranch);
|
|
521
|
+
try {
|
|
522
|
+
const read = await this.workspaceService.readFileBinary(workspaceId, this.wsPathFor(repoRelative));
|
|
523
|
+
return Buffer.isBuffer(read) ? read : Buffer.from(read);
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
// A missing file is a bad or stale reference, not a server fault —
|
|
527
|
+
// surface it as a typed 404 so the view says what is wrong instead of
|
|
528
|
+
// showing a 500. Everything else (EACCES, EIO, …) stays unexpected.
|
|
529
|
+
const code = err?.code;
|
|
530
|
+
if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EISDIR') {
|
|
531
|
+
throw new EmbedNodeNotFoundError(`${repoRelative} doesn't exist on ${this.kb.defaultBranch}`);
|
|
532
|
+
}
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
verifyToken(token) {
|
|
537
|
+
let decoded;
|
|
538
|
+
try {
|
|
539
|
+
decoded = jwt.verify(token, this.signingKey);
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
throw new EmbedTokenError();
|
|
543
|
+
}
|
|
544
|
+
const c = decoded;
|
|
545
|
+
if (c?.scope !== 'embed' || typeof c.repoRelative !== 'string' || !c.repoRelative) {
|
|
546
|
+
throw new EmbedTokenError();
|
|
547
|
+
}
|
|
548
|
+
// A token minted by the release BEFORE the subject was generalised keyed
|
|
549
|
+
// the identity on `accountId` alone. Those were minted for two hours, so an upgrade
|
|
550
|
+
// would otherwise expire every open Atlassian panel on the spot; reading
|
|
551
|
+
// the old spelling costs one branch and keeps them working.
|
|
552
|
+
if (typeof c.accountId === 'string' && c.accountId) {
|
|
553
|
+
return { scope: 'embed', kind: 'atlassian', sub: c.accountId, repoRelative: c.repoRelative, slug: c.slug };
|
|
554
|
+
}
|
|
555
|
+
if ((c.kind !== 'user' && c.kind !== 'atlassian') || typeof c.sub !== 'string' || !c.sub) {
|
|
556
|
+
throw new EmbedTokenError();
|
|
557
|
+
}
|
|
558
|
+
return { scope: 'embed', kind: c.kind, sub: c.sub, repoRelative: c.repoRelative, slug: c.slug };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/** Display name = file basename without its extension. */
|
|
562
|
+
function nodeNameFor(repoRelative) {
|
|
563
|
+
const base = repoRelative.split('/').pop() ?? repoRelative;
|
|
564
|
+
const dot = base.lastIndexOf('.');
|
|
565
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* A path written relative to `from`'s folder, resolved to a repo-relative
|
|
569
|
+
* path — or null when it escapes the repository or is otherwise unsafe.
|
|
570
|
+
* A path with a leading slash (`/x`) is taken from the repository root:
|
|
571
|
+
* that is the form the embed view sends, translating the workspace path a
|
|
572
|
+
* renderer hands it.
|
|
573
|
+
*/
|
|
574
|
+
export function resolveBeside(from, path) {
|
|
575
|
+
// eslint-disable-next-line no-control-regex
|
|
576
|
+
if (/[\x00-\x1f\x7f]/.test(path))
|
|
577
|
+
return null;
|
|
578
|
+
let decoded = path;
|
|
579
|
+
try {
|
|
580
|
+
decoded = decodeURIComponent(path);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
// Judged again decoded: `%0a` is a newline once the escape is undone.
|
|
586
|
+
// eslint-disable-next-line no-control-regex
|
|
587
|
+
if (/[\x00-\x1f\x7f]/.test(decoded))
|
|
588
|
+
return null;
|
|
589
|
+
const absolute = decoded.startsWith('/');
|
|
590
|
+
const base = absolute ? [] : from.split('/').slice(0, -1);
|
|
591
|
+
const parts = [...base];
|
|
592
|
+
for (const seg of decoded.replace(/^\/+/, '').split('/')) {
|
|
593
|
+
if (seg === '' || seg === '.')
|
|
594
|
+
continue;
|
|
595
|
+
if (seg === '..') {
|
|
596
|
+
// Out of the repository — never a path this view may serve.
|
|
597
|
+
if (parts.length === 0)
|
|
598
|
+
return null;
|
|
599
|
+
parts.pop();
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
parts.push(seg);
|
|
603
|
+
}
|
|
604
|
+
return parts.length > 0 ? parts.join('/') : null;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* The number of the open request the server says already covers this branch
|
|
608
|
+
* pair, or null when the failure was something else. The refusal carries the
|
|
609
|
+
* number precisely so a caller can point at the request instead of treating
|
|
610
|
+
* the state as broken.
|
|
611
|
+
*/
|
|
612
|
+
function duplicateRequestNumber(err) {
|
|
613
|
+
const detail = err ?? null;
|
|
614
|
+
const candidate = detail?.details?.existingNumber ?? detail?.existingNumber;
|
|
615
|
+
if (typeof candidate === 'number')
|
|
616
|
+
return candidate;
|
|
617
|
+
if (err instanceof Error) {
|
|
618
|
+
// The refusal's own sentence ("An open change request already exists
|
|
619
|
+
// from … to … (#12)"), and only that shape: a count or a numeric token
|
|
620
|
+
// after some other "already" is not a request.
|
|
621
|
+
const match = /change request already exists[^#]*#(\d+)/i.exec(err.message);
|
|
622
|
+
if (match)
|
|
623
|
+
return Number(match[1]);
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
//# sourceMappingURL=embed.service.js.map
|