@bevel-software/platform-core-backend 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +16 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +25 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +45 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts +19 -1
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +44 -4
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +54 -28
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +129 -14
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +435 -66
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts.map +1 -1
- package/dist/modules/access/access-declarations.js +5 -3
- package/dist/modules/access/access-declarations.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +39 -6
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +78 -18
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.d.ts +31 -4
- package/dist/modules/access/access-splice.d.ts.map +1 -1
- package/dist/modules/access/access-splice.js +40 -16
- 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 +204 -82
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/admin-locked-commit.d.ts +134 -0
- package/dist/modules/access/admin-locked-commit.d.ts.map +1 -0
- package/dist/modules/access/admin-locked-commit.js +277 -0
- package/dist/modules/access/admin-locked-commit.js.map +1 -0
- package/dist/modules/access/admin-route-helpers.d.ts +32 -0
- package/dist/modules/access/admin-route-helpers.d.ts.map +1 -0
- package/dist/modules/access/admin-route-helpers.js +44 -0
- package/dist/modules/access/admin-route-helpers.js.map +1 -0
- package/dist/modules/access/capability-registry.d.ts +41 -0
- package/dist/modules/access/capability-registry.d.ts.map +1 -0
- package/dist/modules/access/capability-registry.js +46 -0
- package/dist/modules/access/capability-registry.js.map +1 -0
- package/dist/modules/access/directory-sync-bot.d.ts +13 -0
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -0
- package/dist/modules/access/directory-sync-bot.js +64 -0
- package/dist/modules/access/directory-sync-bot.js.map +1 -0
- package/dist/modules/access/group-files.d.ts +83 -0
- package/dist/modules/access/group-files.d.ts.map +1 -0
- package/dist/modules/access/group-files.js +167 -0
- package/dist/modules/access/group-files.js.map +1 -0
- package/dist/modules/access/groups-admin.routes.d.ts +19 -0
- package/dist/modules/access/groups-admin.routes.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.routes.js +98 -0
- package/dist/modules/access/groups-admin.routes.js.map +1 -0
- package/dist/modules/access/groups-admin.service.d.ts +166 -0
- package/dist/modules/access/groups-admin.service.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.service.js +442 -0
- package/dist/modules/access/groups-admin.service.js.map +1 -0
- package/dist/modules/access/groups-edit.d.ts +58 -0
- package/dist/modules/access/groups-edit.d.ts.map +1 -0
- package/dist/modules/access/groups-edit.js +162 -0
- package/dist/modules/access/groups-edit.js.map +1 -0
- package/dist/modules/access/reference-scan.d.ts +141 -0
- package/dist/modules/access/reference-scan.d.ts.map +1 -0
- package/dist/modules/access/reference-scan.js +440 -0
- package/dist/modules/access/reference-scan.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +88 -119
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +230 -384
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access/roles-edit.d.ts +51 -25
- package/dist/modules/access/roles-edit.d.ts.map +1 -1
- package/dist/modules/access/roles-edit.js +133 -59
- package/dist/modules/access/roles-edit.js.map +1 -1
- package/dist/modules/access/synced-groups-committer.d.ts +28 -0
- package/dist/modules/access/synced-groups-committer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-committer.js +139 -0
- package/dist/modules/access/synced-groups-committer.js.map +1 -0
- package/dist/modules/access/synced-groups-writer.d.ts +78 -0
- package/dist/modules/access/synced-groups-writer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-writer.js +219 -0
- package/dist/modules/access/synced-groups-writer.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +17 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +9 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +13 -2
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +61 -2
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +9 -3
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +126 -2
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +14 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +6 -1
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/update-check/update-check.routes.d.ts +13 -0
- package/dist/modules/update-check/update-check.routes.d.ts.map +1 -0
- package/dist/modules/update-check/update-check.routes.js +21 -0
- package/dist/modules/update-check/update-check.routes.js.map +1 -0
- package/dist/modules/update-check/update-check.service.d.ts +57 -0
- package/dist/modules/update-check/update-check.service.d.ts.map +1 -0
- package/dist/modules/update-check/update-check.service.js +109 -0
- package/dist/modules/update-check/update-check.service.js.map +1 -0
- package/dist/modules/workflow/file-lock.service.d.ts +11 -1
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +15 -1
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +34 -11
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +173 -37
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/locking-filesystem.d.ts +4 -0
- package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/workflow/locking-filesystem.js +181 -28
- package/dist/modules/workflow/locking-filesystem.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +10 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +19 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +17 -2
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +113 -19
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/version.d.ts +14 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -1
- package/kb-template/AGENTS.md +4 -1
- package/migrations/0004_file_lock_mode.sql +2 -0
- package/migrations/meta/0004_snapshot.json +1564 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/src/__tests__/core-config.domain.test.ts +92 -0
- package/src/core/create-core-server.ts +21 -0
- package/src/core/create-core-services.ts +82 -0
- package/src/core-config.ts +48 -4
- package/src/index.ts +10 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +9 -3
- package/src/modules/access/__tests__/access-groups.test.ts +427 -0
- package/src/modules/access/__tests__/access-mutation.service.test.ts +171 -5
- package/src/modules/access/__tests__/access-splice.test.ts +65 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +337 -0
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +48 -0
- package/src/modules/access/__tests__/admin-locked-commit.test.ts +221 -0
- package/src/modules/access/__tests__/admin-route-helpers.test.ts +61 -0
- package/src/modules/access/__tests__/directory-sync-bot.test.ts +106 -0
- package/src/modules/access/__tests__/grant-sources.test.ts +67 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +440 -0
- package/src/modules/access/__tests__/reference-scan.test.ts +288 -0
- package/src/modules/access/__tests__/roles-admin.service.test.ts +161 -83
- package/src/modules/access/__tests__/roles-capabilities.test.ts +325 -0
- package/src/modules/access/__tests__/roles-edit.test.ts +104 -28
- package/src/modules/access/__tests__/roles.routes.test.ts +63 -40
- package/src/modules/access/__tests__/synced-groups-committer.test.ts +238 -0
- package/src/modules/access/__tests__/synced-groups-writer.test.ts +249 -0
- package/src/modules/access/access-control.interface.ts +66 -32
- package/src/modules/access/access-control.service.ts +536 -73
- package/src/modules/access/access-declarations.ts +5 -2
- package/src/modules/access/access-mutation.service.ts +88 -14
- package/src/modules/access/access-splice.ts +55 -17
- package/src/modules/access/access.routes.ts +227 -93
- package/src/modules/access/admin-locked-commit.ts +331 -0
- package/src/modules/access/admin-route-helpers.ts +55 -0
- package/src/modules/access/capability-registry.ts +74 -0
- package/src/modules/access/directory-sync-bot.ts +76 -0
- package/src/modules/access/group-files.ts +212 -0
- package/src/modules/access/groups-admin.routes.ts +113 -0
- package/src/modules/access/groups-admin.service.ts +551 -0
- package/src/modules/access/groups-edit.ts +187 -0
- package/src/modules/access/reference-scan.ts +513 -0
- package/src/modules/access/roles-admin.service.ts +290 -418
- package/src/modules/access/roles-edit.ts +134 -61
- package/src/modules/access/synced-groups-committer.ts +177 -0
- package/src/modules/access/synced-groups-writer.ts +303 -0
- package/src/modules/database/core-schema.ts +9 -0
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +116 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +6 -1
- package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +301 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +61 -1
- package/src/modules/mcp/mcp.routes.ts +137 -2
- package/src/modules/mcp/mcp.service.ts +6 -1
- package/src/modules/update-check/__tests__/update-check.routes.test.ts +69 -0
- package/src/modules/update-check/__tests__/update-check.service.test.ts +170 -0
- package/src/modules/update-check/update-check.routes.ts +28 -0
- package/src/modules/update-check/update-check.service.ts +130 -0
- package/src/modules/workflow/__tests__/locking-filesystem.test.ts +336 -0
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +32 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +139 -1
- package/src/modules/workflow/file-lock.service.ts +15 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +132 -0
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +0 -1
- package/src/modules/workflow/git/git.service.ts +182 -35
- package/src/modules/workflow/locking-filesystem.ts +188 -26
- package/src/modules/workflow/pending-commits.service.ts +27 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +0 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +0 -1
- package/src/modules/workflow/workflow.service.ts +140 -20
- package/src/version.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/platform-core-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@utcp/code-mode": "^1.2.12",
|
|
35
35
|
"@utcp/direct-call": "^1.1.1",
|
|
36
36
|
"@utcp/http": "^1.1.7",
|
|
37
|
-
"@utcp/mcp": "^1.1.
|
|
37
|
+
"@utcp/mcp": "^1.1.4",
|
|
38
38
|
"@utcp/sdk": "^1.1.1",
|
|
39
39
|
"adm-zip": "^0.6.0",
|
|
40
40
|
"cors": "^2.8.5",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"pg": "^8.20.0",
|
|
47
47
|
"yaml": "^2.9.0",
|
|
48
48
|
"zod": "^3.24.0",
|
|
49
|
-
"@bevel-software/platform-mcp-core": "0.
|
|
50
|
-
"@bevel-software/platform-shared": "0.
|
|
49
|
+
"@bevel-software/platform-mcp-core": "0.11.0",
|
|
50
|
+
"@bevel-software/platform-shared": "0.11.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CoreConfig } from '../core-config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The public-shape derivation from `DOMAIN`.
|
|
6
|
+
*
|
|
7
|
+
* Setting `DOMAIN` declares "the bundled Caddy `https` profile fronts this
|
|
8
|
+
* deployment": one proxy hop, origins = the domain. The config derives
|
|
9
|
+
* `PUBLIC_BACKEND_URL` / `PUBLIC_FRONTEND_URL` (`https://<DOMAIN>`) and
|
|
10
|
+
* `TRUST_PROXY` (`1`) from it — but only as DEFAULTS: an explicit value for
|
|
11
|
+
* any of the three always wins, so a CDN in front of Caddy or a frontend
|
|
12
|
+
* served elsewhere stays expressible.
|
|
13
|
+
*/
|
|
14
|
+
const REQUIRED = {
|
|
15
|
+
KB_REPO_URL: 'https://example.com/org/kb.git',
|
|
16
|
+
ADMIN_EMAIL: 'root@example.com',
|
|
17
|
+
ADMIN_PASSWORD: 'sup3r-secret',
|
|
18
|
+
JWT_SECRET: 'test-jwt-secret',
|
|
19
|
+
SECRETS_ENC_KEY: 'kToAi8FXWDpDn3A6yQ/60O39bv05N7XzVOIu/0CJrFc=',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let saved: NodeJS.ProcessEnv;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
saved = { ...process.env };
|
|
26
|
+
// A clean slate for the keys under test (a developer .env loaded by
|
|
27
|
+
// dotenv at import time may carry any of them).
|
|
28
|
+
for (const key of [
|
|
29
|
+
'DOMAIN',
|
|
30
|
+
'PUBLIC_BACKEND_URL',
|
|
31
|
+
'PUBLIC_FRONTEND_URL',
|
|
32
|
+
'TRUST_PROXY',
|
|
33
|
+
'NODE_ENV',
|
|
34
|
+
'PORT',
|
|
35
|
+
]) {
|
|
36
|
+
delete process.env[key];
|
|
37
|
+
}
|
|
38
|
+
Object.assign(process.env, REQUIRED);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
process.env = saved;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('CoreConfig — DOMAIN derives the public shape', () => {
|
|
46
|
+
it('DOMAIN alone yields https origins and one proxy hop', () => {
|
|
47
|
+
process.env.DOMAIN = 'bevel.example.com';
|
|
48
|
+
const config = new CoreConfig();
|
|
49
|
+
expect(config.publicBackendUrl).toBe('https://bevel.example.com');
|
|
50
|
+
expect(config.publicFrontendUrl).toBe('https://bevel.example.com');
|
|
51
|
+
expect(config.trustProxy).toBe('1');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('explicit PUBLIC_* and TRUST_PROXY always win over DOMAIN', () => {
|
|
55
|
+
// The CDN-in-front-of-Caddy shape: two hops, and a frontend served from
|
|
56
|
+
// somewhere the bundled proxy is not.
|
|
57
|
+
process.env.DOMAIN = 'bevel.example.com';
|
|
58
|
+
process.env.PUBLIC_BACKEND_URL = 'https://api.example.com';
|
|
59
|
+
process.env.PUBLIC_FRONTEND_URL = 'https://app.example.com';
|
|
60
|
+
process.env.TRUST_PROXY = '2';
|
|
61
|
+
const config = new CoreConfig();
|
|
62
|
+
expect(config.publicBackendUrl).toBe('https://api.example.com');
|
|
63
|
+
expect(config.publicFrontendUrl).toBe('https://app.example.com');
|
|
64
|
+
expect(config.trustProxy).toBe('2');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('without DOMAIN the defaults are unchanged (dev shape)', () => {
|
|
68
|
+
const config = new CoreConfig();
|
|
69
|
+
expect(config.publicBackendUrl).toBe('http://localhost:3001');
|
|
70
|
+
expect(config.publicFrontendUrl).toBe('http://localhost:5173');
|
|
71
|
+
// Unset means "directly exposed": forwarded headers stay ignored.
|
|
72
|
+
expect(config.trustProxy).toBe('');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('without DOMAIN in production, the frontend origin is the backend origin', () => {
|
|
76
|
+
// The backend serves the built SPA — under docker compose (which sets
|
|
77
|
+
// NODE_ENV=production and no PUBLIC_* by default) this is what keeps a
|
|
78
|
+
// bare `up -d` bouncing logins back to the container's own origin.
|
|
79
|
+
process.env.NODE_ENV = 'production';
|
|
80
|
+
const config = new CoreConfig();
|
|
81
|
+
expect(config.publicBackendUrl).toBe('http://localhost:3001');
|
|
82
|
+
expect(config.publicFrontendUrl).toBe('http://localhost:3001');
|
|
83
|
+
expect(config.trustProxy).toBe('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('an explicit production backend origin carries over to the frontend default', () => {
|
|
87
|
+
process.env.NODE_ENV = 'production';
|
|
88
|
+
process.env.PUBLIC_BACKEND_URL = 'https://bevel.example.com';
|
|
89
|
+
const config = new CoreConfig();
|
|
90
|
+
expect(config.publicFrontendUrl).toBe('https://bevel.example.com');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
createSecretsVaultPublicRoutes,
|
|
29
29
|
} from '../modules/secrets-vault/index.js';
|
|
30
30
|
import { createAdminAccessRoutes } from '../modules/admin/admin-access.routes.js';
|
|
31
|
+
import { createGroupsAdminRoutes } from '../modules/access/groups-admin.routes.js';
|
|
32
|
+
import { createUpdateCheckRoutes } from '../modules/update-check/update-check.routes.js';
|
|
31
33
|
import { createAccountRoutes } from '../modules/auth/account.routes.js';
|
|
32
34
|
import { createSetupRoutes } from '../modules/settings/setup.routes.js';
|
|
33
35
|
import { DEFAULT_BRANCH, PROTECTED_BRANCHES, type AuthUser } from '@bevel-software/platform-shared';
|
|
@@ -265,6 +267,11 @@ export async function createCoreServer(
|
|
|
265
267
|
core.mcpAuthMiddleware,
|
|
266
268
|
core.authMiddleware,
|
|
267
269
|
core.usageMeter,
|
|
270
|
+
// The local-token exchange: verifies an MCP OAuth access token and mints
|
|
271
|
+
// the loopback internal token the local MCP server uses for its REST reads.
|
|
272
|
+
core.internalTokenService,
|
|
273
|
+
core.mcpOAuthProvider,
|
|
274
|
+
core.mcpResourceMetadataUrl,
|
|
268
275
|
));
|
|
269
276
|
|
|
270
277
|
// MCP OAuth 2.1 authorization server: /authorize, /token, /register,
|
|
@@ -426,6 +433,20 @@ export async function createCoreServer(
|
|
|
426
433
|
// Admin-status resolver (CORE — see the note in admin-access.routes.ts;
|
|
427
434
|
// the full admin router is an enterprise `ext.authed` extension).
|
|
428
435
|
app.use('/api', core.authMiddleware, createAdminAccessRoutes(core.adminAccess));
|
|
436
|
+
// Groups admin (manual-mode CRUD; typed refusals in IdP mode) —
|
|
437
|
+
// admin-gated inside.
|
|
438
|
+
app.use('/api', core.authMiddleware, createGroupsAdminRoutes({
|
|
439
|
+
groupsAdmin: core.groupsAdminService,
|
|
440
|
+
adminAccess: core.adminAccess,
|
|
441
|
+
getUserById: async (id) => (await core.authService.getUserById(id)) ?? null,
|
|
442
|
+
}));
|
|
443
|
+
// Update check (admin-only inside): the newest published release vs the
|
|
444
|
+
// running version, cached server-side — see update-check.service.ts.
|
|
445
|
+
app.use(
|
|
446
|
+
'/api',
|
|
447
|
+
core.authMiddleware,
|
|
448
|
+
createUpdateCheckRoutes(core.updateCheckService, core.adminAccess),
|
|
449
|
+
);
|
|
429
450
|
// Account management (list/create password accounts, GDPR erasure) —
|
|
430
451
|
// admin-gated inside.
|
|
431
452
|
app.use('/api', core.authMiddleware, createAccountRoutes(
|
|
@@ -35,6 +35,7 @@ import { OidcAuthProvider } from '../modules/auth/oidc-auth-provider.js';
|
|
|
35
35
|
import { createAuthMiddleware } from '../modules/auth/auth.middleware.js';
|
|
36
36
|
import { AccessControlService } from '../modules/access/access-control.service.js';
|
|
37
37
|
import { CreatorAccessService } from '../modules/access/creator-access.js';
|
|
38
|
+
import { GroupsAdminService } from '../modules/access/groups-admin.service.js';
|
|
38
39
|
import { PendingSkillsService, SkillService } from '../modules/skills/index.js';
|
|
39
40
|
import { ToolManualService } from '../modules/tool-manuals/index.js';
|
|
40
41
|
import { McpServerEditService } from '../modules/tool-manuals/mcp-server-edit.service.js';
|
|
@@ -63,6 +64,12 @@ import {
|
|
|
63
64
|
} from '../modules/workflow/pending-commits.worker.js';
|
|
64
65
|
import { ensureRecoveryBotUser } from '../modules/workflow/recovery-bot.js';
|
|
65
66
|
import { AdminAccessService } from '../modules/admin/admin-access.service.js';
|
|
67
|
+
import {
|
|
68
|
+
SyncedGroupsWriter,
|
|
69
|
+
type SyncedGroupsSource,
|
|
70
|
+
} from '../modules/access/synced-groups-writer.js';
|
|
71
|
+
import { createSyncedGroupsCommitter } from '../modules/access/synced-groups-committer.js';
|
|
72
|
+
import { ensureDirectorySyncBot } from '../modules/access/directory-sync-bot.js';
|
|
66
73
|
import { ExternalApiKeyService } from '../modules/tool-auth/external-api-key.service.js';
|
|
67
74
|
import {
|
|
68
75
|
InternalTokenService,
|
|
@@ -82,6 +89,8 @@ import { ToolRegistry } from '../modules/tool-registry/tool-registry.js';
|
|
|
82
89
|
import { createToolContextResolver } from '../modules/tool-helpers/tool-context.js';
|
|
83
90
|
import { createToolHandlerFactory } from '../modules/tool-helpers/tool-handler.js';
|
|
84
91
|
import { TokenCrypto } from '../shared/token-crypto.js';
|
|
92
|
+
import { UpdateCheckService } from '../modules/update-check/update-check.service.js';
|
|
93
|
+
import { resolveAppVersion } from '../version.js';
|
|
85
94
|
import { noopRecoveryAgent, type CorePorts } from './core-ports.js';
|
|
86
95
|
|
|
87
96
|
/**
|
|
@@ -131,6 +140,22 @@ export interface CoreServices {
|
|
|
131
140
|
pendingCommitsWorker: PendingCommitsWorker;
|
|
132
141
|
recoveryBot: AuthUser;
|
|
133
142
|
adminAccess: AdminAccessService;
|
|
143
|
+
/** Manual-mode groups CRUD + the manual→IdP retirement half. */
|
|
144
|
+
groupsAdminService: GroupsAdminService;
|
|
145
|
+
/**
|
|
146
|
+
* Build the debounced directory → `synced-groups.yaml` materializer for a
|
|
147
|
+
* directory source an OVERLAY provides (e.g. a SCIM mirror fed by the IdP's
|
|
148
|
+
* provisioning engine — core ships no directory integration of its own).
|
|
149
|
+
* Ensures the directory-sync bot user and closes over the git commit
|
|
150
|
+
* pipeline; the overlay only implements {@link SyncedGroupsSource} and
|
|
151
|
+
* calls `notifyMutation()` after each directory change.
|
|
152
|
+
*/
|
|
153
|
+
createSyncedGroupsMaterializer: (
|
|
154
|
+
source: SyncedGroupsSource,
|
|
155
|
+
opts?: { debounceMs?: number; log?: (message: string) => void },
|
|
156
|
+
) => Promise<SyncedGroupsWriter>;
|
|
157
|
+
/** Newest-release lookup behind `GET /api/update-check` (admin-only). */
|
|
158
|
+
updateCheckService: UpdateCheckService;
|
|
134
159
|
/** Deployment settings, env-first — the KB remote resolves through these. */
|
|
135
160
|
settings: DeploymentSettingsService;
|
|
136
161
|
/**
|
|
@@ -147,6 +172,12 @@ export interface CoreServices {
|
|
|
147
172
|
mcpService: McpService;
|
|
148
173
|
mcpAuthMiddleware: ReturnType<typeof createMcpAuthMiddleware>;
|
|
149
174
|
mcpOAuthProvider: BevelOAuthProvider;
|
|
175
|
+
/**
|
|
176
|
+
* RFC 9728 protected-resource-metadata URL carried on MCP 401 challenges.
|
|
177
|
+
* Published so the route layer's own challenges (the local-token exchange)
|
|
178
|
+
* advertise the same pointer the auth middleware does.
|
|
179
|
+
*/
|
|
180
|
+
mcpResourceMetadataUrl: string;
|
|
150
181
|
toolRegistry: ToolRegistry;
|
|
151
182
|
toolAuthMiddleware: ReturnType<typeof createToolAuthMiddleware>;
|
|
152
183
|
manualAuthMiddleware: ReturnType<typeof createManualAuthMiddleware>;
|
|
@@ -499,6 +530,15 @@ export async function createCoreServices(
|
|
|
499
530
|
[config.adminEmail],
|
|
500
531
|
);
|
|
501
532
|
|
|
533
|
+
// In-app update check: lazily compares the running release version against
|
|
534
|
+
// the newest published GitHub release, only when an admin's browser asks —
|
|
535
|
+
// no timers, so a deployment nobody looks at makes zero calls. The flag
|
|
536
|
+
// removes even that (air-gapped deployments; see CoreConfig).
|
|
537
|
+
const updateCheckService = new UpdateCheckService({
|
|
538
|
+
enabled: config.updateCheckEnabled,
|
|
539
|
+
currentVersion: resolveAppVersion(),
|
|
540
|
+
});
|
|
541
|
+
|
|
502
542
|
// Secrets Vault: the per-user store of credentials (static API keys + OAuth
|
|
503
543
|
// tokens) that back UTCP tool variables (`${FOO_API_KEY}`). Encrypted at rest
|
|
504
544
|
// with the connector-config key; read only by the `bevel-secrets` variable
|
|
@@ -614,6 +654,7 @@ export async function createCoreServices(
|
|
|
614
654
|
externalApiKeyService,
|
|
615
655
|
mcpOAuthProvider,
|
|
616
656
|
mcpResourceMetadataUrl,
|
|
657
|
+
internalTokenService,
|
|
617
658
|
);
|
|
618
659
|
|
|
619
660
|
// ── Unified tool surface ──────────────────────────────────────────────
|
|
@@ -712,6 +753,43 @@ export async function createCoreServices(
|
|
|
712
753
|
);
|
|
713
754
|
}
|
|
714
755
|
|
|
756
|
+
// Materializer factory for an overlay-provided directory source: every
|
|
757
|
+
// provisioning burst regenerates `synced-groups.yaml` on the default branch
|
|
758
|
+
// (debounced), committed by the sync bot — that file is what puts access
|
|
759
|
+
// resolution in IdP mode. Core itself ships no directory integration, so
|
|
760
|
+
// nothing is constructed here until an overlay brings a source.
|
|
761
|
+
const createSyncedGroupsMaterializer = async (
|
|
762
|
+
source: SyncedGroupsSource,
|
|
763
|
+
opts?: { debounceMs?: number; log?: (message: string) => void },
|
|
764
|
+
): Promise<SyncedGroupsWriter> => {
|
|
765
|
+
const bot = await ensureDirectorySyncBot(db);
|
|
766
|
+
return new SyncedGroupsWriter({
|
|
767
|
+
source,
|
|
768
|
+
...createSyncedGroupsCommitter({
|
|
769
|
+
workspaceService,
|
|
770
|
+
workflowService,
|
|
771
|
+
accessControl,
|
|
772
|
+
eventBus,
|
|
773
|
+
kbDirName,
|
|
774
|
+
bot,
|
|
775
|
+
defaultBranchOf: () => DEFAULT_BRANCH,
|
|
776
|
+
}),
|
|
777
|
+
debounceMs: opts?.debounceMs,
|
|
778
|
+
log: opts?.log ?? ((message) => console.warn(message)),
|
|
779
|
+
});
|
|
780
|
+
};
|
|
781
|
+
// Groups — the "who you are" principal sets. Manual-mode CRUD on
|
|
782
|
+
// groups.yaml, IdP-mode refusals, and the connect-time retirement the
|
|
783
|
+
// directory token route drives.
|
|
784
|
+
const groupsAdminService = new GroupsAdminService(
|
|
785
|
+
workspaceService,
|
|
786
|
+
workflowService,
|
|
787
|
+
accessControl,
|
|
788
|
+
kbDirName,
|
|
789
|
+
() => DEFAULT_BRANCH,
|
|
790
|
+
eventBus,
|
|
791
|
+
);
|
|
792
|
+
|
|
715
793
|
return {
|
|
716
794
|
config,
|
|
717
795
|
db,
|
|
@@ -746,12 +824,16 @@ export async function createCoreServices(
|
|
|
746
824
|
pendingCommitsWorker,
|
|
747
825
|
recoveryBot,
|
|
748
826
|
adminAccess,
|
|
827
|
+
groupsAdminService,
|
|
828
|
+
createSyncedGroupsMaterializer,
|
|
829
|
+
updateCheckService,
|
|
749
830
|
secretsVaultService,
|
|
750
831
|
externalApiKeyService,
|
|
751
832
|
internalTokenService,
|
|
752
833
|
mcpService,
|
|
753
834
|
mcpAuthMiddleware,
|
|
754
835
|
mcpOAuthProvider,
|
|
836
|
+
mcpResourceMetadataUrl,
|
|
755
837
|
toolRegistry,
|
|
756
838
|
toolAuthMiddleware,
|
|
757
839
|
manualAuthMiddleware,
|
package/src/core-config.ts
CHANGED
|
@@ -144,6 +144,16 @@ export class CoreConfig {
|
|
|
144
144
|
* `session-ontology.gate.ts`), so it must exist wherever the tracking runs.
|
|
145
145
|
*/
|
|
146
146
|
readonly ontologySessionBlock: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* In-app update check. When true (default), `GET /api/update-check` lazily
|
|
149
|
+
* asks api.github.com for the newest Hexis release — only when an admin's
|
|
150
|
+
* browser asks, cached for hours, never on a timer — so admins see a quiet
|
|
151
|
+
* banner when this deployment is behind. The request carries nothing but
|
|
152
|
+
* the request itself: no token, no identifier, no telemetry. Set
|
|
153
|
+
* `UPDATE_CHECK=false` for air-gapped deployments or anyone who objects to
|
|
154
|
+
* the phone-home; disabled, the server never makes the call.
|
|
155
|
+
*/
|
|
156
|
+
readonly updateCheckEnabled: boolean;
|
|
147
157
|
/**
|
|
148
158
|
* Password-login toggle for the login screen. Default enabled; set
|
|
149
159
|
* `LOGIN_PASSWORD=false` to hide the method AND reject `/auth/login`
|
|
@@ -203,14 +213,22 @@ export class CoreConfig {
|
|
|
203
213
|
* but behind a proxy it makes every client share the proxy's IP (so the
|
|
204
214
|
* per-IP login rate limit would pool all users). Set it to the actual hop
|
|
205
215
|
* count — never a blanket trust — so clients can't spoof X-Forwarded-For.
|
|
216
|
+
* With `DOMAIN` set (the bundled Caddy fronts the deployment) it defaults
|
|
217
|
+
* to `1` instead — see the derivation in the constructor.
|
|
206
218
|
*/
|
|
207
219
|
readonly trustProxy: string;
|
|
208
220
|
/**
|
|
209
221
|
* Public base URL of THIS backend, used to build OAuth redirect URIs.
|
|
210
222
|
* Must match a redirect URI registered with the OAuth provider(s).
|
|
223
|
+
* Defaults to `https://<DOMAIN>` when `DOMAIN` is set.
|
|
211
224
|
*/
|
|
212
225
|
readonly publicBackendUrl: string;
|
|
213
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Public base URL of the frontend, where callbacks redirect post-login.
|
|
228
|
+
* Defaults to `https://<DOMAIN>` when `DOMAIN` is set, else to the backend
|
|
229
|
+
* origin in production (the backend serves the SPA) and Vite's `:5173` in
|
|
230
|
+
* development.
|
|
231
|
+
*/
|
|
214
232
|
readonly publicFrontendUrl: string;
|
|
215
233
|
|
|
216
234
|
constructor() {
|
|
@@ -304,6 +322,8 @@ export class CoreConfig {
|
|
|
304
322
|
this.kbTemplateDir = process.env.KB_TEMPLATE_DIR || defaultKbTemplateDir();
|
|
305
323
|
this.ontologySessionBlock =
|
|
306
324
|
(process.env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false';
|
|
325
|
+
this.updateCheckEnabled =
|
|
326
|
+
(process.env.UPDATE_CHECK ?? 'true').trim().toLowerCase() !== 'false';
|
|
307
327
|
this.allowedEmailDomains = (process.env.ALLOWED_EMAIL_DOMAINS || '')
|
|
308
328
|
.split(/[\s,]+/)
|
|
309
329
|
.map((d) => d.trim().toLowerCase().replace(/^[@.]+/, ''))
|
|
@@ -322,11 +342,35 @@ export class CoreConfig {
|
|
|
322
342
|
);
|
|
323
343
|
}
|
|
324
344
|
this.internalTokenSecret = (process.env.INTERNAL_TOKEN_SECRET || '').trim();
|
|
325
|
-
|
|
326
|
-
|
|
345
|
+
// Setting DOMAIN declares "the bundled Caddy `https` profile fronts this
|
|
346
|
+
// deployment" — one proxy hop, and the public origin IS that domain. The
|
|
347
|
+
// three values below therefore default from it, so `DOMAIN=x.example.com`
|
|
348
|
+
// in `.env` is the whole configuration for that shape: TRUST_PROXY falls
|
|
349
|
+
// to 1 (Caddy is the hop) and both public URLs to `https://<DOMAIN>` (the
|
|
350
|
+
// backend serves the SPA, so they share an origin). Explicit
|
|
351
|
+
// PUBLIC_BACKEND_URL / PUBLIC_FRONTEND_URL / TRUST_PROXY always win — a
|
|
352
|
+
// CDN in front of Caddy (TRUST_PROXY=2) or a frontend served elsewhere
|
|
353
|
+
// stays expressible.
|
|
354
|
+
const domain = (process.env.DOMAIN || '').trim();
|
|
355
|
+
this.trustProxy = (process.env.TRUST_PROXY || (domain ? '1' : '')).trim();
|
|
356
|
+
this.publicBackendUrl = (
|
|
357
|
+
process.env.PUBLIC_BACKEND_URL ||
|
|
358
|
+
(domain ? `https://${domain}` : `http://localhost:${this.port}`)
|
|
359
|
+
)
|
|
327
360
|
.trim()
|
|
328
361
|
.replace(/\/+$/, '');
|
|
329
|
-
|
|
362
|
+
// Unset, the frontend origin is the backend's own in production (the
|
|
363
|
+
// backend serves the built SPA — under docker compose this is what makes
|
|
364
|
+
// a bare `up -d` bounce logins back to the right place), and Vite's dev
|
|
365
|
+
// server in development.
|
|
366
|
+
this.publicFrontendUrl = (
|
|
367
|
+
process.env.PUBLIC_FRONTEND_URL ||
|
|
368
|
+
(domain
|
|
369
|
+
? `https://${domain}`
|
|
370
|
+
: this.nodeEnv === 'production'
|
|
371
|
+
? this.publicBackendUrl
|
|
372
|
+
: 'http://localhost:5173')
|
|
373
|
+
)
|
|
330
374
|
.trim()
|
|
331
375
|
.replace(/\/+$/, '');
|
|
332
376
|
// Parse-validate so a malformed URL fails at boot rather than producing a
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,16 @@ export type {
|
|
|
52
52
|
} from './modules/tool-auth/external-api-key.interface.js';
|
|
53
53
|
|
|
54
54
|
// Key port/seam types an overlay implements.
|
|
55
|
+
// SyncedGroupsWriter is a CLASS — exported by VALUE so an overlay can
|
|
56
|
+
// construct one; the seam types (including the writer's deps) stay type-only.
|
|
57
|
+
export { SyncedGroupsWriter } from './modules/access/synced-groups-writer.js';
|
|
58
|
+
export type {
|
|
59
|
+
SyncedGroupsSource,
|
|
60
|
+
SyncedGroupRecord,
|
|
61
|
+
SyncedGroupMember,
|
|
62
|
+
SyncedGroupsWriteResult,
|
|
63
|
+
SyncedGroupsWriterDeps,
|
|
64
|
+
} from './modules/access/synced-groups-writer.js';
|
|
55
65
|
export type { ISessionSink } from './modules/workspace/session-sink.js';
|
|
56
66
|
export type {
|
|
57
67
|
ISystemNoticeSink,
|
|
@@ -283,6 +283,12 @@ describe('AccessControlService', () => {
|
|
|
283
283
|
const e = await svc.eligibleWriters(workspaceId, 'Knowledge/Foo.md');
|
|
284
284
|
expect(e.roles).toEqual(['Admin']);
|
|
285
285
|
expect(e.users.map((u) => u.email)).toEqual(['felix@example.com']);
|
|
286
|
+
// The kinded twin of `roles`: a roles.yaml principal reads kind 'role'.
|
|
287
|
+
expect(e.principals).toEqual([{ name: 'Admin', kind: 'role' }]);
|
|
288
|
+
// The Admin-override insertion (write on an access.md is admin-rescued)
|
|
289
|
+
// also carries kind 'role' — it is the role's capability, never a group's.
|
|
290
|
+
const onAccessMd = await svc.eligibleWriters(workspaceId, 'access.md');
|
|
291
|
+
expect(onAccessMd.principals).toContainEqual({ name: 'Admin', kind: 'role' });
|
|
286
292
|
});
|
|
287
293
|
|
|
288
294
|
it('throws AccessConfigError when roles.yaml is missing', async () => {
|
|
@@ -947,7 +953,7 @@ describe('AccessControlService', () => {
|
|
|
947
953
|
|
|
948
954
|
const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR);
|
|
949
955
|
const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Foo.md');
|
|
950
|
-
expect(e).toEqual({ restricted: true, roles: [], users: [] });
|
|
956
|
+
expect(e).toEqual({ restricted: true, principals: [], roles: [], users: [] });
|
|
951
957
|
});
|
|
952
958
|
|
|
953
959
|
it('reports restricted=false when read: everyone applies', async () => {
|
|
@@ -957,7 +963,7 @@ describe('AccessControlService', () => {
|
|
|
957
963
|
|
|
958
964
|
const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR);
|
|
959
965
|
const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Foo.md');
|
|
960
|
-
expect(e).toEqual({ restricted: false, roles: [], users: [] });
|
|
966
|
+
expect(e).toEqual({ restricted: false, principals: [], roles: [], users: [] });
|
|
961
967
|
});
|
|
962
968
|
|
|
963
969
|
it('restricted=false when a closer everyone grant shadows a farther by-name deny', async () => {
|
|
@@ -972,7 +978,7 @@ describe('AccessControlService', () => {
|
|
|
972
978
|
// node really is readable by everyone — not restricted.
|
|
973
979
|
expect(await svc.canRead(workspaceId, 'felix@example.com', 'Knowledge/Open/Foo.md')).toBe(true);
|
|
974
980
|
const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Open/Foo.md');
|
|
975
|
-
expect(e).toEqual({ restricted: false, roles: [], users: [] });
|
|
981
|
+
expect(e).toEqual({ restricted: false, principals: [], roles: [], users: [] });
|
|
976
982
|
});
|
|
977
983
|
|
|
978
984
|
it('restricted=true when a same-scope by-name deny carves someone out of read: everyone', async () => {
|