@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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
3
|
-
import { isProtectedBranch, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
3
|
+
import { isProtectedBranch, DEFAULT_BRANCH, pluginManifestName } from '@bevel-software/platform-shared';
|
|
4
4
|
import type {
|
|
5
5
|
IAccessControl,
|
|
6
6
|
GrantPrincipal,
|
|
7
7
|
GrantSources,
|
|
8
|
+
ResolvedPrincipal,
|
|
8
9
|
} from './access-control.interface.js';
|
|
9
10
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
10
11
|
import { branchForWorkspaceId, workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
@@ -22,7 +23,10 @@ import {
|
|
|
22
23
|
import {
|
|
23
24
|
canonicalRoleName,
|
|
24
25
|
EVERYONE_CANONICAL,
|
|
26
|
+
PLUGIN_TOKEN_PREFIX,
|
|
27
|
+
PLUGIN_TOKEN_VERBS,
|
|
25
28
|
ROLE_TOKEN_PREFIX,
|
|
29
|
+
type PluginTokenVerb,
|
|
26
30
|
type Verb,
|
|
27
31
|
} from '../access-model/access-grammar.js';
|
|
28
32
|
import { listAccessDeclarationsUnder } from './access-declarations.js';
|
|
@@ -79,20 +83,51 @@ export function createAccessRoutes(
|
|
|
79
83
|
// accepts — the same principals the admin screens manage, regardless of
|
|
80
84
|
// which branch the caller is viewing. People stay branch-local (the
|
|
81
85
|
// caller's own workspace).
|
|
82
|
-
const defaultBranchPrincipals = async (): Promise<{
|
|
86
|
+
const defaultBranchPrincipals = async (): Promise<{
|
|
87
|
+
roles: string[];
|
|
88
|
+
groups: string[];
|
|
89
|
+
plugins: { name: string; folder: string }[];
|
|
90
|
+
}> => {
|
|
83
91
|
await workspaceService.getOrCreateForBranch(DEFAULT_BRANCH);
|
|
84
|
-
const { roles, groups } = await accessControl.kbPrincipals(
|
|
85
|
-
|
|
92
|
+
const { roles, groups, plugins } = await accessControl.kbPrincipals(
|
|
93
|
+
workspaceIdForBranch(DEFAULT_BRANCH),
|
|
94
|
+
);
|
|
95
|
+
// `plugins` is defensive: an older resolver double may omit it.
|
|
96
|
+
return { roles, groups, plugins: plugins ?? [] };
|
|
86
97
|
};
|
|
87
98
|
|
|
88
99
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
100
|
+
* The plugins `email` may DISCOVER — read the folder's access.md, the same
|
|
101
|
+
* verdict the plugin index lists on. Both the suggest list and the grant
|
|
102
|
+
* route go through this, so a plugin hidden from the picker cannot be
|
|
103
|
+
* named in a grant either, and an undiscoverable one answers exactly like
|
|
104
|
+
* one that does not exist.
|
|
94
105
|
*/
|
|
95
|
-
|
|
106
|
+
const discoverablePlugins = async (
|
|
107
|
+
email: string,
|
|
108
|
+
plugins: { name: string; folder: string }[],
|
|
109
|
+
): Promise<{ name: string; folder: string }[]> => {
|
|
110
|
+
if (plugins.length === 0) return [];
|
|
111
|
+
const verdicts = await accessControl.canReadBatch(
|
|
112
|
+
workspaceIdForBranch(DEFAULT_BRANCH),
|
|
113
|
+
email,
|
|
114
|
+
plugins.map((p) => `${p.folder}/access.md`),
|
|
115
|
+
);
|
|
116
|
+
return plugins.filter((p) => verdicts.get(`${p.folder}/access.md`) === true);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What the mutation routes accept as a principal. `group` and `plugin`
|
|
121
|
+
* exist only at this boundary: in the access.md entry grammar a group grant
|
|
122
|
+
* is a bare-name token (bare names resolve GROUP-FIRST, then fall back to
|
|
123
|
+
* the role) and a plugin grant is the `plugin/<Name>/<verb>` token, so both
|
|
124
|
+
* are spliced as role-shaped principals — the separate kinds buy validation
|
|
125
|
+
* against the right namespace and honest 404s.
|
|
126
|
+
*/
|
|
127
|
+
type RoutePrincipal =
|
|
128
|
+
| Principal
|
|
129
|
+
| { kind: 'group'; group: string }
|
|
130
|
+
| { kind: 'plugin'; plugin: string; verb: PluginTokenVerb };
|
|
96
131
|
|
|
97
132
|
/**
|
|
98
133
|
* The role-shaped principal the splice layer actually writes/matches.
|
|
@@ -104,6 +139,9 @@ export function createAccessRoutes(
|
|
|
104
139
|
*/
|
|
105
140
|
const asSplicePrincipal = (p: RoutePrincipal): Principal => {
|
|
106
141
|
if (p.kind === 'group') return { kind: 'role', role: p.group };
|
|
142
|
+
// Written with the folder's own casing; `parseAccessEntry` canonicalises
|
|
143
|
+
// the name half to the manifest slug on the way back in.
|
|
144
|
+
if (p.kind === 'plugin') return { kind: 'role', role: `${PLUGIN_TOKEN_PREFIX}${p.plugin.trim()}/${p.verb}` };
|
|
107
145
|
if (p.kind === 'role') {
|
|
108
146
|
const canonical = canonicalRoleName(p.role);
|
|
109
147
|
const bare = canonical.startsWith(ROLE_TOKEN_PREFIX)
|
|
@@ -336,7 +374,7 @@ export function createAccessRoutes(
|
|
|
336
374
|
// Independent lookups batched in ONE Promise.all: the default-branch
|
|
337
375
|
// principals (cached model), the branch-local KB people (cached model),
|
|
338
376
|
// and the users table (only consulted once the query is long enough).
|
|
339
|
-
const [{ roles, groups }, { people: kbPeople }, userRows] = await Promise.all([
|
|
377
|
+
const [{ roles, groups, plugins }, { people: kbPeople }, userRows] = await Promise.all([
|
|
340
378
|
defaultBranchPrincipals(),
|
|
341
379
|
accessControl.kbPrincipals(req.params.id),
|
|
342
380
|
q.length >= 2 ? db.select().from(users) : Promise.resolve([]),
|
|
@@ -350,6 +388,19 @@ export function createAccessRoutes(
|
|
|
350
388
|
.filter((g) => !q || g.toLowerCase().includes(q))
|
|
351
389
|
.slice(0, CAP);
|
|
352
390
|
|
|
391
|
+
// Plugins the caller can DISCOVER (read the folder's access.md — the
|
|
392
|
+
// same verdict the plugin index lists on), so the picker never names a
|
|
393
|
+
// plugin its enumeration would hide. Their `plugin/<Name>/<verb>`
|
|
394
|
+
// tokens are built client-side; the name is what a person searches for.
|
|
395
|
+
const matchedPlugins = (
|
|
396
|
+
await discoverablePlugins(
|
|
397
|
+
user.email,
|
|
398
|
+
plugins.filter((p) => !q || p.name.toLowerCase().includes(q)),
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
.map((p) => p.name)
|
|
402
|
+
.slice(0, CAP);
|
|
403
|
+
|
|
353
404
|
let people: { name: string; email: string }[] = [];
|
|
354
405
|
if (q.length >= 2) {
|
|
355
406
|
// Union the KB-canonical people with the login-only users table.
|
|
@@ -373,6 +424,10 @@ export function createAccessRoutes(
|
|
|
373
424
|
res.json({
|
|
374
425
|
roles: matchedRoles,
|
|
375
426
|
groups: matchedGroups,
|
|
427
|
+
// Plugin FOLDER names; each stands for three grantable principals
|
|
428
|
+
// (`plugin/<Name>/read|write|owner`). Not `plugins` — that key is the
|
|
429
|
+
// deprecated alias of `roles` below.
|
|
430
|
+
pluginPrincipals: matchedPlugins,
|
|
376
431
|
people,
|
|
377
432
|
peopleWithheld: q.length < 2,
|
|
378
433
|
// DEPRECATED alias of `roles` — the shipped share dialog still reads
|
|
@@ -428,8 +483,19 @@ export function createAccessRoutes(
|
|
|
428
483
|
throw new AccessMutationError('group principal needs a group name');
|
|
429
484
|
}
|
|
430
485
|
principal = { kind: 'group', group: principalRaw.group };
|
|
486
|
+
} else if (principalRaw.kind === 'plugin') {
|
|
487
|
+
if (typeof principalRaw.plugin !== 'string' || !principalRaw.plugin.trim()) {
|
|
488
|
+
throw new AccessMutationError('plugin principal needs a plugin name');
|
|
489
|
+
}
|
|
490
|
+
const verb = principalRaw.verb;
|
|
491
|
+
if (typeof verb !== 'string' || !(PLUGIN_TOKEN_VERBS as readonly string[]).includes(verb)) {
|
|
492
|
+
throw new AccessMutationError(
|
|
493
|
+
`plugin principal needs a verb: one of ${PLUGIN_TOKEN_VERBS.join(', ')}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
principal = { kind: 'plugin', plugin: principalRaw.plugin, verb: verb as PluginTokenVerb };
|
|
431
497
|
} else {
|
|
432
|
-
throw new AccessMutationError("principal.kind must be 'user', 'group', or 'role'");
|
|
498
|
+
throw new AccessMutationError("principal.kind must be 'user', 'group', 'plugin', or 'role'");
|
|
433
499
|
}
|
|
434
500
|
const targetKind = kind as TargetKind;
|
|
435
501
|
const repoRelTarget = toRepoRelative(rawPath);
|
|
@@ -517,12 +583,12 @@ export function createAccessRoutes(
|
|
|
517
583
|
// non-actionable. Built over the union of every principal in the four
|
|
518
584
|
// eligible lists (kinded `principals`, with the name-only `roles` list as
|
|
519
585
|
// the all-roles fallback).
|
|
520
|
-
const collectives = new Map<string,
|
|
586
|
+
const collectives = new Map<string, ResolvedPrincipal>();
|
|
521
587
|
for (const list of [eligible, readers, owners, downloaders]) {
|
|
522
588
|
const kinded =
|
|
523
589
|
list.principals ?? list.roles.map((name) => ({ name, kind: 'role' as const }));
|
|
524
590
|
for (const p of kinded) {
|
|
525
|
-
const key = `${p.kind === 'group' ? 'g' : 'r'}:${p.name.toLowerCase()}`;
|
|
591
|
+
const key = `${p.kind === 'group' ? 'g' : p.kind === 'plugin' ? 'p' : 'r'}:${p.name.toLowerCase()}`;
|
|
526
592
|
if (!collectives.has(key)) collectives.set(key, p);
|
|
527
593
|
}
|
|
528
594
|
}
|
|
@@ -673,6 +739,22 @@ export function createAccessRoutes(
|
|
|
673
739
|
{ kind: 'unknown-group', group: principal.group },
|
|
674
740
|
);
|
|
675
741
|
}
|
|
742
|
+
} else if (principal.kind === 'plugin') {
|
|
743
|
+
// A plugin grant must name a plugin the caller can DISCOVER on the
|
|
744
|
+
// default branch — the same set the suggest list offers, so a plugin
|
|
745
|
+
// hidden from the picker is refused here with the same words as one
|
|
746
|
+
// that does not exist. Matched on the manifest slug, the identity the
|
|
747
|
+
// token canonicalises to.
|
|
748
|
+
const { plugins } = await defaultBranchPrincipals();
|
|
749
|
+
const slug = pluginManifestName(principal.plugin);
|
|
750
|
+
const visible = await discoverablePlugins(user.email, plugins);
|
|
751
|
+
if (!visible.some((p) => pluginManifestName(p.name) === slug)) {
|
|
752
|
+
throw new AccessMutationError(
|
|
753
|
+
`No plugin named "${principal.plugin}". Pick an existing plugin.`,
|
|
754
|
+
404,
|
|
755
|
+
{ kind: 'unknown-plugin', plugin: principal.plugin },
|
|
756
|
+
);
|
|
757
|
+
}
|
|
676
758
|
}
|
|
677
759
|
// Splice shape: group → bare token (group-first precedence); role →
|
|
678
760
|
// explicit `role/<Name>` token (see asSplicePrincipal).
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
registerAccessFrontmatterExtensions,
|
|
6
6
|
accessFrontmatterExtensionList,
|
|
7
7
|
parseOwnAccessEntries,
|
|
8
|
+
parseRolesYaml,
|
|
8
9
|
} from '../access-grammar.js';
|
|
9
10
|
import { parseGroupsFile } from '../group-files.js';
|
|
10
11
|
|
|
@@ -22,6 +23,23 @@ describe('parseYamlSubset — inline empty collections', () => {
|
|
|
22
23
|
});
|
|
23
24
|
});
|
|
24
25
|
|
|
26
|
+
describe("the reserved 'plugin/' prefix — no role or group may pose as a plugin principal", () => {
|
|
27
|
+
it('parseGroupsFile skips such a group with a warning and keeps the rest', () => {
|
|
28
|
+
const res = parseGroupsFile('groups:\n plugin/GTM/read:\n - mallory@x.io\n Sales:\n - sam@x.io\n', 'groups.yaml');
|
|
29
|
+
expect(res.ok).toBe(true);
|
|
30
|
+
if (!res.ok) return;
|
|
31
|
+
expect([...res.groups.keys()]).toEqual(['sales']);
|
|
32
|
+
expect(res.warnings.some((w) => w.includes("reserved 'plugin/'"))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('parseRolesYaml refuses such a role outright', () => {
|
|
36
|
+
const res = parseRolesYaml('roles:\n Admin:\n - admin@x.io\n plugin/GTM/read:\n - mallory@x.io\n');
|
|
37
|
+
expect(res.ok).toBe(false);
|
|
38
|
+
if (res.ok) return;
|
|
39
|
+
expect(res.errors.some((e) => e.includes("reserved 'plugin/'"))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
25
43
|
describe('parseGroupsFile — empty group sources', () => {
|
|
26
44
|
it('accepts a bare `groups:` as a valid empty set', () => {
|
|
27
45
|
const res = parseGroupsFile('groups:\n', 'groups.yaml');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
canonicalPluginToken,
|
|
4
|
+
canonicalRoleName,
|
|
5
|
+
parseAccessEntry,
|
|
6
|
+
parsePluginPrincipalKey,
|
|
7
|
+
} from '../access-grammar.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ONE spelling for a plugin principal. Every spelling of a token folds to the
|
|
11
|
+
* same canonical key — through the parser, through the role canonicaliser
|
|
12
|
+
* that grants and revocations compare with — and a canonical key is exactly
|
|
13
|
+
* one name and one verb: nothing nested, nothing that is not already a slug.
|
|
14
|
+
*/
|
|
15
|
+
describe('plugin principal tokens — one canonical form', () => {
|
|
16
|
+
it('folds any spelling of a plugin token to its slugged key', () => {
|
|
17
|
+
for (const spelling of ['plugin/Sales Team/read', 'plugin/sales-team/read', 'Plugin/SALES TEAM/READ', ' plugin/Sales Team/read ']) {
|
|
18
|
+
expect(canonicalPluginToken(spelling)).toBe('plugin/sales-team/read');
|
|
19
|
+
expect(canonicalRoleName(spelling)).toBe('plugin/sales-team/read');
|
|
20
|
+
}
|
|
21
|
+
const parsed = parseAccessEntry('plugin/Sales Team/write');
|
|
22
|
+
expect(parsed.ok && parsed.entry.kind === 'role' ? parsed.entry.role : null).toBe('plugin/sales-team/write');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('is not a plugin token without a name, with a nested name, or with an unknown verb', () => {
|
|
26
|
+
for (const bad of ['plugin//read', 'plugin/GTM', 'plugin/GTM/foo/read', 'plugin/GTM/delete', 'role/GTM/read']) {
|
|
27
|
+
expect(canonicalPluginToken(bad)).toBeNull();
|
|
28
|
+
}
|
|
29
|
+
// A non-plugin role still canonicalises the old way.
|
|
30
|
+
expect(canonicalRoleName(' Sales Team ')).toBe('sales team');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('accepts as a canonical key only what it would itself generate', () => {
|
|
34
|
+
expect(parsePluginPrincipalKey('plugin/gtm/read')).toEqual({ slug: 'gtm', verb: 'read' });
|
|
35
|
+
expect(parsePluginPrincipalKey('plugin/gtm/foo/read')).toBeNull();
|
|
36
|
+
expect(parsePluginPrincipalKey('plugin/GTM/read')).toBeNull(); // not a slug
|
|
37
|
+
expect(parsePluginPrincipalKey('plugin/gtm/delete')).toBeNull();
|
|
38
|
+
expect(parsePluginPrincipalKey('plugin//read')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { parse as parseFullYaml } from 'yaml';
|
|
15
|
+
import { pluginManifestName } from '@bevel-software/platform-shared';
|
|
15
16
|
import type { GroupsIndex } from './group-files.js';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -86,6 +87,73 @@ export const GROUP_REF_PREFIX = 'group:';
|
|
|
86
87
|
*/
|
|
87
88
|
export const ROLE_TOKEN_PREFIX = 'role/';
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* PLUGIN-principal token prefix in access.md entries: `plugin/<Name>/<verb>`
|
|
92
|
+
* names everyone who holds `<verb>` on the plugin folder `Plugins/<Name>` —
|
|
93
|
+
* `plugin/GTM/read` is the GTM plugin's members, `plugin/GTM/write` its
|
|
94
|
+
* managers. Membership is DERIVED at model-load time from the plugin's own
|
|
95
|
+
* `access.md` (see `plugin-principals.ts`), so a grant naming one follows the
|
|
96
|
+
* plugin's roster with no copying. This is how a shared skill under `Skills/`
|
|
97
|
+
* is made visible to a plugin: `read: plugin/GTM/read` on the skill folder.
|
|
98
|
+
*
|
|
99
|
+
* The name half is canonicalised with the plugin's MANIFEST slug, so
|
|
100
|
+
* `plugin/Sales Team/read` and `plugin/sales-team/read` are one principal —
|
|
101
|
+
* the same identity a conformant client keys the plugin on. Reserved in the
|
|
102
|
+
* group name-safety rules like `role/`.
|
|
103
|
+
*/
|
|
104
|
+
export const PLUGIN_TOKEN_PREFIX = 'plugin/';
|
|
105
|
+
|
|
106
|
+
/** The verbs a plugin token may name — each is a distinct principal. */
|
|
107
|
+
export const PLUGIN_TOKEN_VERBS = ['read', 'write', 'owner'] as const;
|
|
108
|
+
export type PluginTokenVerb = (typeof PLUGIN_TOKEN_VERBS)[number];
|
|
109
|
+
|
|
110
|
+
/** The canonical key of a plugin principal, from the plugin's manifest slug. */
|
|
111
|
+
export function pluginPrincipalKey(slug: string, verb: PluginTokenVerb): string {
|
|
112
|
+
return `${PLUGIN_TOKEN_PREFIX}${slug}/${verb}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The parts of a CANONICAL plugin key, or null when the text is not one:
|
|
117
|
+
* exactly one separator after the prefix, a slug that is its own manifest
|
|
118
|
+
* slug, one of the three verbs. A generated key never carries a nested
|
|
119
|
+
* name, so `plugin/a/b/read` is a typo, not a principal.
|
|
120
|
+
*/
|
|
121
|
+
export function parsePluginPrincipalKey(
|
|
122
|
+
canonical: string,
|
|
123
|
+
): { slug: string; verb: PluginTokenVerb } | null {
|
|
124
|
+
if (!canonical.startsWith(PLUGIN_TOKEN_PREFIX)) return null;
|
|
125
|
+
const rest = canonical.slice(PLUGIN_TOKEN_PREFIX.length);
|
|
126
|
+
const cut = rest.indexOf('/');
|
|
127
|
+
if (cut <= 0 || rest.indexOf('/', cut + 1) !== -1) return null;
|
|
128
|
+
const slug = rest.slice(0, cut);
|
|
129
|
+
if (pluginManifestName(slug) !== slug) return null;
|
|
130
|
+
const verb = rest.slice(cut + 1);
|
|
131
|
+
if (!(PLUGIN_TOKEN_VERBS as readonly string[]).includes(verb)) return null;
|
|
132
|
+
return { slug, verb: verb as PluginTokenVerb };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The canonical key for ANY spelling of a plugin token — `plugin/Sales Team/read`,
|
|
137
|
+
* `Plugin/sales-team/READ` — or null when the text is not one: a name must be
|
|
138
|
+
* present and hold no further `/`, the verb must be one of the three. The ONE
|
|
139
|
+
* place a plugin's spelling folds to its slug: the access-file parser, the
|
|
140
|
+
* role canonicaliser (which grant and revoke compare through) and the link
|
|
141
|
+
* service all go through it, so a grant written one way is found again
|
|
142
|
+
* however it was spelled.
|
|
143
|
+
*/
|
|
144
|
+
export function canonicalPluginToken(token: string): string | null {
|
|
145
|
+
const body = token.trim();
|
|
146
|
+
if (!body.toLowerCase().startsWith(PLUGIN_TOKEN_PREFIX)) return null;
|
|
147
|
+
const rest = body.slice(PLUGIN_TOKEN_PREFIX.length).trim();
|
|
148
|
+
const cut = rest.lastIndexOf('/');
|
|
149
|
+
const name = cut > 0 ? rest.slice(0, cut).trim() : '';
|
|
150
|
+
const verb = cut > 0 ? rest.slice(cut + 1).trim().toLowerCase() : '';
|
|
151
|
+
if (!name || name.includes('/') || !(PLUGIN_TOKEN_VERBS as readonly string[]).includes(verb)) return null;
|
|
152
|
+
const slug = pluginManifestName(name);
|
|
153
|
+
if (!slug) return null;
|
|
154
|
+
return pluginPrincipalKey(slug, verb as PluginTokenVerb);
|
|
155
|
+
}
|
|
156
|
+
|
|
89
157
|
export const USER_REF_REGEX = /^(.+?)\s+<\s*([^<>\s]+@[^<>\s]+)\s*>\s*$/;
|
|
90
158
|
export const EMAIL_REGEX = /^[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+$/;
|
|
91
159
|
|
|
@@ -224,9 +292,24 @@ export function hasAccessFrontmatterExtension(p: string): boolean {
|
|
|
224
292
|
export interface RolesIndex {
|
|
225
293
|
byCanonical: Map<
|
|
226
294
|
string,
|
|
227
|
-
{
|
|
295
|
+
{
|
|
296
|
+
displayName: string;
|
|
297
|
+
emails: Set<string>;
|
|
298
|
+
groupRefs?: Set<string>;
|
|
299
|
+
kind?: 'role' | 'group' | 'plugin';
|
|
300
|
+
/** For `kind: 'plugin'`: the plugin's identity (its manifest name) the principal derives from. */
|
|
301
|
+
pluginName?: string;
|
|
302
|
+
/** For `kind: 'plugin'`: the repo-relative plugin directory, e.g. `Plugins/GTM`. */
|
|
303
|
+
pluginDir?: string;
|
|
304
|
+
}
|
|
228
305
|
>;
|
|
229
306
|
byEmail: Map<string, Set<string>>;
|
|
307
|
+
/**
|
|
308
|
+
* Principal keys EVERY caller holds, known or not — a plugin whose own
|
|
309
|
+
* rules grant `everyone` yields a plugin principal no email list can
|
|
310
|
+
* enumerate. The resolver unions these into every caller's key set.
|
|
311
|
+
*/
|
|
312
|
+
publicKeys?: Set<string>;
|
|
230
313
|
}
|
|
231
314
|
// ---------------------------------------------------------------------------
|
|
232
315
|
// Tiny YAML subset parser — handles only block mappings + block sequences
|
|
@@ -429,7 +512,9 @@ export function bodyAfterFrontmatter(text: string): string {
|
|
|
429
512
|
// ---------------------------------------------------------------------------
|
|
430
513
|
|
|
431
514
|
export function canonicalRoleName(name: string): string {
|
|
432
|
-
|
|
515
|
+
// A plugin token canonicalises to its KEY (slugged name), not to its
|
|
516
|
+
// lowercased spelling — see `canonicalPluginToken`.
|
|
517
|
+
return canonicalPluginToken(name) ?? name.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
433
518
|
}
|
|
434
519
|
|
|
435
520
|
export function canonicalEmail(email: string): string {
|
|
@@ -482,6 +567,23 @@ export function parseAccessEntry(
|
|
|
482
567
|
const suffix = canonicalRoleName(role.slice(ROLE_TOKEN_PREFIX.length));
|
|
483
568
|
if (!suffix) return { ok: false, error: `entry '${body}' names no role after '${ROLE_TOKEN_PREFIX}'` };
|
|
484
569
|
role = `${ROLE_TOKEN_PREFIX}${suffix}`;
|
|
570
|
+
} else if (role.startsWith(PLUGIN_TOKEN_PREFIX)) {
|
|
571
|
+
// `plugin/<Name>/<verb>`: the name is canonicalised to the plugin's
|
|
572
|
+
// manifest slug, the verb must be one of the three. Anything else is a
|
|
573
|
+
// parse error naming the valid shapes — a token that silently resolved to
|
|
574
|
+
// nothing would be a grant nobody gets and nobody is told about.
|
|
575
|
+
const key = canonicalPluginToken(body);
|
|
576
|
+
// Exactly one separator: `plugin/GTM/foo/read` is a typo, not a grant to
|
|
577
|
+
// some plugin whose slug happens to fold `GTM/foo` into `gtm-foo`.
|
|
578
|
+
if (key === null) {
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
error:
|
|
582
|
+
`entry '${body}' is not a plugin principal — write it as ` +
|
|
583
|
+
`${PLUGIN_TOKEN_PREFIX}<plugin>/read, ${PLUGIN_TOKEN_PREFIX}<plugin>/write or ${PLUGIN_TOKEN_PREFIX}<plugin>/owner`,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
role = key;
|
|
485
587
|
}
|
|
486
588
|
return { ok: true, entry: { kind: 'role', role, displayRole: body, deny } };
|
|
487
589
|
}
|
|
@@ -529,6 +631,16 @@ export function parseRolesYaml(
|
|
|
529
631
|
);
|
|
530
632
|
continue;
|
|
531
633
|
}
|
|
634
|
+
// A role spelled `plugin/…` would be overwritten by the synthesised plugin
|
|
635
|
+
// principal of the same key while its members kept the token — plugin
|
|
636
|
+
// access for people who are not plugin members. Refused at parse time,
|
|
637
|
+
// like the group name-safety rule.
|
|
638
|
+
if (canonical.startsWith(PLUGIN_TOKEN_PREFIX)) {
|
|
639
|
+
errors.push(
|
|
640
|
+
`roles.yaml: role '${displayName}' starts with the reserved '${PLUGIN_TOKEN_PREFIX}' prefix — that spelling is the plugin-principal token in access entries`,
|
|
641
|
+
);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
532
644
|
if (index.byCanonical.has(canonical)) {
|
|
533
645
|
const prev = index.byCanonical.get(canonical)!.displayName;
|
|
534
646
|
errors.push(
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
EMAIL_REGEX,
|
|
3
3
|
GROUP_REF_PREFIX,
|
|
4
4
|
RESERVED_ROLE_NAMES,
|
|
5
|
+
PLUGIN_TOKEN_PREFIX,
|
|
5
6
|
ROLE_TOKEN_PREFIX,
|
|
6
7
|
canonicalEmail,
|
|
7
8
|
canonicalRoleName,
|
|
@@ -68,6 +69,9 @@ export function unsafeNameReason(displayName: string): string | null {
|
|
|
68
69
|
if (canonicalRoleName(trimmed).startsWith(ROLE_TOKEN_PREFIX)) {
|
|
69
70
|
return `starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix (the explicit role token in access entries)`;
|
|
70
71
|
}
|
|
72
|
+
if (canonicalRoleName(trimmed).startsWith(PLUGIN_TOKEN_PREFIX)) {
|
|
73
|
+
return `starts with the reserved '${PLUGIN_TOKEN_PREFIX}' prefix (the plugin-principal token in access entries)`;
|
|
74
|
+
}
|
|
71
75
|
return null;
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { comparePathComponents } from '../../shared/path-order.js';
|
|
2
|
+
import {
|
|
3
|
+
EVERYONE_CANONICAL,
|
|
4
|
+
PLUGIN_TOKEN_PREFIX,
|
|
5
|
+
PLUGIN_TOKEN_VERBS,
|
|
6
|
+
pluginPrincipalKey,
|
|
7
|
+
type AccessFile,
|
|
8
|
+
type PluginTokenVerb,
|
|
9
|
+
type RolesIndex,
|
|
10
|
+
type Verb,
|
|
11
|
+
} from './access-grammar.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Plugin principals — `plugin/<Name>/<verb>` — synthesised into the principal
|
|
15
|
+
* index from each plugin folder's own `access.md`, so a grant naming one
|
|
16
|
+
* resolves exactly like a group grant: the resolver sees a named principal
|
|
17
|
+
* with member emails and needs no plugin awareness at all.
|
|
18
|
+
*
|
|
19
|
+
* WHAT A PLUGIN'S ROSTER IS: the folder-governing rules of
|
|
20
|
+
* `Plugins/<Name>/access.md` — the file provisioning seeds and the join flow
|
|
21
|
+
* edits. Three principals per plugin, following the resolver's own superset
|
|
22
|
+
* convention (write implies read, owner implies both):
|
|
23
|
+
*
|
|
24
|
+
* plugin/<slug>/read everyone under read, write or owner
|
|
25
|
+
* plugin/<slug>/write everyone under write or owner
|
|
26
|
+
* plugin/<slug>/owner everyone under owner
|
|
27
|
+
*
|
|
28
|
+
* The `<slug>` is the manifest slug of the folder (`pluginManifestName`), the
|
|
29
|
+
* identity `parseAccessEntry` canonicalises a hand-written token to.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately NOT membership:
|
|
32
|
+
* - the file's frontmatter (`read: everyone` there is DISCOVERABILITY of
|
|
33
|
+
* the access.md itself, not membership) — `AccessFile.entries` already
|
|
34
|
+
* holds only the folder block;
|
|
35
|
+
* - rules inherited from above the plugin folder (the repo root's
|
|
36
|
+
* `write: Admin`) — admins reach the skill through those same rules
|
|
37
|
+
* anyway, and the roster should be what the plugin's page shows;
|
|
38
|
+
* - `deny` entries — a denial subtracts nothing here; a plugin that wants
|
|
39
|
+
* someone out removes the grant;
|
|
40
|
+
* - other plugin tokens inside a plugin's rules — skipped, so synthesis is
|
|
41
|
+
* one pass with no recursion and no cycle to detect.
|
|
42
|
+
*
|
|
43
|
+
* `everyone` in a plugin's own rules makes the corresponding principal
|
|
44
|
+
* PUBLIC: no email list can enumerate it, so its key is recorded in
|
|
45
|
+
* `index.publicKeys` and the resolver unions those into every caller's set.
|
|
46
|
+
*
|
|
47
|
+
* Runs AFTER groups are merged (so role and group entries expand through the
|
|
48
|
+
* finished index) and BEFORE unknown-role validation (so plugin tokens in
|
|
49
|
+
* access files count as known).
|
|
50
|
+
*/
|
|
51
|
+
export function synthesizePluginPrincipals(
|
|
52
|
+
index: RolesIndex,
|
|
53
|
+
accessFiles: ReadonlyMap<string, AccessFile>,
|
|
54
|
+
/**
|
|
55
|
+
* The repo-relative folders that ARE plugins — the ones carrying a
|
|
56
|
+
* `plugin.json` — each with its IDENTITY (the manifest's name, or the
|
|
57
|
+
* folder folded into one), as the model loader read them. A plugin is a
|
|
58
|
+
* folder with a manifest, at any depth; its access.md is its roster.
|
|
59
|
+
* Nothing else's is.
|
|
60
|
+
*/
|
|
61
|
+
pluginDirs: ReadonlyMap<string, string>,
|
|
62
|
+
): void {
|
|
63
|
+
const claimed = new Set<string>();
|
|
64
|
+
// The same path order discovery walks in, so "first by path" is the same
|
|
65
|
+
// plugin in both places — see `comparePathComponents`.
|
|
66
|
+
for (const [dir, slug] of [...pluginDirs].sort(([a], [b]) => comparePathComponents(a, b))) {
|
|
67
|
+
// Two plugins with one name: discovery keeps the first by path, so do we —
|
|
68
|
+
// and the claim is made BEFORE looking for a roster, or a first plugin
|
|
69
|
+
// without rules would leave its slug to a later twin's roster.
|
|
70
|
+
if (claimed.has(slug)) continue;
|
|
71
|
+
claimed.add(slug);
|
|
72
|
+
const file = accessFiles.get(dir);
|
|
73
|
+
if (!file) continue; // a plugin without rules has no roster
|
|
74
|
+
for (const verb of PLUGIN_TOKEN_VERBS) {
|
|
75
|
+
const { emails, everyone } = holdersOf(index, file, verb);
|
|
76
|
+
const key = pluginPrincipalKey(slug, verb);
|
|
77
|
+
index.byCanonical.set(key, {
|
|
78
|
+
displayName: key,
|
|
79
|
+
emails,
|
|
80
|
+
kind: 'plugin',
|
|
81
|
+
pluginName: slug,
|
|
82
|
+
pluginDir: dir,
|
|
83
|
+
});
|
|
84
|
+
for (const email of emails) {
|
|
85
|
+
let set = index.byEmail.get(email);
|
|
86
|
+
if (!set) {
|
|
87
|
+
set = new Set();
|
|
88
|
+
index.byEmail.set(email, set);
|
|
89
|
+
}
|
|
90
|
+
set.add(key);
|
|
91
|
+
}
|
|
92
|
+
if (everyone) {
|
|
93
|
+
index.publicKeys ??= new Set();
|
|
94
|
+
index.publicKeys.add(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The verbs whose grants confer `verb` on the plugin (the superset fold). */
|
|
101
|
+
function sourceVerbsFor(verb: PluginTokenVerb): Verb[] {
|
|
102
|
+
switch (verb) {
|
|
103
|
+
case 'read':
|
|
104
|
+
return ['read', 'write', 'owner'];
|
|
105
|
+
case 'write':
|
|
106
|
+
return ['write', 'owner'];
|
|
107
|
+
case 'owner':
|
|
108
|
+
return ['owner'];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function holdersOf(
|
|
113
|
+
index: RolesIndex,
|
|
114
|
+
file: AccessFile,
|
|
115
|
+
verb: PluginTokenVerb,
|
|
116
|
+
): { emails: Set<string>; everyone: boolean } {
|
|
117
|
+
const emails = new Set<string>();
|
|
118
|
+
let everyone = false;
|
|
119
|
+
for (const source of sourceVerbsFor(verb)) {
|
|
120
|
+
for (const entry of file.entries[source]) {
|
|
121
|
+
if (entry.deny) continue;
|
|
122
|
+
if (entry.kind === 'user') {
|
|
123
|
+
emails.add(entry.email);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (entry.role === EVERYONE_CANONICAL) {
|
|
127
|
+
everyone = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (entry.role.startsWith(PLUGIN_TOKEN_PREFIX)) continue; // no recursion
|
|
131
|
+
const record = index.byCanonical.get(entry.role);
|
|
132
|
+
if (!record) continue; // an unknown role contributes nothing (loadModel warns separately)
|
|
133
|
+
for (const email of record.emails) emails.add(email);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { emails, everyone };
|
|
137
|
+
}
|
|
@@ -183,6 +183,12 @@ export const externalApiKeys = pgTable('api_tokens', {
|
|
|
183
183
|
userId: uuid('user_id').notNull().references(() => users.id),
|
|
184
184
|
tokenHash: text('token_hash').notNull().unique(),
|
|
185
185
|
label: text('label').notNull(),
|
|
186
|
+
/**
|
|
187
|
+
* What the key was minted as: `key` by hand, or a flow's own kind (a
|
|
188
|
+
* Claude link). The one fact that tells such keys apart — the label is
|
|
189
|
+
* free text the person may edit or imitate.
|
|
190
|
+
*/
|
|
191
|
+
kind: text('kind').default('key').notNull(),
|
|
186
192
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
187
193
|
lastUsedAt: timestamp('last_used_at'),
|
|
188
194
|
revokedAt: timestamp('revoked_at'),
|
|
@@ -490,3 +496,51 @@ export const deploymentSettings = pgTable('deployment_settings', {
|
|
|
490
496
|
updatedBy: uuid('updated_by').references(() => users.id, { onDelete: 'set null' }),
|
|
491
497
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
492
498
|
});
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* What this deployment presents to claude.ai as a "GitHub Enterprise Server"
|
|
502
|
+
* so that Cowork and claude.ai can add the per-user marketplace: the app id,
|
|
503
|
+
* client id and secrets an Owner pastes into Claude's admin settings. ONE row,
|
|
504
|
+
* generated on first use, replaced whole on rotate. Secrets are sealed with
|
|
505
|
+
* the secrets key, as stored settings are — see
|
|
506
|
+
* `marketplace/github-facade/github-facade-credentials.service.ts`.
|
|
507
|
+
*/
|
|
508
|
+
export const githubFacadeIdentity = pgTable('github_facade_identity', {
|
|
509
|
+
id: text('id').primaryKey(),
|
|
510
|
+
appId: text('app_id').notNull(),
|
|
511
|
+
clientId: text('client_id').notNull(),
|
|
512
|
+
/** Sealed. */
|
|
513
|
+
clientSecret: text('client_secret').notNull(),
|
|
514
|
+
/** Sealed. */
|
|
515
|
+
webhookSecret: text('webhook_secret').notNull(),
|
|
516
|
+
/** Sealed — PKCS#1 PEM. */
|
|
517
|
+
privateKeyPem: text('private_key_pem').notNull(),
|
|
518
|
+
publicKeyPem: text('public_key_pem').notNull(),
|
|
519
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
520
|
+
rotatedAt: timestamp('rotated_at'),
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* One-time codes the Claude connect flow issues on the consent page and
|
|
525
|
+
* Anthropic's backend exchanges seconds later — in the database so the
|
|
526
|
+
* replica that issued a code and the replica asked to exchange it agree.
|
|
527
|
+
*
|
|
528
|
+
* Keyed by the PERSON: "one live code per person" is the table's own rule,
|
|
529
|
+
* not a cleanup's. Issuing upserts their row — the newest code overwrites
|
|
530
|
+
* the last in one statement — so the table holds at most one row per user,
|
|
531
|
+
* for as long as the user exists (the row goes with them), and nothing ever
|
|
532
|
+
* has to sweep it. The client id is data the exchange checks, not part of
|
|
533
|
+
* the key: the bridge has one client, and a rotated client id must not leave
|
|
534
|
+
* a dead row behind under the old one. The code's hash is the unique lookup
|
|
535
|
+
* an exchange uses, spent by a conditional update. See
|
|
536
|
+
* `marketplace/github-facade/github-facade-codes.store.ts`.
|
|
537
|
+
*/
|
|
538
|
+
export const githubFacadeCodes = pgTable('github_facade_codes', {
|
|
539
|
+
userId: uuid('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
|
|
540
|
+
clientId: text('client_id').notNull(),
|
|
541
|
+
codeHash: text('code_hash').notNull().unique(),
|
|
542
|
+
redirectUri: text('redirect_uri').notNull(),
|
|
543
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
544
|
+
consumedAt: timestamp('consumed_at'),
|
|
545
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
546
|
+
});
|