@bevel-software/platform-core-backend 0.13.6 → 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 +101 -9
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +20 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +105 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +12 -5
- package/dist/core-config.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/kb-read-filter.d.ts +12 -0
- package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
- package/dist/modules/access-model/kb-read-filter.js +15 -0
- package/dist/modules/access-model/kb-read-filter.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/connection-probe/connection-probe.contract.d.ts +43 -0
- package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
- package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.js +684 -0
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
- package/dist/modules/connection-probe/index.d.ts +3 -0
- package/dist/modules/connection-probe/index.d.ts.map +1 -0
- package/dist/modules/connection-probe/index.js +3 -0
- package/dist/modules/connection-probe/index.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/diff/diff.routes.d.ts.map +1 -1
- package/dist/modules/diff/diff.routes.js +3 -5
- package/dist/modules/diff/diff.routes.js.map +1 -1
- package/dist/modules/kb-fs/mutex.d.ts +37 -0
- package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
- package/dist/modules/kb-fs/mutex.js +48 -5
- package/dist/modules/kb-fs/mutex.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/secrets-vault/db-secrets-vault.service.js +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.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 -83
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +58 -3
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +325 -60
- 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.routes.d.ts +2 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +98 -9
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +133 -9
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +462 -52
- 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 +11 -14
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +38 -1
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +131 -20
- 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/token-crypto.d.ts +17 -2
- package/dist/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -8
- package/dist/shared/token-crypto.js.map +1 -1
- 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__/core-config.admin.test.ts +21 -0
- 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 +126 -7
- package/src/core/create-core-services.ts +173 -41
- package/src/core-config.ts +14 -7
- 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/kb-read-filter.ts +22 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
- package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
- package/src/modules/connection-probe/connection-probe.service.ts +734 -0
- package/src/modules/connection-probe/index.ts +2 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/diff/diff.routes.ts +9 -5
- package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
- package/src/modules/kb-fs/mutex.ts +50 -5
- 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/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
- package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
- 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 +35 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
- 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.history-guards.test.ts +86 -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 +354 -63
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.routes.ts +104 -4
- package/src/modules/workflow/workflow.service.ts +505 -64
- 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 +17 -21
- package/src/modules/workspace/workspace.service.ts +128 -19
- 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__/token-crypto.test.ts +34 -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/token-crypto.ts +28 -10
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import '../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_BRANCH,
|
|
5
|
+
joinBranchFor,
|
|
6
|
+
type AuthUser,
|
|
7
|
+
type IWorkflowService,
|
|
8
|
+
} from '@bevel-software/platform-shared';
|
|
9
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
10
|
+
import { spliceGrant } from '../access-model/access-splice.js';
|
|
11
|
+
import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
12
|
+
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
13
|
+
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
14
|
+
import type { JoinRequestsService } from '../plugins/join-requests.service.js';
|
|
15
|
+
import type { ISkillService } from './skills.contract.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Asking for WRITE on a shared skill — the same machinery as asking to join a
|
|
19
|
+
* plugin, pointed at a skill folder instead of a plugin folder.
|
|
20
|
+
*
|
|
21
|
+
* POST /api/skills/:name/access-request → { ok, number }
|
|
22
|
+
* GET /api/skills/:name/access-requests → { requests } (skill editors)
|
|
23
|
+
* POST /api/skills/:name/access-requests/:n/reconcile → { closed }
|
|
24
|
+
*
|
|
25
|
+
* Why it exists: linking a skill into a plugin needs write on the skill's
|
|
26
|
+
* rules (the link grants the plugin's principal there), and a plugin manager
|
|
27
|
+
* often has no say over a skill some other scope owns. The request is a plain
|
|
28
|
+
* change request whose branch splices `write: <requester>` into the skill
|
|
29
|
+
* folder's `access.md`; the skill's editors see its proposals and accept
|
|
30
|
+
* through the ordinary grant path, and it retires itself once the default
|
|
31
|
+
* branch carries the grant (`JoinRequestsService`, keyed on the skill FOLDER
|
|
32
|
+
* path rather than a plugin name — the branch-name convention takes any
|
|
33
|
+
* exact string).
|
|
34
|
+
*
|
|
35
|
+
* Fail-closed like the skill catalog: a skill the caller cannot read answers
|
|
36
|
+
* as unknown, and the editors' listing answers `[]` to anyone else.
|
|
37
|
+
*/
|
|
38
|
+
export function createSkillAccessRequestRoutes(deps: {
|
|
39
|
+
skillService: ISkillService;
|
|
40
|
+
accessControl: IAccessControl;
|
|
41
|
+
workflow: IWorkflowService;
|
|
42
|
+
workspaceService: WorkspaceService;
|
|
43
|
+
joinRequests: JoinRequestsService;
|
|
44
|
+
kbDirName: string;
|
|
45
|
+
resolveUser: (req: express.Request) => Promise<AuthUser | null>;
|
|
46
|
+
}): express.Router {
|
|
47
|
+
const { skillService, accessControl, workflow, workspaceService, joinRequests, kbDirName, resolveUser } = deps;
|
|
48
|
+
const router = express.Router();
|
|
49
|
+
const wsId = () => workspaceIdForBranch(DEFAULT_BRANCH);
|
|
50
|
+
const rulesOf = (folder: string) => `${folder}/access.md`;
|
|
51
|
+
|
|
52
|
+
/** The skill the caller may read, by name — or null after answering 404. */
|
|
53
|
+
async function readableSkill(
|
|
54
|
+
req: express.Request,
|
|
55
|
+
res: express.Response,
|
|
56
|
+
): Promise<{ user: AuthUser; folder: string; name: string } | null> {
|
|
57
|
+
const user = await resolveUser(req);
|
|
58
|
+
if (!user) {
|
|
59
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const name = String(req.params.name);
|
|
63
|
+
const skill = (await skillService.listSkills(user.email)).find((s) => s.name === name);
|
|
64
|
+
if (!skill) {
|
|
65
|
+
res.status(404).json({ error: 'Unknown skill', kind: 'unknown-skill' });
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return { user, folder: skill.path, name };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
router.post('/skills/:name/access-request', async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const ctx = await readableSkill(req, res);
|
|
74
|
+
if (!ctx) return;
|
|
75
|
+
const { user, folder, name } = ctx;
|
|
76
|
+
if (await accessControl.canWrite(wsId(), user.email, rulesOf(folder))) {
|
|
77
|
+
res.status(409).json({ error: 'You can already edit this skill', kind: 'already-writable' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const branch = joinBranchFor(user.email, folder);
|
|
81
|
+
// FRESH: a repeated click must find the request the previous one opened,
|
|
82
|
+
// and the cached listing can trail it — a miss here would send the
|
|
83
|
+
// retry into a duplicate refusal.
|
|
84
|
+
const existing = (await workflow.listChangeRequestsAuthoredBy(user.email, { fresh: true })).find(
|
|
85
|
+
(cr) => cr.state === 'open' && cr.branch === branch,
|
|
86
|
+
);
|
|
87
|
+
if (existing) {
|
|
88
|
+
res.json({ ok: true, number: existing.number });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// The branch may already exist (a request listed as closed, a retry
|
|
92
|
+
// after a failed open). Existence is PROBED, before and — if creation
|
|
93
|
+
// fails — after: the race shows up as "already exists" locally or as a
|
|
94
|
+
// rejected push when origin got there first, and a message is not a
|
|
95
|
+
// contract. A branch that is there is proceeded against; anything else
|
|
96
|
+
// is a real failure, and a proposal on a branch that was not made would
|
|
97
|
+
// be worse than the error. STRICT: the list proves absence, and a
|
|
98
|
+
// listing that could not fetch proves nothing — it throws instead of
|
|
99
|
+
// serving refs from before the branch was made (or deleted).
|
|
100
|
+
const branchExists = async () =>
|
|
101
|
+
(await workflow.listBranches(wsId(), { freshFetch: true, strictFetch: true })).some(
|
|
102
|
+
(b) => b.name === branch,
|
|
103
|
+
);
|
|
104
|
+
if (!(await branchExists())) {
|
|
105
|
+
try {
|
|
106
|
+
await workflow.createBranch(wsId(), branch, DEFAULT_BRANCH);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (!(await branchExists())) throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const ws = await workspaceService.getOrCreateForBranch(branch);
|
|
112
|
+
const accessPath = `${kbDirName}/${rulesOf(folder)}`;
|
|
113
|
+
// Only a PROVEN absence reads as "no rules yet": any other failure must
|
|
114
|
+
// not be turned into an empty file that a splice then overwrites.
|
|
115
|
+
const current = await workspaceService.readFile(ws.id, accessPath).catch((err: unknown) => {
|
|
116
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
117
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') throw err;
|
|
118
|
+
return '';
|
|
119
|
+
});
|
|
120
|
+
const spliced = spliceGrant(
|
|
121
|
+
current,
|
|
122
|
+
'write',
|
|
123
|
+
{ kind: 'user', email: user.email, displayName: user.name },
|
|
124
|
+
{ target: 'folder' },
|
|
125
|
+
);
|
|
126
|
+
if (spliced.changed) {
|
|
127
|
+
await workspaceService.writeFile(ws.id, accessPath, spliced.text);
|
|
128
|
+
await workflow.commitChanges(ws.id, user, `Request write access to ${name}`);
|
|
129
|
+
}
|
|
130
|
+
const detail = await workflow.openChangeRequest(ws.id, user, {
|
|
131
|
+
sourceBranch: branch,
|
|
132
|
+
targetBranch: DEFAULT_BRANCH,
|
|
133
|
+
title: `Access request: ${name}`,
|
|
134
|
+
description:
|
|
135
|
+
`${user.name} asked to edit the skill ${name}. An editor of the skill accepts by ` +
|
|
136
|
+
`granting the access this branch proposes; the request closes itself once ` +
|
|
137
|
+
`every proposal has landed.`,
|
|
138
|
+
});
|
|
139
|
+
res.json({ ok: true, number: detail.number });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err instanceof WorkflowDomainError) {
|
|
142
|
+
res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.error('[skills] failed to open an access request:', err);
|
|
146
|
+
res.status(500).json({ error: 'Failed to request access' });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/** The caller as an EDITOR of the skill's rules, or null after answering. */
|
|
151
|
+
async function requireEditor(
|
|
152
|
+
req: express.Request,
|
|
153
|
+
res: express.Response,
|
|
154
|
+
onDenied: () => void,
|
|
155
|
+
): Promise<{ user: AuthUser; folder: string } | null> {
|
|
156
|
+
const ctx = await readableSkill(req, res);
|
|
157
|
+
if (!ctx) return null;
|
|
158
|
+
if (!(await accessControl.canWrite(wsId(), ctx.user.email, rulesOf(ctx.folder)))) {
|
|
159
|
+
onDenied();
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return ctx;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
router.get('/skills/:name/access-requests', async (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const ctx = await requireEditor(req, res, () => res.json({ requests: [] }));
|
|
168
|
+
if (!ctx) return;
|
|
169
|
+
const crs = await workflow.listChangeRequests();
|
|
170
|
+
res.json({ requests: await joinRequests.list(ctx.folder, ctx.folder, crs, ctx.user) });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('[skills] failed to list access requests:', err);
|
|
173
|
+
res.status(500).json({ error: 'Failed to list access requests' });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
router.post('/skills/:name/access-requests/:number/reconcile', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const ctx = await requireEditor(req, res, () => res.status(404).json({ error: 'Not found' }));
|
|
180
|
+
if (!ctx) return;
|
|
181
|
+
const crNumber = Number(req.params.number);
|
|
182
|
+
if (!Number.isSafeInteger(crNumber) || crNumber <= 0) {
|
|
183
|
+
res.status(400).json({ error: 'Invalid change request number' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const cr = await workflow.getChangeRequest(crNumber);
|
|
187
|
+
if (!cr) {
|
|
188
|
+
res.status(404).json({ error: 'Not found' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
res.json({ closed: await joinRequests.reconcile(ctx.folder, ctx.folder, cr, ctx.user) });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error('[skills] failed to reconcile an access request:', err);
|
|
194
|
+
res.status(500).json({ error: 'Failed to reconcile the request' });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return router;
|
|
199
|
+
}
|
|
@@ -7,13 +7,45 @@
|
|
|
7
7
|
* bundled file's content (level 3).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* One plugin a skill belongs to — by sitting INSIDE the plugin folder
|
|
12
|
+
* (`linked: false`) or by being LINKED from the plugin's manifest
|
|
13
|
+
* (`linked: true`). For a link, `granted` is the link's HEALTH: whether the
|
|
14
|
+
* plugin's members can read the skill through it. For a link this platform
|
|
15
|
+
* manages that is whether the skill's own access rules name the plugin's
|
|
16
|
+
* `plugin/<Name>/read` principal — the link service writes that grant with
|
|
17
|
+
* the link, so `false` means a hand edit took it away. A link read from an
|
|
18
|
+
* external plugin format needs no grant (the skill's own scope decides who
|
|
19
|
+
* reads it) and is always `true`; the flag never proves a principal exists.
|
|
20
|
+
*/
|
|
21
|
+
export interface PluginMembership {
|
|
22
|
+
/** The plugin folder name. */
|
|
23
|
+
name: string;
|
|
24
|
+
linked: boolean;
|
|
25
|
+
granted: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
export interface SkillSummary {
|
|
11
29
|
/** Canonical id = the skill's folder name (e.g. `rfi`). */
|
|
12
30
|
name: string;
|
|
13
31
|
description: string;
|
|
14
32
|
version?: string;
|
|
33
|
+
/** The governance owner from `metadata.owner`, when the skill declares one. */
|
|
34
|
+
owner?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The governance lifecycle from `metadata.lifecycle`, lowercased — `active`
|
|
37
|
+
* (or absent), `deprecated` (still served, flagged), `retired` (served to the
|
|
38
|
+
* catalog for its owners, never compiled into a distribution).
|
|
39
|
+
*/
|
|
40
|
+
lifecycle?: string;
|
|
15
41
|
/** Repo-root-relative skill folder, e.g. `Plugins/Everyone/rfi`. */
|
|
16
42
|
path: string;
|
|
43
|
+
/**
|
|
44
|
+
* The plugins this skill belongs to, decorated onto the catalog by the
|
|
45
|
+
* browser route (the catalog itself is plugin-unaware). Absent on the agent
|
|
46
|
+
* surfaces, which load skills by name and never by plugin.
|
|
47
|
+
*/
|
|
48
|
+
plugins?: PluginMembership[];
|
|
17
49
|
}
|
|
18
50
|
|
|
19
51
|
export interface Skill extends SkillSummary {
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
2
3
|
import '../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
|
|
3
|
-
import
|
|
4
|
+
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
5
|
+
import type { IPendingSkillService, ISkillService, PluginMembership } from './skills.contract.js';
|
|
6
|
+
|
|
7
|
+
/** The slice of the plugin link index this surface reads — see `PluginLinkIndex`. */
|
|
8
|
+
export interface SkillMembershipSource {
|
|
9
|
+
membership(): Promise<{
|
|
10
|
+
bySkill: Map<string, PluginMembership[]>;
|
|
11
|
+
byPlugin: Map<string, { folder: string }>;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The one access verdict this surface needs: may the caller read a path on the default branch? */
|
|
16
|
+
export interface SkillMembershipGate {
|
|
17
|
+
canReadBatch(workspaceId: string, userEmail: string, paths: string[]): Promise<Map<string, boolean>>;
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
/**
|
|
6
21
|
* Browser-facing (JWT) skill routes for the `/`-command menu, mounted behind
|
|
@@ -8,14 +23,27 @@ import type { IPendingSkillService, ISkillService } from './skills.contract.js';
|
|
|
8
23
|
* so discovery, default-branch pinning, and per-user `canRead` filtering live
|
|
9
24
|
* ONCE — the frontend never re-implements skill discovery or bypasses access.
|
|
10
25
|
*
|
|
11
|
-
* GET /api/skills → { skills: SkillSummary[] } (filtered to this user
|
|
26
|
+
* GET /api/skills → { skills: SkillSummary[] } (filtered to this user,
|
|
27
|
+
* each with its `plugins`)
|
|
12
28
|
* GET /api/skills/pending → { skills: PendingSkill[] } (proposed, not released)
|
|
13
29
|
* GET /api/skills/:name → GetSkillResult (body + files)
|
|
14
30
|
* GET /api/skills/:name?file=… → GetSkillResult (a bundled file's content)
|
|
31
|
+
*
|
|
32
|
+
* `plugins` is decorated HERE, not in the catalog: the catalog is what agents
|
|
33
|
+
* load by name and it stays plugin-unaware; the browser is the surface that
|
|
34
|
+
* groups skills by plugin. Optional dependency, so a host without the link
|
|
35
|
+
* index gets undecorated summaries rather than a 500.
|
|
15
36
|
*/
|
|
16
37
|
export function createSkillsRoutes(
|
|
17
38
|
skillService: ISkillService,
|
|
18
39
|
pendingSkills?: IPendingSkillService,
|
|
40
|
+
links?: SkillMembershipSource,
|
|
41
|
+
/**
|
|
42
|
+
* With `links`: the memberships a caller sees name only plugins they may
|
|
43
|
+
* DISCOVER (read the plugin's access.md — the plugin index's own listing
|
|
44
|
+
* verdict), so this surface never reveals a plugin `/api/plugins` omits.
|
|
45
|
+
*/
|
|
46
|
+
gate?: SkillMembershipGate,
|
|
19
47
|
): express.Router {
|
|
20
48
|
const router = express.Router();
|
|
21
49
|
|
|
@@ -25,7 +53,35 @@ export function createSkillsRoutes(
|
|
|
25
53
|
res.status(401).json({ error: 'Unauthenticated' });
|
|
26
54
|
return;
|
|
27
55
|
}
|
|
28
|
-
|
|
56
|
+
const skills = await skillService.listSkills(email);
|
|
57
|
+
if (!links) {
|
|
58
|
+
res.json({ skills });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let bySkill: Map<string, PluginMembership[]>;
|
|
62
|
+
// Fail closed: memberships without a gate to judge them by name nothing.
|
|
63
|
+
let discoverable: (plugin: string) => boolean = () => false;
|
|
64
|
+
try {
|
|
65
|
+
const membership = await links.membership();
|
|
66
|
+
bySkill = membership.bySkill;
|
|
67
|
+
if (gate) {
|
|
68
|
+
const folders = [...membership.byPlugin.entries()];
|
|
69
|
+
const verdicts = folders.length
|
|
70
|
+
? await gate.canReadBatch(
|
|
71
|
+
workspaceIdForBranch(DEFAULT_BRANCH),
|
|
72
|
+
email,
|
|
73
|
+
folders.map(([, p]) => `${p.folder}/access.md`),
|
|
74
|
+
)
|
|
75
|
+
: new Map<string, boolean>();
|
|
76
|
+
const visible = new Set(folders.filter(([, p]) => verdicts.get(`${p.folder}/access.md`) === true).map(([name]) => name));
|
|
77
|
+
discoverable = (plugin) => visible.has(plugin);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
bySkill = new Map(); // membership is a decoration; the shelf must not fall with it
|
|
81
|
+
}
|
|
82
|
+
res.json({
|
|
83
|
+
skills: skills.map((s) => ({ ...s, plugins: (bySkill.get(s.path) ?? []).filter((m) => discoverable(m.name)) })),
|
|
84
|
+
});
|
|
29
85
|
});
|
|
30
86
|
|
|
31
87
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import { parseDocument } from 'yaml';
|
|
4
|
-
import { DEFAULT_BRANCH, PLUGINS_DIR } from '@bevel-software/platform-shared';
|
|
4
|
+
import { DEFAULT_BRANCH, PLUGINS_DIR, SKILLS_DIR } from '@bevel-software/platform-shared';
|
|
5
5
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
6
6
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
7
7
|
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
8
8
|
import { extractFrontmatter, resolveDeclaredId, dedupeById } from '../../shared/frontmatter-id.js';
|
|
9
9
|
import { walkFiles } from '../../shared/fs-walk.js';
|
|
10
10
|
import { TtlCache } from '../../shared/ttl-cache.js';
|
|
11
|
+
import { BevelIgnoreStack } from '../workspace/bevel-ignore.js';
|
|
11
12
|
import type {
|
|
12
13
|
ISkillService,
|
|
13
14
|
GetSkillResult,
|
|
@@ -75,6 +76,9 @@ export class SkillService implements ISkillService {
|
|
|
75
76
|
if (file !== undefined) {
|
|
76
77
|
if (!isSafeRelFile(file)) return { ok: false, error: 'invalid_file' };
|
|
77
78
|
const repoPath = `${found.summary.path}/${file}`;
|
|
79
|
+
// Only a file the catalog LISTS is served: the listing already applied
|
|
80
|
+
// the ignore rules, so a path that is not in it is one they hid.
|
|
81
|
+
if (!found.files.includes(repoPath)) return { ok: false, error: 'not_found' };
|
|
78
82
|
try {
|
|
79
83
|
const content = await this.workspaceService.readFile(wsId, `${this.kbDirName}/${repoPath}`);
|
|
80
84
|
return { ok: true, kind: 'file', file: { name, file, path: repoPath, content } };
|
|
@@ -97,15 +101,20 @@ export class SkillService implements ISkillService {
|
|
|
97
101
|
private async scan(): Promise<ParsedSkill[]> {
|
|
98
102
|
const cached = this.cache.get();
|
|
99
103
|
if (cached) return cached;
|
|
104
|
+
// Token first: a merge's `invalidate()` can land while `scanDisk` is still
|
|
105
|
+
// reading the pre-merge tree, and storing that read afterwards would undo
|
|
106
|
+
// the drop for a full TTL.
|
|
107
|
+
const token = this.cache.begin();
|
|
100
108
|
const skills = await this.scanDisk();
|
|
101
|
-
this.cache.set(skills);
|
|
109
|
+
this.cache.set(skills, token);
|
|
102
110
|
return skills;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
private async scanDisk(): Promise<ParsedSkill[]> {
|
|
106
|
-
// Ensure the default-branch clone exists, then scan its
|
|
107
|
-
// failure (no workspace, no
|
|
108
|
-
// manual/tools must never break because skills can't
|
|
114
|
+
// Ensure the default-branch clone exists, then scan its Skills/ and
|
|
115
|
+
// Plugins/ roots. Any failure (no workspace, no such dir) degrades to an
|
|
116
|
+
// empty catalog — the manual/tools must never break because skills can't
|
|
117
|
+
// be read.
|
|
109
118
|
let wsId: string;
|
|
110
119
|
try {
|
|
111
120
|
wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
|
|
@@ -115,20 +124,34 @@ export class SkillService implements ISkillService {
|
|
|
115
124
|
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
116
125
|
|
|
117
126
|
// Skills may be grouped in category subfolders (each carrying its own
|
|
118
|
-
// access.md), so a SKILL.md can live at any depth under
|
|
119
|
-
// tree and treat every folder that directly contains a SKILL.md as a
|
|
120
|
-
// don't descend past it — its inner files are bundled assets, not
|
|
121
|
-
// skills. The skill name is the leaf folder name; its path is the
|
|
122
|
-
// repo-relative folder (e.g. `
|
|
127
|
+
// access.md), so a SKILL.md can live at any depth under either root. Walk
|
|
128
|
+
// the tree and treat every folder that directly contains a SKILL.md as a
|
|
129
|
+
// skill; don't descend past it — its inner files are bundled assets, not
|
|
130
|
+
// nested skills. The skill name is the leaf folder name; its path is the
|
|
131
|
+
// full repo-relative folder (e.g. `Skills/Engineering/deploy`).
|
|
132
|
+
//
|
|
133
|
+
// `.bevelignore` files INSIDE a root are honoured on the way down, the
|
|
134
|
+
// same layered rules the file tree applies: a repository that carries a
|
|
135
|
+
// build output beside its source (a `dist/` holding compiled copies of
|
|
136
|
+
// every skill) would otherwise list each skill twice and refuse the
|
|
137
|
+
// duplicate — the wrong one, half the time. The REPO-ROOT file is
|
|
138
|
+
// deliberately not consulted: it is where the template hides `Plugins/`
|
|
139
|
+
// from the Knowledge tree, and a rule that hides a root from the browser
|
|
140
|
+
// must not empty the catalog that root exists to feed.
|
|
123
141
|
const out: ParsedSkill[] = [];
|
|
124
|
-
const walk = async (dir: string, relFolder: string): Promise<void> => {
|
|
142
|
+
const walk = async (dir: string, relFolder: string, ignore: BevelIgnoreStack): Promise<void> => {
|
|
125
143
|
let entries: import('node:fs').Dirent[];
|
|
126
144
|
try {
|
|
127
145
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
128
146
|
} catch {
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
149
|
+
ignore = await ignore.extendedWith(dir);
|
|
131
150
|
if (entries.some((e) => e.isFile() && e.name === 'SKILL.md')) {
|
|
151
|
+
// A skill folder is a LEAF whatever the ignore rules say of it: a rule
|
|
152
|
+
// naming its SKILL.md suppresses the skill, it does not turn the
|
|
153
|
+
// folder's assets into skills of their own.
|
|
154
|
+
if (ignore.isIgnored(path.join(dir, 'SKILL.md'), false)) return;
|
|
132
155
|
let raw: string;
|
|
133
156
|
try {
|
|
134
157
|
raw = await fs.readFile(path.join(dir, 'SKILL.md'), 'utf-8');
|
|
@@ -144,26 +167,43 @@ export class SkillService implements ISkillService {
|
|
|
144
167
|
const declared = resolveDeclaredId(fm.frontmatter, path.basename(dir));
|
|
145
168
|
const name = isSafeSkillName(declared) ? declared : path.basename(dir);
|
|
146
169
|
out.push({
|
|
147
|
-
summary: {
|
|
170
|
+
summary: {
|
|
171
|
+
name,
|
|
172
|
+
description: fm.description,
|
|
173
|
+
version: fm.version,
|
|
174
|
+
owner: fm.owner,
|
|
175
|
+
lifecycle: fm.lifecycle,
|
|
176
|
+
path: relFolder,
|
|
177
|
+
},
|
|
148
178
|
body: fm.body,
|
|
149
179
|
allowedTools: fm.allowedTools,
|
|
150
|
-
files: await listBundledFiles(dir, relFolder),
|
|
180
|
+
files: await listBundledFiles(dir, relFolder, ignore),
|
|
151
181
|
});
|
|
152
182
|
return; // a skill folder is a leaf — its subfolders hold assets, not skills
|
|
153
183
|
}
|
|
154
184
|
for (const entry of entries) {
|
|
155
185
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
156
|
-
|
|
186
|
+
const abs = path.join(dir, entry.name);
|
|
187
|
+
if (ignore.isIgnored(abs, true)) continue;
|
|
188
|
+
await walk(abs, `${relFolder}/${entry.name}`, ignore);
|
|
157
189
|
}
|
|
158
190
|
};
|
|
159
|
-
|
|
191
|
+
// Each root starts its own ignore stack (the walk extends it with the
|
|
192
|
+
// root's own file first). Order is cosmetic: the sort below is by name
|
|
193
|
+
// then path, so a same-named pair resolves the same way regardless.
|
|
194
|
+
await walk(path.join(kbRoot, SKILLS_DIR), SKILLS_DIR, BevelIgnoreStack.empty());
|
|
195
|
+
await walk(path.join(kbRoot, PLUGINS_DIR), PLUGINS_DIR, BevelIgnoreStack.empty());
|
|
160
196
|
// A skill's id (frontmatter `id`/`name`, else folder name) is how getSkill()
|
|
161
|
-
// resolves it, so it must be unique. Sort by (name, path) for a
|
|
162
|
-
// winner
|
|
163
|
-
//
|
|
197
|
+
// resolves it, so it must be unique. Sort by (name, root, path) for a
|
|
198
|
+
// deterministic winner — the shared root FIRST, since `Skills/` is a
|
|
199
|
+
// skill's canonical home and a same-named inline copy is the stale one —
|
|
200
|
+
// then REFUSE later duplicates via the shared dedup — the same rule tools
|
|
201
|
+
// use (no silent auto-suffix that would rebind an id under the caller).
|
|
202
|
+
const rootRank = (p: string) => (p === SKILLS_DIR || p.startsWith(`${SKILLS_DIR}/`) ? 0 : 1);
|
|
164
203
|
out.sort(
|
|
165
204
|
(a, b) =>
|
|
166
205
|
a.summary.name.localeCompare(b.summary.name) ||
|
|
206
|
+
rootRank(a.summary.path) - rootRank(b.summary.path) ||
|
|
167
207
|
a.summary.path.localeCompare(b.summary.path),
|
|
168
208
|
);
|
|
169
209
|
return dedupeById(out, (s) => s.summary.name, (s, id) =>
|
|
@@ -209,6 +249,10 @@ function scalarToString(value: unknown): string | undefined {
|
|
|
209
249
|
export function parseSkillFrontmatter(raw: string): {
|
|
210
250
|
description: string;
|
|
211
251
|
version?: string;
|
|
252
|
+
/** `metadata.owner` (or a top-level `owner`) — the governance record's owner, verbatim. */
|
|
253
|
+
owner?: string;
|
|
254
|
+
/** `metadata.lifecycle` (or top-level) — e.g. `active`, `deprecated`, `retired`; lowercased. */
|
|
255
|
+
lifecycle?: string;
|
|
212
256
|
allowedTools?: string[];
|
|
213
257
|
body: string;
|
|
214
258
|
/** The parsed frontmatter object (for shared id resolution: `id`/`name`). */
|
|
@@ -233,6 +277,11 @@ export function parseSkillFrontmatter(raw: string): {
|
|
|
233
277
|
const description = typeof data.description === 'string' ? data.description.trim() : '';
|
|
234
278
|
const metadata = (data.metadata ?? {}) as Record<string, unknown>;
|
|
235
279
|
const version = scalarToString(data.version) ?? scalarToString(metadata.version);
|
|
280
|
+
// The governance record: `metadata.owner` / `metadata.lifecycle` first (the
|
|
281
|
+
// agentskills convention this catalog reads), a top-level spelling second.
|
|
282
|
+
const owner = (scalarToString(metadata.owner) ?? scalarToString(data.owner))?.trim() || undefined;
|
|
283
|
+
const lifecycle =
|
|
284
|
+
(scalarToString(metadata.lifecycle) ?? scalarToString(data.lifecycle))?.trim().toLowerCase() || undefined;
|
|
236
285
|
|
|
237
286
|
// `allowed-tools` is a space-separated string (agentskills) or a YAML list.
|
|
238
287
|
const at = data['allowed-tools'];
|
|
@@ -242,12 +291,25 @@ export function parseSkillFrontmatter(raw: string): {
|
|
|
242
291
|
? at.split(/\s+/).filter(Boolean)
|
|
243
292
|
: undefined;
|
|
244
293
|
|
|
245
|
-
return { description, version, allowedTools, body, frontmatter: data };
|
|
294
|
+
return { description, version, owner, lifecycle, allowedTools, body, frontmatter: data };
|
|
246
295
|
}
|
|
247
296
|
|
|
248
|
-
/**
|
|
249
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Repo-root-relative paths of every bundled file under a skill folder
|
|
299
|
+
* (excludes SKILL.md), under the same ignore rules the walk applied on the
|
|
300
|
+
* way down — a rule in the skill's own `.bevelignore` or any folder above
|
|
301
|
+
* hides an asset from the listing, and therefore (see `getSkill`) from
|
|
302
|
+
* being served.
|
|
303
|
+
*/
|
|
304
|
+
async function listBundledFiles(dir: string, relFolder: string, ignore: BevelIgnoreStack): Promise<string[]> {
|
|
305
|
+
const hidden = (rel: string): boolean => {
|
|
306
|
+
const parts = rel.split('/');
|
|
307
|
+
for (let i = 1; i < parts.length; i++) {
|
|
308
|
+
if (ignore.isIgnored(path.join(dir, ...parts.slice(0, i)), true)) return true;
|
|
309
|
+
}
|
|
310
|
+
return ignore.isIgnored(path.join(dir, ...parts), false);
|
|
311
|
+
};
|
|
250
312
|
return (await walkFiles(dir, () => true))
|
|
251
|
-
.filter((rel) => rel !== 'SKILL.md')
|
|
313
|
+
.filter((rel) => rel !== 'SKILL.md' && !hidden(rel))
|
|
252
314
|
.map((rel) => `${relFolder}/${rel}`);
|
|
253
315
|
}
|
|
@@ -22,6 +22,12 @@ declare global {
|
|
|
22
22
|
export interface ExternalApiKeySummary {
|
|
23
23
|
id: string;
|
|
24
24
|
label: string;
|
|
25
|
+
/**
|
|
26
|
+
* What the key was minted AS — `key` for one a person created by hand,
|
|
27
|
+
* another kind for one a flow minted on their behalf (a Claude link).
|
|
28
|
+
* Stored with the row; the label is the person's to edit and proves nothing.
|
|
29
|
+
*/
|
|
30
|
+
kind: string;
|
|
25
31
|
createdAt: number;
|
|
26
32
|
lastUsedAt: number | null;
|
|
27
33
|
revokedAt: number | null;
|
|
@@ -37,6 +43,19 @@ export interface MintedExternalApiKey {
|
|
|
37
43
|
summary: ExternalApiKeySummary;
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
/** The kind of a key a person creates by hand. */
|
|
47
|
+
export const DEFAULT_KEY_KIND = 'key';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* How a key is minted. `kind` names one of the kinds the service was built
|
|
51
|
+
* with, each of which carries its own plaintext prefix (the tenant's for the
|
|
52
|
+
* default kind, `gho_` for a Claude link); an unknown kind is refused, since
|
|
53
|
+
* nothing would route its bearer back.
|
|
54
|
+
*/
|
|
55
|
+
export interface MintOptions {
|
|
56
|
+
kind?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
/**
|
|
41
60
|
* Contract for connection-key (API token) lifecycle. Used by the MCP auth
|
|
42
61
|
* middleware to resolve a Bearer token to a Bevel user, and by the settings
|
|
@@ -59,7 +78,7 @@ export interface IExternalApiKeyService {
|
|
|
59
78
|
* plaintext is **only** returned here — there is no read path that can
|
|
60
79
|
* surface it again.
|
|
61
80
|
*/
|
|
62
|
-
mint(userId: string, label: string): Promise<MintedExternalApiKey>;
|
|
81
|
+
mint(userId: string, label: string, options?: MintOptions): Promise<MintedExternalApiKey>;
|
|
63
82
|
|
|
64
83
|
/**
|
|
65
84
|
* Resolve a plaintext token to the owning user. Returns null when the
|
|
@@ -8,36 +8,53 @@ import {
|
|
|
8
8
|
TokenNotFoundError,
|
|
9
9
|
TokenStillActiveError,
|
|
10
10
|
} from './external-api-key.errors.js';
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_KEY_KIND,
|
|
13
|
+
type ExternalApiKeySummary,
|
|
14
|
+
type IExternalApiKeyService,
|
|
15
|
+
type MintOptions,
|
|
16
|
+
type MintedExternalApiKey,
|
|
15
17
|
} from './external-api-key.interface.js';
|
|
16
18
|
|
|
17
|
-
// Plaintext format: `<
|
|
19
|
+
// Plaintext format: `<prefix><43 base64url chars>`. 32 random bytes encoded
|
|
18
20
|
// base64url is 43 chars and gives 256 bits of entropy — overkill against
|
|
19
|
-
// brute force, cheap to copy. The
|
|
20
|
-
//
|
|
21
|
+
// brute force, cheap to copy. The prefix lets the auth middleware route
|
|
22
|
+
// key-vs-JWT requests without parsing both shapes.
|
|
21
23
|
const TOKEN_BYTES = 32;
|
|
22
24
|
|
|
23
25
|
const MAX_LABEL_LEN = 200;
|
|
24
26
|
|
|
25
27
|
export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
28
|
+
private readonly prefixes: readonly string[];
|
|
29
|
+
|
|
26
30
|
/**
|
|
27
31
|
* @param keyPrefix Tenant-derived plaintext prefix (e.g. `bevel_`) — injected
|
|
28
32
|
* from {@link AppConfig.externalApiKeyPrefix} so a deploy can brand its keys.
|
|
33
|
+
* Every key of the default kind is minted with it.
|
|
34
|
+
* @param kinds Other kinds a key may be minted as, each with the plaintext
|
|
35
|
+
* prefix that kind is spelled with — a Claude link's `gho_`, the shape
|
|
36
|
+
* claude.ai expects from a GitHub host. Same key, same table, same
|
|
37
|
+
* revocation; the KIND is stored on the row and is what tells such keys
|
|
38
|
+
* apart (a label is the person's to edit), the prefix is only what the
|
|
39
|
+
* outside world sees and what routes the bearer back here.
|
|
29
40
|
*/
|
|
30
41
|
constructor(
|
|
31
42
|
private readonly db: Database,
|
|
32
43
|
private readonly keyPrefix: string,
|
|
33
|
-
|
|
44
|
+
private readonly kinds: Readonly<Record<string, string>> = {},
|
|
45
|
+
) {
|
|
46
|
+
this.prefixes = [keyPrefix, ...Object.values(kinds)];
|
|
47
|
+
}
|
|
34
48
|
|
|
35
|
-
/** True if a bearer string carries this
|
|
49
|
+
/** True if a bearer string carries one of this service's key prefixes. */
|
|
36
50
|
looksLikeExternalApiKey(token: string): boolean {
|
|
37
|
-
return typeof token === 'string' && token.startsWith(
|
|
51
|
+
return typeof token === 'string' && this.prefixes.some((p) => token.startsWith(p));
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
async mint(userId: string, label: string): Promise<MintedExternalApiKey> {
|
|
54
|
+
async mint(userId: string, label: string, options: MintOptions = {}): Promise<MintedExternalApiKey> {
|
|
55
|
+
const kind = options.kind ?? DEFAULT_KEY_KIND;
|
|
56
|
+
const prefix = kind === DEFAULT_KEY_KIND ? this.keyPrefix : this.kinds[kind];
|
|
57
|
+
if (!prefix) throw new Error(`Unknown key kind "${kind}"`);
|
|
41
58
|
const trimmed = (label ?? '').trim();
|
|
42
59
|
if (trimmed.length === 0) {
|
|
43
60
|
throw new InvalidTokenLabelError('Label cannot be empty');
|
|
@@ -48,12 +65,12 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
48
65
|
);
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
const plaintext =
|
|
68
|
+
const plaintext = prefix + randomBytes(TOKEN_BYTES).toString('base64url');
|
|
52
69
|
const tokenHash = hashToken(plaintext);
|
|
53
70
|
|
|
54
71
|
const [row] = await this.db
|
|
55
72
|
.insert(externalApiKeys)
|
|
56
|
-
.values({ userId, tokenHash, label: trimmed })
|
|
73
|
+
.values({ userId, tokenHash, label: trimmed, kind })
|
|
57
74
|
.returning();
|
|
58
75
|
|
|
59
76
|
return {
|
|
@@ -202,6 +219,7 @@ function toSummary(row: typeof externalApiKeys.$inferSelect): ExternalApiKeySumm
|
|
|
202
219
|
return {
|
|
203
220
|
id: row.id,
|
|
204
221
|
label: row.label,
|
|
222
|
+
kind: row.kind,
|
|
205
223
|
createdAt: row.createdAt.getTime(),
|
|
206
224
|
lastUsedAt: row.lastUsedAt ? row.lastUsedAt.getTime() : null,
|
|
207
225
|
revokedAt: row.revokedAt ? row.revokedAt.getTime() : null,
|