@bevel-software/platform-core-backend 0.4.1 → 0.5.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 +6118 -0
- package/dist/core/core-ports.d.ts +15 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +40 -6
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +17 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +153 -29
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts +70 -21
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +134 -58
- package/dist/core-config.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +6 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +74 -5
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +116 -15
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +93 -0
- package/dist/modules/access/access-declarations.d.ts.map +1 -0
- package/dist/modules/access/access-declarations.js +168 -0
- package/dist/modules/access/access-declarations.js.map +1 -0
- package/dist/modules/access/access-mutation.service.js +4 -4
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.d.ts +16 -1
- package/dist/modules/access/access-splice.d.ts.map +1 -1
- package/dist/modules/access/access-splice.js +40 -21
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +51 -0
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +8 -0
- package/dist/modules/access/creator-access.d.ts.map +1 -1
- package/dist/modules/access/creator-access.js +21 -2
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/admin/admin-access.service.d.ts +1 -1
- package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
- package/dist/modules/admin/admin-access.service.js +3 -2
- package/dist/modules/admin/admin-access.service.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +50 -1
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +39 -10
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +38 -16
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +124 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +45 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/groups/group-provision.service.d.ts +130 -0
- package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
- package/dist/modules/groups/group-provision.service.js +288 -0
- package/dist/modules/groups/group-provision.service.js.map +1 -0
- package/dist/modules/groups/groups.contract.d.ts +106 -0
- package/dist/modules/groups/groups.contract.d.ts.map +1 -0
- package/dist/modules/groups/groups.contract.js +36 -0
- package/dist/modules/groups/groups.contract.js.map +1 -0
- package/dist/modules/groups/groups.routes.d.ts +42 -0
- package/dist/modules/groups/groups.routes.d.ts.map +1 -0
- package/dist/modules/groups/groups.routes.js +379 -0
- package/dist/modules/groups/groups.routes.js.map +1 -0
- package/dist/modules/groups/groups.service.d.ts +60 -0
- package/dist/modules/groups/groups.service.d.ts.map +1 -0
- package/dist/modules/groups/groups.service.js +172 -0
- package/dist/modules/groups/groups.service.js.map +1 -0
- package/dist/modules/groups/index.d.ts +7 -0
- package/dist/modules/groups/index.d.ts.map +1 -0
- package/dist/modules/groups/index.js +6 -0
- package/dist/modules/groups/index.js.map +1 -0
- package/dist/modules/groups/join-proposals.d.ts +53 -0
- package/dist/modules/groups/join-proposals.d.ts.map +1 -0
- package/dist/modules/groups/join-proposals.js +67 -0
- package/dist/modules/groups/join-proposals.js.map +1 -0
- package/dist/modules/groups/join-requests.service.d.ts +81 -0
- package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
- package/dist/modules/groups/join-requests.service.js +135 -0
- package/dist/modules/groups/join-requests.service.js.map +1 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
- package/dist/modules/settings/deployment-settings.service.js +368 -0
- package/dist/modules/settings/deployment-settings.service.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +49 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -0
- package/dist/modules/settings/setup.routes.js +292 -0
- package/dist/modules/settings/setup.routes.js.map +1 -0
- package/dist/modules/skills/index.d.ts +2 -1
- 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 +40 -0
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
- package/dist/modules/skills/pending-skills.service.js +147 -0
- package/dist/modules/skills/pending-skills.service.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +32 -3
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.routes.d.ts +3 -2
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +18 -1
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +22 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +24 -12
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +1 -1
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/event-bus.d.ts +11 -0
- package/dist/modules/workflow/event-bus.d.ts.map +1 -1
- package/dist/modules/workflow/event-bus.js +22 -0
- package/dist/modules/workflow/event-bus.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +9 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +16 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/branch-name.d.ts +1 -1
- package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
- package/dist/modules/workflow/git/branch-name.js +1 -1
- package/dist/modules/workflow/git/branch-name.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +31 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +144 -52
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +19 -9
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +95 -14
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +92 -3
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +381 -108
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
- package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
- package/dist/modules/workspace/bevel-ignore.js +5 -1
- package/dist/modules/workspace/bevel-ignore.js.map +1 -1
- package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +228 -51
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts +2 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +51 -18
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +16 -4
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +36 -9
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +8 -3
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/kb-layout.test.js +2 -2
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +18 -0
- package/dist/test-setup.js.map +1 -0
- package/kb-template/.bevelignore +6 -2
- package/kb-template/AGENTS.md +272 -0
- package/kb-template/KnowledgeBase/How to get started.md +65 -0
- package/kb-template/access.md +36 -36
- package/migrations/0002_user_onboarding_done.sql +1 -0
- package/migrations/0003_deployment_settings.sql +9 -0
- package/migrations/meta/0001_snapshot.json +1486 -0
- package/migrations/meta/0002_snapshot.json +1493 -0
- package/migrations/meta/0003_snapshot.json +1552 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +5 -4
- package/src/__tests__/branch-model.test.ts +79 -0
- package/src/__tests__/core-config.admin.test.ts +114 -0
- package/src/__tests__/database-url.test.ts +70 -0
- package/src/core/core-ports.ts +15 -0
- package/src/core/create-core-server.ts +55 -6
- package/src/core/create-core-services.ts +201 -36
- package/src/core-config.ts +139 -59
- package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
- package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
- package/src/modules/access/__tests__/access-splice.test.ts +21 -0
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
- package/src/modules/access/access-control.interface.ts +11 -0
- package/src/modules/access/access-control.service.ts +113 -15
- package/src/modules/access/access-declarations.ts +251 -0
- package/src/modules/access/access-mutation.service.ts +4 -4
- package/src/modules/access/access-splice.ts +64 -21
- package/src/modules/access/access.routes.ts +61 -0
- package/src/modules/access/creator-access.ts +22 -2
- package/src/modules/admin/admin-access.service.ts +4 -3
- package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
- package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
- package/src/modules/auth/auth.routes.ts +50 -1
- package/src/modules/auth/auth.service.ts +55 -17
- package/src/modules/database/core-schema.ts +46 -0
- package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
- package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
- package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
- package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
- package/src/modules/groups/group-provision.service.ts +328 -0
- package/src/modules/groups/groups.contract.ts +107 -0
- package/src/modules/groups/groups.routes.ts +433 -0
- package/src/modules/groups/groups.service.ts +193 -0
- package/src/modules/groups/index.ts +17 -0
- package/src/modules/groups/join-proposals.ts +114 -0
- package/src/modules/groups/join-requests.service.ts +185 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
- package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
- package/src/modules/settings/deployment-settings.service.ts +419 -0
- package/src/modules/settings/setup.routes.ts +317 -0
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/index.ts +3 -0
- package/src/modules/skills/pending-skills.service.ts +168 -0
- package/src/modules/skills/skills.contract.ts +34 -3
- package/src/modules/skills/skills.routes.ts +23 -2
- package/src/modules/skills/skills.service.ts +24 -16
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
- package/src/modules/tool-manuals/index.ts +2 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
- package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
- package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
- package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +251 -5
- package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
- package/src/modules/workflow/event-bus.ts +25 -0
- package/src/modules/workflow/file-lock.service.ts +17 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
- package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
- package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +5 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
- package/src/modules/workflow/git/branch-name.ts +5 -1
- package/src/modules/workflow/git/git.service.ts +159 -62
- package/src/modules/workflow/git/pull-request.service.ts +22 -11
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow.routes.ts +91 -13
- package/src/modules/workflow/workflow.service.ts +353 -13
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
- package/src/modules/workspace/bevel-ignore.ts +66 -62
- package/src/modules/workspace/kb-seed.interface.ts +1 -1
- package/src/modules/workspace/kb-seed.service.ts +247 -53
- package/src/modules/workspace/workspace.routes.ts +54 -18
- package/src/modules/workspace/workspace.service.ts +45 -7
- package/src/modules/workspace/workspace.tools.ts +922 -917
- package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
- package/src/shared/__tests__/join-request.test.ts +96 -0
- package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +2 -2
- package/src/test-setup.ts +18 -0
- package/kb-template/Agents/README.md +0 -38
- package/kb-template/CLAUDE.md +0 -419
- package/kb-template/Data/.gitkeep +0 -0
- package/kb-template/Data/README.md +0 -39
- package/kb-template/KnowledgeBase/README.md +0 -37
- package/kb-template/Pipelines/.gitkeep +0 -0
- package/kb-template/Pipelines/README.md +0 -38
- package/kb-template/Skills/.gitkeep +0 -0
- package/kb-template/Skills/README.md +0 -32
- package/kb-template/Tools/.gitkeep +0 -0
- package/kb-template/Tools/README.md +0 -42
- /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
package/src/core-config.ts
CHANGED
|
@@ -6,6 +6,35 @@ import { defaultKbTemplateDir } from './assets.js';
|
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The Postgres connection string: `DATABASE_URL` if given, otherwise built
|
|
11
|
+
* from the `POSTGRES_*` parts.
|
|
12
|
+
*
|
|
13
|
+
* The composition lives HERE rather than in docker-compose because expressing
|
|
14
|
+
* it there needed a nested default — `${DATABASE_URL:-postgresql://${POSTGRES_USER:-…}…}`
|
|
15
|
+
* — and deployment UIs that scan compose with a regex rather than a YAML
|
|
16
|
+
* parser truncate that at the first closing brace, offering the operator a
|
|
17
|
+
* variable whose value is the fragment `postgresql://${POSTGRES_USER:-bevel`.
|
|
18
|
+
* A malformed connection string presented as configuration is worse than no
|
|
19
|
+
* configuration at all.
|
|
20
|
+
*
|
|
21
|
+
* It is also the better home for it: the parts are URL-ENCODED, which shell
|
|
22
|
+
* interpolation cannot do — a password containing `@`, `/` or `:` silently
|
|
23
|
+
* produced an unparseable URL, and "check your password" is not the error
|
|
24
|
+
* anyone got.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDatabaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
27
|
+
const explicit = (env.DATABASE_URL || '').trim();
|
|
28
|
+
if (explicit) return explicit;
|
|
29
|
+
const user = encodeURIComponent(env.POSTGRES_USER || 'bevel');
|
|
30
|
+
const password = encodeURIComponent(env.POSTGRES_PASSWORD || 'bevel');
|
|
31
|
+
// `localhost` suits a local `pnpm dev`; compose passes the service name.
|
|
32
|
+
const host = (env.POSTGRES_HOST || 'localhost').trim();
|
|
33
|
+
const port = (env.POSTGRES_PORT || '5432').trim();
|
|
34
|
+
const database = encodeURIComponent(env.POSTGRES_DB || 'bevel');
|
|
35
|
+
return `postgresql://${user}:${password}@${host}:${port}/${database}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
9
38
|
/**
|
|
10
39
|
* Configuration for the CORE platform: the git-backed workspace/workflow,
|
|
11
40
|
* skills, tools, secrets vault, access control, and the MCP surface. Contains
|
|
@@ -44,16 +73,31 @@ export class CoreConfig {
|
|
|
44
73
|
readonly spillRoot: string;
|
|
45
74
|
readonly jwtSecret: string;
|
|
46
75
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
76
|
+
* The deployment's owner. REQUIRED — three separate jobs rest on it, and a
|
|
77
|
+
* deployment without one has no way to be administered:
|
|
78
|
+
*
|
|
79
|
+
* - always recognized as an admin (see AdminAccessService), whatever the
|
|
80
|
+
* sign-in method, so a KB whose `roles.yaml` does not list them yet is
|
|
81
|
+
* still administrable;
|
|
82
|
+
* - written as the sole Admin into the `roles.yaml` generated when an EMPTY
|
|
83
|
+
* KB remote is seeded (this replaced `SEED_ADMIN_EMAILS`, which asked for
|
|
84
|
+
* the same answer a second time);
|
|
85
|
+
* - half of the bootstrap password credential below.
|
|
55
86
|
*/
|
|
56
87
|
readonly adminEmail: string;
|
|
88
|
+
/**
|
|
89
|
+
* Password half of the bootstrap credential, checked directly against the
|
|
90
|
+
* environment at login time (never stored) so a fresh deployment can sign in
|
|
91
|
+
* before any account exists. Captured ONCE here, so changing or unsetting it
|
|
92
|
+
* takes effect on the next restart rather than at once — accounts in the
|
|
93
|
+
* database are unaffected either way.
|
|
94
|
+
*
|
|
95
|
+
* Required only when password login is ENABLED. An SSO-only deployment
|
|
96
|
+
* (`LOGIN_PASSWORD=false`) would otherwise have to mint a shared password
|
|
97
|
+
* the server is configured to reject — an unused credential someone still
|
|
98
|
+
* has to store and rotate. The admin identity above is required either way,
|
|
99
|
+
* so "there is always an identifiable admin" holds regardless.
|
|
100
|
+
*/
|
|
57
101
|
readonly adminPassword: string;
|
|
58
102
|
/**
|
|
59
103
|
* Generic OIDC single sign-on (any spec-compliant provider: Entra, Okta,
|
|
@@ -89,13 +133,6 @@ export class CoreConfig {
|
|
|
89
133
|
* existing one. Override with `KB_TEMPLATE_DIR`.
|
|
90
134
|
*/
|
|
91
135
|
readonly kbTemplateDir: string;
|
|
92
|
-
/**
|
|
93
|
-
* Admin emails written into the generated `roles.yaml` when a fresh KB repo is
|
|
94
|
-
* seeded. Comma/whitespace-separated. A freshly-seeded repo with no Admin is
|
|
95
|
-
* unusable (access resolution requires ≥1 Admin), so seeding an EMPTY remote
|
|
96
|
-
* fails fast when this is empty. Not needed once the remote is already seeded.
|
|
97
|
-
*/
|
|
98
|
-
readonly seedAdminEmails: string[];
|
|
99
136
|
/**
|
|
100
137
|
* Ontology-session boundary kill-switch. When true (default), an agent run
|
|
101
138
|
* that has read across more than one ontology can no longer write. A no-op on
|
|
@@ -115,10 +152,24 @@ export class CoreConfig {
|
|
|
115
152
|
*/
|
|
116
153
|
readonly loginPasswordEnabled: boolean;
|
|
117
154
|
/**
|
|
118
|
-
* Optional allow-list of email domains
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
155
|
+
* Optional allow-list of email domains for SSO — part of the OIDC
|
|
156
|
+
* configuration, and only meaningful alongside it.
|
|
157
|
+
*
|
|
158
|
+
* SSO AUTO-PROVISIONS: `loginWithSso` upserts the account on first sign-in,
|
|
159
|
+
* with no admin approval step. Against a single-tenant issuer the issuer is
|
|
160
|
+
* already the boundary and this is belt-and-braces; against a multi-tenant
|
|
161
|
+
* one (Google, the Entra `common` endpoint, Auth0 with social connections)
|
|
162
|
+
* it is the ONLY boundary — without it, anyone the issuer will authenticate
|
|
163
|
+
* provisions themselves into the deployment.
|
|
164
|
+
*
|
|
165
|
+
* Deliberately NOT applied to the other two entry points. An account an
|
|
166
|
+
* admin created is vetted by the act of creating it, and password login can
|
|
167
|
+
* only reach an account that already exists — so a check there would gate
|
|
168
|
+
* nothing while being able to lock out a bootstrap admin whose own address
|
|
169
|
+
* sits outside the list.
|
|
170
|
+
*
|
|
171
|
+
* Comma/whitespace-separated; empty (default) allows any email. Matches the
|
|
172
|
+
* domain exactly or as a subdomain, and a leading `@` or `.` is tolerated:
|
|
122
173
|
* `ALLOWED_EMAIL_DOMAINS=bevel.software, example.com` works as expected.
|
|
123
174
|
*/
|
|
124
175
|
readonly allowedEmailDomains: string[];
|
|
@@ -127,8 +178,12 @@ export class CoreConfig {
|
|
|
127
178
|
* secrets: the secrets-vault values and the MCP OAuth client secrets/tokens.
|
|
128
179
|
* Read from `SECRETS_ENC_KEY`, falling back to the legacy
|
|
129
180
|
* `CONNECTOR_CONFIG_ENC_KEY` → `SHAREPOINT_TOKEN_ENC_KEY` chain so existing
|
|
130
|
-
* deploys keep decrypting without re-keying.
|
|
131
|
-
*
|
|
181
|
+
* deploys keep decrypting without re-keying.
|
|
182
|
+
*
|
|
183
|
+
* REQUIRED. It used to be optional, with the failure surfacing only when
|
|
184
|
+
* somebody wrote a secret — which now includes the git token typed into the
|
|
185
|
+
* setup screen, so an unset key turns the first-run flow into a dead end at
|
|
186
|
+
* the last step. Refusing to boot names the variable instead.
|
|
132
187
|
*/
|
|
133
188
|
readonly secretsEncKey: string;
|
|
134
189
|
/**
|
|
@@ -160,7 +215,7 @@ export class CoreConfig {
|
|
|
160
215
|
|
|
161
216
|
constructor() {
|
|
162
217
|
this.port = parseInt(process.env.PORT || '3001', 10);
|
|
163
|
-
this.databaseUrl =
|
|
218
|
+
this.databaseUrl = resolveDatabaseUrl();
|
|
164
219
|
this.nodeEnv = process.env.NODE_ENV || 'development';
|
|
165
220
|
this.tenantId = (process.env.TENANT_ID || 'bevel').trim().toLowerCase();
|
|
166
221
|
if (!/^[a-z0-9]+$/.test(this.tenantId)) {
|
|
@@ -171,12 +226,42 @@ export class CoreConfig {
|
|
|
171
226
|
this.workspacesRoot = process.env.WORKSPACES_ROOT || path.resolve(process.cwd(), 'workspaces');
|
|
172
227
|
this.backupsRoot = process.env.BACKUPS_ROOT || path.resolve(this.workspacesRoot, '..', 'backups');
|
|
173
228
|
this.spillRoot = process.env.SPILL_ROOT || path.resolve(this.workspacesRoot, '..', 'tool-chain-spills');
|
|
174
|
-
|
|
229
|
+
// Required, and checked BEFORE the auth routes it signs for are ever
|
|
230
|
+
// mounted. Empty, the first login attempt fails deep inside the JWT library
|
|
231
|
+
// with a message about a missing key — a boot error naming the variable is
|
|
232
|
+
// the same information, hours earlier.
|
|
233
|
+
this.jwtSecret = (process.env.JWT_SECRET || '').trim();
|
|
234
|
+
if (!this.jwtSecret) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
'JWT_SECRET is required — it signs login sessions. Generate one with: ' +
|
|
237
|
+
`node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
// Parsed here rather than beside the other toggles below: whether the
|
|
241
|
+
// bootstrap PASSWORD is required depends on it.
|
|
242
|
+
this.loginPasswordEnabled =
|
|
243
|
+
(process.env.LOGIN_PASSWORD ?? 'true').trim().toLowerCase() !== 'false';
|
|
175
244
|
this.adminEmail = (process.env.ADMIN_EMAIL || '').trim().toLowerCase();
|
|
176
245
|
this.adminPassword = process.env.ADMIN_PASSWORD || '';
|
|
177
|
-
if (this.adminEmail
|
|
246
|
+
if (!this.adminEmail) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
'ADMIN_EMAIL is required — the address that owns this deployment. It is ' +
|
|
249
|
+
'always treated as an admin (whatever the sign-in method) and is written ' +
|
|
250
|
+
'as the initial Admin when an empty knowledge-base repo is seeded.',
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.adminEmail)) {
|
|
178
254
|
throw new Error(`ADMIN_EMAIL is not a valid email: "${this.adminEmail}"`);
|
|
179
255
|
}
|
|
256
|
+
// Only when password login can actually be used. An SSO-only deployment
|
|
257
|
+
// has no use for this value and should not have to hold one.
|
|
258
|
+
if (this.loginPasswordEnabled && !this.adminPassword) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'ADMIN_PASSWORD is required while password login is enabled — it is the ' +
|
|
261
|
+
'credential that signs in before any account exists. Set it, or set ' +
|
|
262
|
+
'LOGIN_PASSWORD=false if this deployment signs in through SSO only.',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
180
265
|
this.oidcIssuerUrl = (process.env.OIDC_ISSUER_URL || '').trim().replace(/\/+$/, '');
|
|
181
266
|
this.oidcClientId = (process.env.OIDC_CLIENT_ID || '').trim();
|
|
182
267
|
this.oidcClientSecret = (process.env.OIDC_CLIENT_SECRET || '').trim();
|
|
@@ -190,15 +275,14 @@ export class CoreConfig {
|
|
|
190
275
|
throw new Error(`OIDC_ISSUER_URL is not a valid URL: "${this.oidcIssuerUrl}"`);
|
|
191
276
|
}
|
|
192
277
|
}
|
|
193
|
-
// No default
|
|
194
|
-
//
|
|
278
|
+
// No default, and no longer fatal when absent: an unconfigured deployment
|
|
279
|
+
// boots into the setup screen, where an admin supplies it (and where the
|
|
280
|
+
// value can be verified against the real remote before it is saved). What
|
|
281
|
+
// stays fatal is a value that is present and WRONG — see
|
|
282
|
+
// `validateKbIdentity` — because that surfaces later as a confusing clone
|
|
283
|
+
// failure instead of a clear one now.
|
|
195
284
|
this.kbRepoUrl = (process.env.KB_REPO_URL || '').trim();
|
|
196
|
-
|
|
197
|
-
throw new Error(
|
|
198
|
-
'KB_REPO_URL is required — the https clone/push URL of the git repository that stores your knowledge base.',
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
this.kbDirName = process.env.KB_DIR_NAME || 'knowledge-base';
|
|
285
|
+
this.kbDirName = (process.env.KB_DIR_NAME || '').trim();
|
|
202
286
|
// Provider-neutral git token: operators can set GIT_TOKEN (or the legacy
|
|
203
287
|
// GITHUB_TOKEN / GH_TOKEN). Normalize onto GITHUB_TOKEN — the name the
|
|
204
288
|
// credential helper and every `$GITHUB_TOKEN` read + redaction use — so all
|
|
@@ -218,14 +302,8 @@ export class CoreConfig {
|
|
|
218
302
|
// Default: the `kb-template/` folder shipped inside this package (works
|
|
219
303
|
// both from src/ and compiled dist/ — see assets.ts).
|
|
220
304
|
this.kbTemplateDir = process.env.KB_TEMPLATE_DIR || defaultKbTemplateDir();
|
|
221
|
-
this.seedAdminEmails = (process.env.SEED_ADMIN_EMAILS || '')
|
|
222
|
-
.split(/[\s,]+/)
|
|
223
|
-
.map((e) => e.trim().toLowerCase())
|
|
224
|
-
.filter((e) => e.length > 0);
|
|
225
305
|
this.ontologySessionBlock =
|
|
226
306
|
(process.env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false';
|
|
227
|
-
this.loginPasswordEnabled =
|
|
228
|
-
(process.env.LOGIN_PASSWORD ?? 'true').trim().toLowerCase() !== 'false';
|
|
229
307
|
this.allowedEmailDomains = (process.env.ALLOWED_EMAIL_DOMAINS || '')
|
|
230
308
|
.split(/[\s,]+/)
|
|
231
309
|
.map((d) => d.trim().toLowerCase().replace(/^[@.]+/, ''))
|
|
@@ -236,6 +314,13 @@ export class CoreConfig {
|
|
|
236
314
|
process.env.SHAREPOINT_TOKEN_ENC_KEY ||
|
|
237
315
|
''
|
|
238
316
|
).trim();
|
|
317
|
+
if (!this.secretsEncKey) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
'SECRETS_ENC_KEY is required — it encrypts the secrets vault, the MCP OAuth ' +
|
|
320
|
+
'tokens and the git credential saved from the setup screen. Generate one ' +
|
|
321
|
+
`with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
239
324
|
this.internalTokenSecret = (process.env.INTERNAL_TOKEN_SECRET || '').trim();
|
|
240
325
|
this.trustProxy = (process.env.TRUST_PROXY || '').trim();
|
|
241
326
|
this.publicBackendUrl = (process.env.PUBLIC_BACKEND_URL || `http://localhost:${this.port}`)
|
|
@@ -258,14 +343,6 @@ export class CoreConfig {
|
|
|
258
343
|
}
|
|
259
344
|
}
|
|
260
345
|
|
|
261
|
-
// Validate format now so a typo surfaces at boot, not at first-seed time.
|
|
262
|
-
// Emptiness is NOT an error here: an already-seeded remote never needs it.
|
|
263
|
-
for (const email of this.seedAdminEmails) {
|
|
264
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
265
|
-
throw new Error(`SEED_ADMIN_EMAILS contains an invalid email: "${email}"`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
346
|
this.validateKbIdentity();
|
|
270
347
|
}
|
|
271
348
|
|
|
@@ -289,25 +366,28 @@ export class CoreConfig {
|
|
|
289
366
|
return `${this.tenantId}-mcp_`;
|
|
290
367
|
}
|
|
291
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Validate the KB-identity inputs THAT WERE SUPPLIED. Absent is fine — the
|
|
371
|
+
* setup screen collects them — but a present-and-malformed value fails at
|
|
372
|
+
* boot rather than later, as a clone error nobody can read.
|
|
373
|
+
*/
|
|
292
374
|
private validateKbIdentity(): void {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (parsed.protocol !== 'https:') {
|
|
304
|
-
throw new Error(`KB_REPO_URL must use https://: ${this.kbRepoUrl}`);
|
|
375
|
+
if (this.kbRepoUrl) {
|
|
376
|
+
let parsed: URL;
|
|
377
|
+
try {
|
|
378
|
+
parsed = new URL(this.kbRepoUrl);
|
|
379
|
+
} catch {
|
|
380
|
+
throw new Error(`KB_REPO_URL is not a valid URL: ${this.kbRepoUrl}`);
|
|
381
|
+
}
|
|
382
|
+
if (parsed.protocol !== 'https:') {
|
|
383
|
+
throw new Error(`KB_REPO_URL must use https://: ${this.kbRepoUrl}`);
|
|
384
|
+
}
|
|
305
385
|
}
|
|
306
386
|
|
|
307
387
|
// Joined with workspace paths via path.join — separators or ".." would let it
|
|
308
388
|
// escape the workspace dir.
|
|
389
|
+
if (!this.kbDirName) return;
|
|
309
390
|
if (
|
|
310
|
-
!this.kbDirName ||
|
|
311
391
|
this.kbDirName === '.' ||
|
|
312
392
|
this.kbDirName === '..' ||
|
|
313
393
|
this.kbDirName.includes('/') ||
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { FileTreeEntry, IWorkspaceService } from '@bevel-software/platform-shared';
|
|
3
|
+
import {
|
|
4
|
+
listAccessDeclarationsUnder,
|
|
5
|
+
AccessDeclarationsError,
|
|
6
|
+
DECLARATION_SCAN_FILE_CAP,
|
|
7
|
+
} from '../access-declarations.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The declaration scan: WHICH files under a folder declare access rules, and
|
|
11
|
+
* what those rules literally say. Everything here is about honesty of report —
|
|
12
|
+
* the folder's own summary must not be echoed back as an override, a broken
|
|
13
|
+
* rule file must be visible, and a typo'd node must not take the page down.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const WS = 'main';
|
|
17
|
+
const KB = 'knowledge-base';
|
|
18
|
+
|
|
19
|
+
/** Build the nested tree `listFiles` returns from a flat ws-path → text map. */
|
|
20
|
+
function buildTree(files: Record<string, string>): FileTreeEntry {
|
|
21
|
+
const root: FileTreeEntry = { name: '', relativePath: '', type: 'directory', children: [] };
|
|
22
|
+
for (const wsPath of Object.keys(files)) {
|
|
23
|
+
const segments = wsPath.split('/');
|
|
24
|
+
let node = root;
|
|
25
|
+
segments.forEach((segment, i) => {
|
|
26
|
+
const isLeaf = i === segments.length - 1;
|
|
27
|
+
const relativePath = segments.slice(0, i + 1).join('/');
|
|
28
|
+
let child = node.children?.find((c) => c.name === segment);
|
|
29
|
+
if (!child) {
|
|
30
|
+
child = {
|
|
31
|
+
name: segment,
|
|
32
|
+
relativePath,
|
|
33
|
+
type: isLeaf ? 'file' : 'directory',
|
|
34
|
+
...(isLeaf ? {} : { children: [] }),
|
|
35
|
+
};
|
|
36
|
+
node.children?.push(child);
|
|
37
|
+
}
|
|
38
|
+
node = child;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return root;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeWorkspace(files: Record<string, string>): IWorkspaceService {
|
|
45
|
+
return {
|
|
46
|
+
listFiles: async () => buildTree(files),
|
|
47
|
+
readFile: async (_id: string, relativePath: string) => {
|
|
48
|
+
const text = files[relativePath];
|
|
49
|
+
if (text === undefined) throw new Error(`ENOENT ${relativePath}`);
|
|
50
|
+
return text;
|
|
51
|
+
},
|
|
52
|
+
} as unknown as IWorkspaceService;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const scan = (files: Record<string, string>, folder = 'Groups/GTM') =>
|
|
56
|
+
listAccessDeclarationsUnder(makeWorkspace(files), WS, KB, folder);
|
|
57
|
+
|
|
58
|
+
const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
|
|
59
|
+
|
|
60
|
+
describe('listAccessDeclarationsUnder', () => {
|
|
61
|
+
it('reports a descendant access.md against the directory it governs', async () => {
|
|
62
|
+
const { overrides, truncated } = await scan({
|
|
63
|
+
[`${KB}/Groups/GTM/battlecards/access.md`]: fm('write:\n - GTM Team'),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(truncated).toBe(false);
|
|
67
|
+
expect(overrides).toEqual([
|
|
68
|
+
{
|
|
69
|
+
path: 'Groups/GTM/battlecards/access.md',
|
|
70
|
+
governs: 'Groups/GTM/battlecards',
|
|
71
|
+
source: 'access-md',
|
|
72
|
+
entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'GTM Team' } }],
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("excludes the target folder's own access.md — that is the summary, not an override", async () => {
|
|
78
|
+
const { overrides } = await scan({
|
|
79
|
+
[`${KB}/Groups/GTM/access.md`]: fm('read:\n - GTM Team'),
|
|
80
|
+
[`${KB}/Groups/GTM/battlecards/access.md`]: fm('read:\n - Developer'),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(overrides.map((o) => o.path)).toEqual(['Groups/GTM/battlecards/access.md']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('reads verbs out of SKILL.md and .tool frontmatter, governing the file itself', async () => {
|
|
87
|
+
const { overrides } = await scan({
|
|
88
|
+
[`${KB}/Groups/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\nowner: Ali <ali@bevel.software>'),
|
|
89
|
+
[`${KB}/Groups/GTM/slack.tool`]: fm('write:\n - Developer'),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(overrides).toEqual([
|
|
93
|
+
{
|
|
94
|
+
path: 'Groups/GTM/outreach/SKILL.md',
|
|
95
|
+
governs: 'Groups/GTM/outreach/SKILL.md',
|
|
96
|
+
source: 'frontmatter',
|
|
97
|
+
entries: [
|
|
98
|
+
{
|
|
99
|
+
verb: 'owner',
|
|
100
|
+
deny: false,
|
|
101
|
+
principal: { kind: 'user', email: 'ali@bevel.software', name: 'Ali' },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
path: 'Groups/GTM/slack.tool',
|
|
107
|
+
governs: 'Groups/GTM/slack.tool',
|
|
108
|
+
source: 'frontmatter',
|
|
109
|
+
entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'Developer' } }],
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('ignores nodes whose frontmatter declares no access verb', async () => {
|
|
115
|
+
const { overrides } = await scan({
|
|
116
|
+
[`${KB}/Groups/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\ndescription: Runs outreach'),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(overrides).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('preserves the deny prefix, collapses everyone, and carries user name + email', async () => {
|
|
123
|
+
const { overrides } = await scan({
|
|
124
|
+
[`${KB}/Groups/GTM/battlecards/access.md`]: fm(
|
|
125
|
+
'read:\n - everyone\n - deny Bob Ruiz <bob@bevel.software>\nwrite:\n - deny GTM Team',
|
|
126
|
+
),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(overrides[0].entries).toEqual([
|
|
130
|
+
{ verb: 'read', deny: false, principal: { kind: 'everyone' } },
|
|
131
|
+
{
|
|
132
|
+
verb: 'read',
|
|
133
|
+
deny: true,
|
|
134
|
+
principal: { kind: 'user', email: 'bob@bevel.software', name: 'Bob Ruiz' },
|
|
135
|
+
},
|
|
136
|
+
{ verb: 'write', deny: true, principal: { kind: 'role', role: 'GTM Team' } },
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('surfaces an unparseable access.md and silently skips an unparseable node', async () => {
|
|
141
|
+
const { overrides } = await scan({
|
|
142
|
+
// `read:` must be a LIST in an access.md — this file controls nothing.
|
|
143
|
+
[`${KB}/Groups/GTM/battlecards/access.md`]: fm('read: GTM Team'),
|
|
144
|
+
// Unterminated frontmatter on a node — forgiven, exactly like the resolver.
|
|
145
|
+
[`${KB}/Groups/GTM/outreach/SKILL.md`]: '---\nowner: Ali <ali@bevel.software>\n\n# Body\n',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(overrides).toHaveLength(1);
|
|
149
|
+
expect(overrides[0].path).toBe('Groups/GTM/battlecards/access.md');
|
|
150
|
+
expect(overrides[0].entries).toEqual([]);
|
|
151
|
+
expect(overrides[0].parseError).toContain("'read:' must be a list");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('stops at the file cap and says so', async () => {
|
|
155
|
+
const files: Record<string, string> = {};
|
|
156
|
+
for (let i = 0; i < DECLARATION_SCAN_FILE_CAP + 5; i++) {
|
|
157
|
+
const n = String(i).padStart(4, '0');
|
|
158
|
+
files[`${KB}/Groups/GTM/n${n}/SKILL.md`] = fm('read:\n - Developer');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { overrides, truncated } = await scan(files);
|
|
162
|
+
|
|
163
|
+
expect(truncated).toBe(true);
|
|
164
|
+
expect(overrides).toHaveLength(DECLARATION_SCAN_FILE_CAP);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns an empty result for a folder with nothing in it', async () => {
|
|
168
|
+
expect(await scan({ [`${KB}/Groups/Product/roadmap/SKILL.md`]: fm('read:\n - Developer') }))
|
|
169
|
+
.toEqual({ overrides: [], truncated: false });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('returns an empty result for a folder that is not in the clone', async () => {
|
|
173
|
+
expect(await scan({}, 'Groups/Nope')).toEqual({ overrides: [], truncated: false });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('refuses a target that is a file', async () => {
|
|
177
|
+
await expect(
|
|
178
|
+
scan({ [`${KB}/Groups/GTM/slack.tool`]: fm('write:\n - Developer') }, 'Groups/GTM/slack.tool'),
|
|
179
|
+
).rejects.toBeInstanceOf(AccessDeclarationsError);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('sorts rows by declaring path', async () => {
|
|
183
|
+
const { overrides } = await scan({
|
|
184
|
+
[`${KB}/Groups/GTM/zeta.tool`]: fm('write:\n - Developer'),
|
|
185
|
+
[`${KB}/Groups/GTM/alpha/access.md`]: fm('read:\n - Developer'),
|
|
186
|
+
[`${KB}/Groups/GTM/mid/SKILL.md`]: fm('owner:\n - Developer'),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(overrides.map((o) => o.path)).toEqual([
|
|
190
|
+
'Groups/GTM/alpha/access.md',
|
|
191
|
+
'Groups/GTM/mid/SKILL.md',
|
|
192
|
+
'Groups/GTM/zeta.tool',
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
});
|