@bevel-software/platform-core-backend 0.14.0 → 0.15.1
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/THIRD-PARTY-NOTICES.md +6 -6
- package/dist/core/catalog-cache-invalidation.d.ts +29 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +71 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +99 -8
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +18 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +96 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +18 -6
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +26 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +213 -71
- 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 +59 -7
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +51 -1
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +95 -1
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +49 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +125 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +326 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +52 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +183 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +190 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +102 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +10 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +8 -0
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +66 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +160 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +304 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +152 -52
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +245 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +27 -1
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +7 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +120 -12
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +50 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +108 -47
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +82 -2
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +26 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +9 -1
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +1 -0
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +9 -4
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +168 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +31 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +28 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +42 -3
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +4 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +83 -22
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +26 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +40 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +227 -54
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +8 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +52 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +242 -9
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.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 +46 -2
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +9 -9
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +33 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +110 -16
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +20 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +29 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +95 -30
- package/kb-template/Skills/.gitkeep +0 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/__tests__/kb-layout-config.test.ts +110 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
- package/src/core/catalog-cache-invalidation.ts +92 -0
- package/src/core/create-core-server.ts +123 -7
- package/src/core/create-core-services.ts +162 -41
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
- package/src/modules/access/access-control.interface.ts +20 -9
- package/src/modules/access/access-control.service.ts +240 -78
- package/src/modules/access/access.routes.ts +96 -14
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/access-grammar.ts +114 -2
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
- package/src/modules/kb-sync/kb-sync.service.ts +201 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
- package/src/modules/marketplace/git-http.routes.ts +193 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
- package/src/modules/marketplace/github-facade/index.ts +25 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
- package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
- package/src/modules/plugins/discovery/native.source.ts +122 -0
- package/src/modules/plugins/discovery/plugin-source.ts +99 -0
- package/src/modules/plugins/index.ts +10 -0
- package/src/modules/plugins/plugin-links.service.ts +363 -0
- package/src/modules/plugins/plugin-links.ts +184 -0
- package/src/modules/plugins/plugin-provision.service.ts +160 -54
- package/src/modules/plugins/plugin-rename.service.ts +298 -0
- package/src/modules/plugins/plugins.contract.ts +28 -1
- package/src/modules/plugins/plugins.routes.ts +128 -11
- package/src/modules/plugins/plugins.service.ts +106 -53
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/deployment-settings.service.ts +95 -2
- package/src/modules/settings/setup.routes.ts +24 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
- package/src/modules/skills/index.ts +1 -0
- package/src/modules/skills/pending-skills.service.ts +7 -5
- package/src/modules/skills/skill-access-requests.routes.ts +199 -0
- package/src/modules/skills/skills.contract.ts +32 -0
- package/src/modules/skills/skills.routes.ts +59 -3
- package/src/modules/skills/skills.service.ts +84 -22
- package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
- package/src/modules/tool-auth/external-api-key.service.ts +31 -13
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
- package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/git.service.ts +239 -56
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.service.ts +260 -9
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
- package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
- package/src/modules/workspace/workspace.routes.ts +9 -17
- package/src/modules/workspace/workspace.service.ts +107 -16
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/fs-walk.test.ts +22 -1
- package/src/shared/__tests__/kb-walk.test.ts +107 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/domain-errors.ts +29 -0
- package/src/shared/fs-errors.ts +10 -0
- package/src/shared/fs-walk.ts +38 -23
- package/src/shared/kb-walk.ts +95 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
4
|
+
import type { MarketplaceRepoService } from './marketplace-repo.service.js';
|
|
5
|
+
import '../tool-auth/external-api-key.interface.js'; // Express Request augmentation (req.externalApiKeyId)
|
|
6
|
+
|
|
7
|
+
/** How a bearer string resolves to a user — the connection-key service's shape. */
|
|
8
|
+
export interface MarketplaceKeyResolver {
|
|
9
|
+
looksLikeExternalApiKey(token: string): boolean;
|
|
10
|
+
verifyAndLoadToken(plaintext: string): Promise<{ tokenId: string; user: AuthUser } | null>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The per-user marketplace as a git remote:
|
|
15
|
+
*
|
|
16
|
+
* git clone https://key:<connection-key>@<host>/git/marketplace.git
|
|
17
|
+
*
|
|
18
|
+
* Anything that speaks git's smart HTTP protocol — Claude Code, Codex, `npx
|
|
19
|
+
* skills`, plain git — clones a repository whose `main` is the caller's
|
|
20
|
+
* compiled tree. The route authenticates, brings the caller's namespace up to
|
|
21
|
+
* date (`MarketplaceRepoService.ensureCompiled`), and hands the request to
|
|
22
|
+
* `git http-backend` with `GIT_NAMESPACE` set, so git itself serves the
|
|
23
|
+
* protocol and only that namespace's refs exist as far as the client can tell.
|
|
24
|
+
*
|
|
25
|
+
* AUTH: HTTP Basic with the connection key as the password (the username is
|
|
26
|
+
* anything — git sends whatever the URL carried), or the key as a Bearer
|
|
27
|
+
* token. The same keys the MCP endpoint takes, the same service resolving
|
|
28
|
+
* them, and the same metering stamp (`req.externalApiKeyId`), so a key's
|
|
29
|
+
* `lastUsedAt` moves when an agent refreshes its plugins. A 401 challenges
|
|
30
|
+
* with Basic so a client without credentials prompts for them.
|
|
31
|
+
*
|
|
32
|
+
* READ-ONLY: the receive-pack service is refused here by name, and the bare
|
|
33
|
+
* repository pins `http.receivepack=false` for good measure — a push would
|
|
34
|
+
* otherwise be enabled by http-backend the moment REMOTE_USER is set.
|
|
35
|
+
*/
|
|
36
|
+
export function createMarketplaceGitRoutes(deps: {
|
|
37
|
+
repo: MarketplaceRepoService;
|
|
38
|
+
keys: MarketplaceKeyResolver;
|
|
39
|
+
/** The URL path the router is mounted at, e.g. `/git`. Used for PATH_INFO. */
|
|
40
|
+
mountPath: string;
|
|
41
|
+
}): express.Router {
|
|
42
|
+
const { repo, keys, mountPath } = deps;
|
|
43
|
+
const router = express.Router();
|
|
44
|
+
const repoRoute = `/${repo.repoName}`;
|
|
45
|
+
|
|
46
|
+
const challenge = (res: express.Response, message: string) => {
|
|
47
|
+
res.setHeader('WWW-Authenticate', 'Basic realm="hexis-marketplace"');
|
|
48
|
+
res.status(401).type('text/plain').send(message);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The connection key out of Basic (`user:key`, `key:` or `:key`) or Bearer. */
|
|
52
|
+
const keyOf = (header: string | undefined): string | null => {
|
|
53
|
+
if (!header) return null;
|
|
54
|
+
const space = header.indexOf(' ');
|
|
55
|
+
if (space < 0) return null;
|
|
56
|
+
const scheme = header.slice(0, space).toLowerCase();
|
|
57
|
+
const value = header.slice(space + 1).trim();
|
|
58
|
+
if (scheme === 'bearer') return value || null;
|
|
59
|
+
if (scheme !== 'basic') return null;
|
|
60
|
+
let decoded: string;
|
|
61
|
+
try {
|
|
62
|
+
decoded = Buffer.from(value, 'base64').toString('utf-8');
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const colon = decoded.indexOf(':');
|
|
67
|
+
const user = colon < 0 ? decoded : decoded.slice(0, colon);
|
|
68
|
+
const pass = colon < 0 ? '' : decoded.slice(colon + 1);
|
|
69
|
+
// Either half may carry the key: `https://key:<k>@` puts it in the
|
|
70
|
+
// password, `https://<k>@` in the username.
|
|
71
|
+
if (pass && keys.looksLikeExternalApiKey(pass)) return pass;
|
|
72
|
+
if (user && keys.looksLikeExternalApiKey(user)) return user;
|
|
73
|
+
return pass || user || null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
router.all(`${repoRoute}{/*rest}`, async (req, res) => {
|
|
77
|
+
const service = String(req.query.service ?? '');
|
|
78
|
+
const rest = `/${([] as string[]).concat((req.params as { rest?: string | string[] }).rest ?? []).join('/')}`;
|
|
79
|
+
// Refuse writes before touching anything — by service name on info/refs,
|
|
80
|
+
// and by endpoint on the POST.
|
|
81
|
+
if (service === 'git-receive-pack' || rest === '/git-receive-pack') {
|
|
82
|
+
res.status(403).type('text/plain').send('This repository is read-only.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const key = keyOf(req.headers.authorization);
|
|
87
|
+
if (!key || !keys.looksLikeExternalApiKey(key)) {
|
|
88
|
+
challenge(res, 'A connection key is required: https://key:<connection-key>@<host>/git/marketplace.git');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let resolved: { tokenId: string; user: AuthUser } | null;
|
|
92
|
+
try {
|
|
93
|
+
resolved = await keys.verifyAndLoadToken(key);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('[marketplace] connection-key verification failed:', err);
|
|
96
|
+
res.status(500).type('text/plain').send('Authentication backend unavailable');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!resolved) {
|
|
100
|
+
challenge(res, 'Invalid or revoked connection key');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
req.userId = resolved.user.id;
|
|
104
|
+
req.userEmail = resolved.user.email;
|
|
105
|
+
req.externalApiKeyId = resolved.tokenId;
|
|
106
|
+
|
|
107
|
+
let ensured: { namespace: string };
|
|
108
|
+
try {
|
|
109
|
+
ensured = await repo.ensureCompiled({ id: resolved.user.id, email: resolved.user.email });
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error('[marketplace] compile failed:', err);
|
|
112
|
+
res.status(500).type('text/plain').send('Could not prepare your marketplace');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// CGI, as git documents it. PATH_INFO is the path below GIT_PROJECT_ROOT
|
|
117
|
+
// — `/marketplace.git/info/refs` — regardless of where we are mounted.
|
|
118
|
+
const query = req.url.includes('?') ? req.url.slice(req.url.indexOf('?') + 1) : '';
|
|
119
|
+
const env: NodeJS.ProcessEnv = {
|
|
120
|
+
...process.env,
|
|
121
|
+
GIT_PROJECT_ROOT: repo.projectRoot,
|
|
122
|
+
GIT_HTTP_EXPORT_ALL: '1',
|
|
123
|
+
GIT_NAMESPACE: ensured.namespace,
|
|
124
|
+
PATH_INFO: `${repoRoute}${rest}`,
|
|
125
|
+
REQUEST_METHOD: req.method,
|
|
126
|
+
QUERY_STRING: query,
|
|
127
|
+
CONTENT_TYPE: req.headers['content-type'] ?? '',
|
|
128
|
+
REMOTE_USER: resolved.user.email,
|
|
129
|
+
REMOTE_ADDR: req.ip ?? '',
|
|
130
|
+
SERVER_PROTOCOL: 'HTTP/1.1',
|
|
131
|
+
HTTP_HOST: req.headers.host ?? '',
|
|
132
|
+
SCRIPT_NAME: mountPath,
|
|
133
|
+
};
|
|
134
|
+
if (req.headers['content-length']) env.CONTENT_LENGTH = String(req.headers['content-length']);
|
|
135
|
+
if (req.headers['content-encoding']) env.HTTP_CONTENT_ENCODING = String(req.headers['content-encoding']);
|
|
136
|
+
if (req.headers['git-protocol']) env.HTTP_GIT_PROTOCOL = String(req.headers['git-protocol']);
|
|
137
|
+
|
|
138
|
+
const child = spawn('git', ['http-backend'], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
139
|
+
child.stderr.on('data', (chunk: Buffer) => console.warn(`[marketplace] http-backend: ${chunk.toString('utf-8').trim()}`));
|
|
140
|
+
child.on('error', (err) => {
|
|
141
|
+
console.error('[marketplace] could not run git http-backend:', err);
|
|
142
|
+
if (!res.headersSent) res.status(500).type('text/plain').send('git is unavailable');
|
|
143
|
+
});
|
|
144
|
+
req.pipe(child.stdin);
|
|
145
|
+
req.on('aborted', () => child.kill());
|
|
146
|
+
|
|
147
|
+
// Parse the CGI header block (`Status:`, `Content-Type:`, …) off stdout,
|
|
148
|
+
// then stream the body straight through.
|
|
149
|
+
let headerBuf = Buffer.alloc(0);
|
|
150
|
+
let headersDone = false;
|
|
151
|
+
// Backpressure: a slow client must not make Node buffer a whole pack. One
|
|
152
|
+
// path for every body write, the remainder of the header chunk included.
|
|
153
|
+
const writeBody = (bytes: Buffer) => {
|
|
154
|
+
if (!res.write(bytes)) {
|
|
155
|
+
child.stdout.pause();
|
|
156
|
+
res.once('drain', () => child.stdout.resume());
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
160
|
+
if (headersDone) {
|
|
161
|
+
writeBody(chunk);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
headerBuf = Buffer.concat([headerBuf, chunk]);
|
|
165
|
+
const cut = headerBuf.indexOf('\r\n\r\n') >= 0 ? headerBuf.indexOf('\r\n\r\n') : headerBuf.indexOf('\n\n');
|
|
166
|
+
if (cut < 0) return;
|
|
167
|
+
const sepLen = headerBuf.indexOf('\r\n\r\n') >= 0 ? 4 : 2;
|
|
168
|
+
const head = headerBuf.subarray(0, cut).toString('utf-8');
|
|
169
|
+
let status = 200;
|
|
170
|
+
for (const line of head.split(/\r?\n/)) {
|
|
171
|
+
const colon = line.indexOf(':');
|
|
172
|
+
if (colon < 0) continue;
|
|
173
|
+
const name = line.slice(0, colon).trim();
|
|
174
|
+
const value = line.slice(colon + 1).trim();
|
|
175
|
+
if (name.toLowerCase() === 'status') status = Number.parseInt(value, 10) || 200;
|
|
176
|
+
else res.setHeader(name, value);
|
|
177
|
+
}
|
|
178
|
+
res.status(status);
|
|
179
|
+
headersDone = true;
|
|
180
|
+
const body = headerBuf.subarray(cut + sepLen);
|
|
181
|
+
if (body.length > 0) writeBody(body);
|
|
182
|
+
});
|
|
183
|
+
child.stdout.on('end', () => {
|
|
184
|
+
if (!headersDone) {
|
|
185
|
+
if (!res.headersSent) res.status(502).type('text/plain').send('git http-backend produced no response');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
res.end();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return router;
|
|
193
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import '../../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
|
|
3
|
+
import {
|
|
4
|
+
GitHubFacadeUnavailableError,
|
|
5
|
+
type GitHubFacadeCredentialsService,
|
|
6
|
+
} from './github-facade-credentials.service.js';
|
|
7
|
+
|
|
8
|
+
export interface GitHubFacadeAdminRoutesDeps {
|
|
9
|
+
credentials: GitHubFacadeCredentialsService;
|
|
10
|
+
isAdmin(email: string | undefined): Promise<boolean>;
|
|
11
|
+
/** The deployment's public address; only its host is shown. */
|
|
12
|
+
publicUrl: string;
|
|
13
|
+
/** The marketplace URL people paste — the same one Claude Code clones. */
|
|
14
|
+
marketplaceUrl: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What an Owner needs to register this deployment in Claude's admin settings
|
|
19
|
+
* as a GitHub Enterprise Server, in the fields that form has — and the one
|
|
20
|
+
* verb on it, rotate. Admins only: the client secret is what makes claude.ai
|
|
21
|
+
* trust the token exchange. Never cached: after a rotation or a sign-out no
|
|
22
|
+
* copy of the old secrets may linger in a browser or a proxy.
|
|
23
|
+
*
|
|
24
|
+
* GET /api/admin/github-facade the credentials and the two URLs
|
|
25
|
+
* POST /api/admin/github-facade/rotate new credentials, all of them
|
|
26
|
+
*/
|
|
27
|
+
export function createGitHubFacadeAdminRoutes(deps: GitHubFacadeAdminRoutesDeps): express.Router {
|
|
28
|
+
const router = express.Router();
|
|
29
|
+
|
|
30
|
+
const requireAdmin: express.RequestHandler = async (req, res, next) => {
|
|
31
|
+
if (!(await deps.isAdmin(req.userEmail))) {
|
|
32
|
+
res.status(403).json({ error: 'Admins only' });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
next();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const describe = async () => {
|
|
39
|
+
const creds = await deps.credentials.ensure();
|
|
40
|
+
return {
|
|
41
|
+
host: new URL(deps.publicUrl).host,
|
|
42
|
+
appId: creds.appId,
|
|
43
|
+
clientId: creds.clientId,
|
|
44
|
+
clientSecret: creds.clientSecret,
|
|
45
|
+
webhookSecret: creds.webhookSecret,
|
|
46
|
+
privateKeyPem: creds.privateKeyPem,
|
|
47
|
+
marketplaceUrl: deps.marketplaceUrl,
|
|
48
|
+
createdAt: creds.createdAt.getTime(),
|
|
49
|
+
rotatedAt: creds.rotatedAt ? creds.rotatedAt.getTime() : null,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const send = async (res: express.Response, body: () => Promise<unknown>) => {
|
|
54
|
+
try {
|
|
55
|
+
const payload = await body();
|
|
56
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
57
|
+
res.json(payload);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err instanceof GitHubFacadeUnavailableError) {
|
|
60
|
+
res.status(409).json({ error: err.message });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.error('[github-facade admin]', err);
|
|
64
|
+
res.status(500).json({ error: 'Internal error' });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
router.get('/admin/github-facade', requireAdmin, (_req, res) => send(res, describe));
|
|
69
|
+
|
|
70
|
+
router.post('/admin/github-facade/rotate', requireAdmin, (_req, res) =>
|
|
71
|
+
send(res, async () => {
|
|
72
|
+
await deps.credentials.rotate();
|
|
73
|
+
return describe();
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return router;
|
|
78
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { and, eq, gt, isNull } from 'drizzle-orm';
|
|
2
|
+
import type { Database } from '../../database/connection.js';
|
|
3
|
+
import { githubFacadeCodes } from '../../database/schema.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A one-time code the consent page issued, waiting for Anthropic's backend
|
|
7
|
+
* to exchange it. Looked up by the code's hash; the plaintext travels once,
|
|
8
|
+
* in the redirect.
|
|
9
|
+
*/
|
|
10
|
+
export interface PendingFacadeCode {
|
|
11
|
+
codeHash: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
redirectUri: string;
|
|
15
|
+
expiresAt: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Where pending codes live. The database in production, so the consent
|
|
20
|
+
* finish on one replica and the exchange on another see the same row.
|
|
21
|
+
*
|
|
22
|
+
* The rule "one live code per person" is the TABLE'S: the person is its
|
|
23
|
+
* primary key, so issuing is an upsert — the newest code overwrites the last
|
|
24
|
+
* in one statement, two finishes racing each other cannot both leave a row
|
|
25
|
+
* behind, and the table holds at most one row per user, which goes when the
|
|
26
|
+
* user does. That is the whole bound; nothing here ever sweeps, and no
|
|
27
|
+
* rotation of the client id can strand a row, because the client id is data
|
|
28
|
+
* the exchange checks rather than part of the key. Spending is one
|
|
29
|
+
* conditional update: the first exchange wins, on whichever replica, and
|
|
30
|
+
* every later attempt finds the code spent. Nothing in memory takes part in
|
|
31
|
+
* either guarantee.
|
|
32
|
+
*/
|
|
33
|
+
export interface GitHubFacadeCodeStore {
|
|
34
|
+
/** Store a fresh code, replacing whatever the same person had. */
|
|
35
|
+
put(code: PendingFacadeCode): Promise<void>;
|
|
36
|
+
/** The live (unspent, unexpired) code with this hash, without spending it. */
|
|
37
|
+
peek(codeHash: string): Promise<PendingFacadeCode | null>;
|
|
38
|
+
/** Spend the code if it is live; null when it was not (unknown, spent, expired). */
|
|
39
|
+
consume(codeHash: string): Promise<PendingFacadeCode | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class DbGitHubFacadeCodeStore implements GitHubFacadeCodeStore {
|
|
43
|
+
constructor(private readonly db: Database) {}
|
|
44
|
+
|
|
45
|
+
async put(code: PendingFacadeCode): Promise<void> {
|
|
46
|
+
const fresh = {
|
|
47
|
+
clientId: code.clientId,
|
|
48
|
+
codeHash: code.codeHash,
|
|
49
|
+
redirectUri: code.redirectUri,
|
|
50
|
+
expiresAt: code.expiresAt,
|
|
51
|
+
consumedAt: null,
|
|
52
|
+
createdAt: new Date(),
|
|
53
|
+
};
|
|
54
|
+
await this.db
|
|
55
|
+
.insert(githubFacadeCodes)
|
|
56
|
+
.values({ userId: code.userId, ...fresh })
|
|
57
|
+
.onConflictDoUpdate({ target: githubFacadeCodes.userId, set: fresh });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async peek(codeHash: string): Promise<PendingFacadeCode | null> {
|
|
61
|
+
const [row] = await this.db
|
|
62
|
+
.select()
|
|
63
|
+
.from(githubFacadeCodes)
|
|
64
|
+
.where(live(codeHash))
|
|
65
|
+
.limit(1);
|
|
66
|
+
return row ? toPending(row) : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async consume(codeHash: string): Promise<PendingFacadeCode | null> {
|
|
70
|
+
const [row] = await this.db
|
|
71
|
+
.update(githubFacadeCodes)
|
|
72
|
+
.set({ consumedAt: new Date() })
|
|
73
|
+
.where(live(codeHash))
|
|
74
|
+
.returning();
|
|
75
|
+
return row ? toPending(row) : null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The one definition of "live": this hash, unspent, unexpired. */
|
|
80
|
+
function live(codeHash: string) {
|
|
81
|
+
return and(
|
|
82
|
+
eq(githubFacadeCodes.codeHash, codeHash),
|
|
83
|
+
isNull(githubFacadeCodes.consumedAt),
|
|
84
|
+
gt(githubFacadeCodes.expiresAt, new Date()),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toPending(row: typeof githubFacadeCodes.$inferSelect): PendingFacadeCode {
|
|
89
|
+
return {
|
|
90
|
+
codeHash: row.codeHash,
|
|
91
|
+
userId: row.userId,
|
|
92
|
+
clientId: row.clientId,
|
|
93
|
+
redirectUri: row.redirectUri,
|
|
94
|
+
expiresAt: row.expiresAt,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* For tests: the same rules over a map keyed the way the table is. Share one
|
|
100
|
+
* instance to model shared storage. Check-and-mark is synchronous, as the
|
|
101
|
+
* database's conditional update is atomic: two exchanges of one code cannot
|
|
102
|
+
* both win here either.
|
|
103
|
+
*/
|
|
104
|
+
export class MemoryGitHubFacadeCodeStore implements GitHubFacadeCodeStore {
|
|
105
|
+
private readonly rows = new Map<string, PendingFacadeCode & { consumedAt: Date | null }>();
|
|
106
|
+
|
|
107
|
+
async put(code: PendingFacadeCode): Promise<void> {
|
|
108
|
+
this.rows.set(code.userId, { ...code, consumedAt: null });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async peek(codeHash: string): Promise<PendingFacadeCode | null> {
|
|
112
|
+
const row = this.findLive(codeHash);
|
|
113
|
+
return row ? strip(row) : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async consume(codeHash: string): Promise<PendingFacadeCode | null> {
|
|
117
|
+
const row = this.findLive(codeHash);
|
|
118
|
+
if (!row) return null;
|
|
119
|
+
row.consumedAt = new Date();
|
|
120
|
+
return strip(row);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** How many rows the store holds — at most one per person. */
|
|
124
|
+
get size(): number {
|
|
125
|
+
return this.rows.size;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private findLive(codeHash: string) {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const row of this.rows.values()) {
|
|
131
|
+
if (row.codeHash === codeHash && row.consumedAt === null && row.expiresAt.getTime() > now) return row;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function strip(row: PendingFacadeCode & { consumedAt: Date | null }): PendingFacadeCode {
|
|
138
|
+
return {
|
|
139
|
+
codeHash: row.codeHash,
|
|
140
|
+
userId: row.userId,
|
|
141
|
+
clientId: row.clientId,
|
|
142
|
+
redirectUri: row.redirectUri,
|
|
143
|
+
expiresAt: row.expiresAt,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { generateKeyPairSync, randomBytes, randomInt } from 'node:crypto';
|
|
2
|
+
import { and, eq } from 'drizzle-orm';
|
|
3
|
+
import type { Database } from '../../database/connection.js';
|
|
4
|
+
import { githubFacadeIdentity } from '../../database/schema.js';
|
|
5
|
+
import type { TokenCrypto } from '../../../shared/token-crypto.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What Claude's "Add manually" form for a GitHub Enterprise Server asks an
|
|
9
|
+
* Owner to paste, and what the token exchange later checks: the identity
|
|
10
|
+
* this deployment presents to claude.ai as if it were a GitHub App.
|
|
11
|
+
*
|
|
12
|
+
* Every field is generated HERE, once per deployment — nothing is registered
|
|
13
|
+
* anywhere else, because there is no GitHub: hexis is the host Claude talks
|
|
14
|
+
* to. The private key is handed over because the form requires one; the
|
|
15
|
+
* user-added marketplace flow never signs with it (the observed contract is
|
|
16
|
+
* the OAuth pair plus three REST calls), but a future organization
|
|
17
|
+
* marketplace would, and rotating everything together is simpler than
|
|
18
|
+
* explaining which half matters.
|
|
19
|
+
*/
|
|
20
|
+
export interface GitHubFacadeCredentials {
|
|
21
|
+
appId: string;
|
|
22
|
+
clientId: string;
|
|
23
|
+
clientSecret: string;
|
|
24
|
+
webhookSecret: string;
|
|
25
|
+
privateKeyPem: string;
|
|
26
|
+
publicKeyPem: string;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
rotatedAt: Date | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Where the one credentials row lives — the database in production, memory
|
|
33
|
+
* in tests. THE row, not a copy of it: every replica reads the store on
|
|
34
|
+
* every use, so a rotation on one replica is what every other replica
|
|
35
|
+
* checks the very next exchange. The reads are rare (a connect, an admin
|
|
36
|
+
* page), and a cache would be a second source of truth with no invalidation.
|
|
37
|
+
*
|
|
38
|
+
* Both writes are conditional, so two replicas writing at once cannot
|
|
39
|
+
* disagree about what is stored: creation happens only when no row exists,
|
|
40
|
+
* and a rotation replaces only the set the caller was looking at. Either way
|
|
41
|
+
* the caller gets back what the store holds afterwards, never what it sent.
|
|
42
|
+
*/
|
|
43
|
+
export interface GitHubFacadeCredentialsStore {
|
|
44
|
+
load(): Promise<GitHubFacadeCredentials | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Store `creds` only if no row exists yet, and return whichever row
|
|
47
|
+
* exists afterwards — so two replicas initialising at once agree on one
|
|
48
|
+
* set, and the loser's generated values are simply dropped.
|
|
49
|
+
*/
|
|
50
|
+
createIfAbsent(creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials>;
|
|
51
|
+
/**
|
|
52
|
+
* Replace the row only while it still carries `expectedClientId`, and
|
|
53
|
+
* return what is stored afterwards. Two admins rotating at once: the first
|
|
54
|
+
* write wins, the second finds a different client id, writes nothing, and
|
|
55
|
+
* is shown the winner's set — the only one Claude will accept.
|
|
56
|
+
*/
|
|
57
|
+
replaceIfCurrent(expectedClientId: string, creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ROW_ID = 'default';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The database-backed store. Secrets are sealed with the deployment's secrets
|
|
64
|
+
* key, exactly as stored settings are; without that key there is nowhere safe
|
|
65
|
+
* to keep a client secret, so the store refuses rather than writing plaintext.
|
|
66
|
+
*/
|
|
67
|
+
export class DbGitHubFacadeCredentialsStore implements GitHubFacadeCredentialsStore {
|
|
68
|
+
constructor(
|
|
69
|
+
private readonly db: Database,
|
|
70
|
+
private readonly crypto: TokenCrypto | null,
|
|
71
|
+
) {}
|
|
72
|
+
|
|
73
|
+
async load(): Promise<GitHubFacadeCredentials | null> {
|
|
74
|
+
const [row] = await this.db
|
|
75
|
+
.select()
|
|
76
|
+
.from(githubFacadeIdentity)
|
|
77
|
+
.where(eq(githubFacadeIdentity.id, ROW_ID))
|
|
78
|
+
.limit(1);
|
|
79
|
+
if (!row) return null;
|
|
80
|
+
const crypto = this.requireCrypto();
|
|
81
|
+
return {
|
|
82
|
+
appId: row.appId,
|
|
83
|
+
clientId: row.clientId,
|
|
84
|
+
clientSecret: crypto.decrypt(row.clientSecret),
|
|
85
|
+
webhookSecret: crypto.decrypt(row.webhookSecret),
|
|
86
|
+
privateKeyPem: crypto.decrypt(row.privateKeyPem),
|
|
87
|
+
publicKeyPem: row.publicKeyPem,
|
|
88
|
+
createdAt: row.createdAt,
|
|
89
|
+
rotatedAt: row.rotatedAt,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async createIfAbsent(creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
|
|
94
|
+
await this.db
|
|
95
|
+
.insert(githubFacadeIdentity)
|
|
96
|
+
.values({ id: ROW_ID, ...this.seal(creds) })
|
|
97
|
+
.onConflictDoNothing({ target: githubFacadeIdentity.id });
|
|
98
|
+
return this.stored('insert');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async replaceIfCurrent(expectedClientId: string, creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
|
|
102
|
+
await this.db
|
|
103
|
+
.update(githubFacadeIdentity)
|
|
104
|
+
.set(this.seal(creds))
|
|
105
|
+
.where(and(eq(githubFacadeIdentity.id, ROW_ID), eq(githubFacadeIdentity.clientId, expectedClientId)));
|
|
106
|
+
return this.stored('replace');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async stored(after: string): Promise<GitHubFacadeCredentials> {
|
|
110
|
+
const row = await this.load();
|
|
111
|
+
if (!row) throw new Error(`claude bridge credentials vanished between ${after} and read`);
|
|
112
|
+
return row;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private seal(creds: GitHubFacadeCredentials) {
|
|
116
|
+
const crypto = this.requireCrypto();
|
|
117
|
+
return {
|
|
118
|
+
appId: creds.appId,
|
|
119
|
+
clientId: creds.clientId,
|
|
120
|
+
clientSecret: crypto.encrypt(creds.clientSecret),
|
|
121
|
+
webhookSecret: crypto.encrypt(creds.webhookSecret),
|
|
122
|
+
privateKeyPem: crypto.encrypt(creds.privateKeyPem),
|
|
123
|
+
publicKeyPem: creds.publicKeyPem,
|
|
124
|
+
createdAt: creds.createdAt,
|
|
125
|
+
rotatedAt: creds.rotatedAt,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private requireCrypto(): TokenCrypto {
|
|
130
|
+
if (!this.crypto) {
|
|
131
|
+
throw new GitHubFacadeUnavailableError(
|
|
132
|
+
'SECRETS_ENC_KEY is not set, so the Claude connection credentials cannot be stored.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return this.crypto;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class GitHubFacadeUnavailableError extends Error {
|
|
140
|
+
constructor(message: string) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.name = 'GitHubFacadeUnavailableError';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generates the credentials on first use and reads them from the store on
|
|
148
|
+
* every use after that. No in-process copy: the store is the one place the
|
|
149
|
+
* truth lives, whichever replica asks.
|
|
150
|
+
*/
|
|
151
|
+
export class GitHubFacadeCredentialsService {
|
|
152
|
+
constructor(private readonly store: GitHubFacadeCredentialsStore) {}
|
|
153
|
+
|
|
154
|
+
/** The credentials, generated on first use. */
|
|
155
|
+
async ensure(): Promise<GitHubFacadeCredentials> {
|
|
156
|
+
const existing = await this.store.load();
|
|
157
|
+
if (existing) return existing;
|
|
158
|
+
return this.store.createIfAbsent(generateCredentials(null));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* New credentials, all of them — replacing the set this call read, and
|
|
163
|
+
* returning whatever is stored afterwards. Every registration on the
|
|
164
|
+
* Claude side stops matching at once, which is the point of rotating: the
|
|
165
|
+
* Owner re-enters the new set, and connected users' tokens (connection
|
|
166
|
+
* keys) are untouched, since those are ours.
|
|
167
|
+
*/
|
|
168
|
+
async rotate(): Promise<GitHubFacadeCredentials> {
|
|
169
|
+
const current = await this.ensure();
|
|
170
|
+
return this.store.replaceIfCurrent(current.clientId, generateCredentials(current.createdAt));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function generateCredentials(createdAt: Date | null): GitHubFacadeCredentials {
|
|
175
|
+
// The shapes GitHub uses, because the form was built for them: a numeric
|
|
176
|
+
// app id, an `Iv1.`-prefixed client id, hex secrets, a PKCS#1 RSA key.
|
|
177
|
+
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
|
|
178
|
+
modulusLength: 2048,
|
|
179
|
+
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
|
180
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
appId: String(randomInt(100_000, 999_999)),
|
|
184
|
+
clientId: `Iv1.${randomBytes(8).toString('hex')}`,
|
|
185
|
+
clientSecret: randomBytes(20).toString('hex'),
|
|
186
|
+
webhookSecret: randomBytes(16).toString('hex'),
|
|
187
|
+
privateKeyPem: privateKey,
|
|
188
|
+
publicKeyPem: publicKey,
|
|
189
|
+
createdAt: createdAt ?? new Date(),
|
|
190
|
+
rotatedAt: createdAt ? new Date() : null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** For tests: the row kept in memory, with the same conditional writes. */
|
|
195
|
+
export class MemoryGitHubFacadeCredentialsStore implements GitHubFacadeCredentialsStore {
|
|
196
|
+
private row: GitHubFacadeCredentials | null = null;
|
|
197
|
+
async load(): Promise<GitHubFacadeCredentials | null> {
|
|
198
|
+
return this.row;
|
|
199
|
+
}
|
|
200
|
+
async createIfAbsent(creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
|
|
201
|
+
this.row ??= creds;
|
|
202
|
+
return this.row;
|
|
203
|
+
}
|
|
204
|
+
async replaceIfCurrent(expectedClientId: string, creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
|
|
205
|
+
if (this.row?.clientId === expectedClientId) this.row = creds;
|
|
206
|
+
return this.row!;
|
|
207
|
+
}
|
|
208
|
+
}
|