@bevel-software/platform-core-backend 0.7.5 → 0.8.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/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +51 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +5 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +30 -18
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.js +19 -19
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +18 -4
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +14 -4
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- 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.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- 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 +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +26 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +83 -12
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +106 -106
- package/src/core/create-core-server.ts +55 -9
- package/src/core/create-core-services.ts +40 -20
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +20 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +13 -2
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- 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 +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/kb-seed.service.ts +38 -9
- package/src/modules/workspace/plugins-migration.ts +479 -0
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +85 -14
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import express from 'express';
|
|
3
|
-
import type { Server } from 'node:http';
|
|
4
|
-
import { createAuthRoutes } from '../auth.routes.js';
|
|
5
|
-
import type { AuthService } from '../auth.service.js';
|
|
6
|
-
|
|
7
|
-
const authService = {
|
|
8
|
-
loginWithPassword: vi.fn(async () => {
|
|
9
|
-
throw new Error('Invalid credentials');
|
|
10
|
-
}),
|
|
11
|
-
changePassword: vi.fn(async () => {
|
|
12
|
-
throw new Error('Current password is incorrect');
|
|
13
|
-
}),
|
|
14
|
-
getUserById: vi.fn(async () => null),
|
|
15
|
-
} as unknown as AuthService;
|
|
16
|
-
|
|
17
|
-
const fakeAuthMiddleware: express.RequestHandler = (req, _res, next) => {
|
|
18
|
-
req.userId = 'user-1';
|
|
19
|
-
req.userEmail = 'alice@example.com';
|
|
20
|
-
next();
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
let server: Server;
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
vi.mocked(authService.loginWithPassword).mockClear();
|
|
26
|
-
});
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
server?.close();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
async function listen(): Promise<string> {
|
|
32
|
-
const app = express();
|
|
33
|
-
app.use(express.json());
|
|
34
|
-
app.use('/api', createAuthRoutes(authService, fakeAuthMiddleware));
|
|
35
|
-
await new Promise<void>((resolve) => {
|
|
36
|
-
server = app.listen(0, resolve);
|
|
37
|
-
});
|
|
38
|
-
const address = server.address();
|
|
39
|
-
if (typeof address === 'string' || !address) throw new Error('no port');
|
|
40
|
-
return `http://127.0.0.1:${address.port}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function postLogin(base: string, email: string, password = 'wrong') {
|
|
44
|
-
return fetch(`${base}/api/auth/login`, {
|
|
45
|
-
method: 'POST',
|
|
46
|
-
headers: { 'Content-Type': 'application/json' },
|
|
47
|
-
body: JSON.stringify({ email, password }),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('auth routes — rate limiting', () => {
|
|
52
|
-
it('429s login for an (ip, email) pair after repeated failures; other emails unaffected', async () => {
|
|
53
|
-
const base = await listen();
|
|
54
|
-
for (let i = 0; i < 10; i++) {
|
|
55
|
-
expect((await postLogin(base, 'alice@example.com')).status).toBe(401);
|
|
56
|
-
}
|
|
57
|
-
const limited = await postLogin(base, 'alice@example.com');
|
|
58
|
-
expect(limited.status).toBe(429);
|
|
59
|
-
// The service is not even consulted once the limit is hit.
|
|
60
|
-
expect(authService.loginWithPassword).toHaveBeenCalledTimes(10);
|
|
61
|
-
// A different email from the same address still has pair budget (the
|
|
62
|
-
// wider per-ip budget is 50).
|
|
63
|
-
expect((await postLogin(base, 'bob@example.com')).status).toBe(401);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('a successful login resets the pair budget', async () => {
|
|
67
|
-
const base = await listen();
|
|
68
|
-
for (let i = 0; i < 9; i++) await postLogin(base, 'alice@example.com');
|
|
69
|
-
vi.mocked(authService.loginWithPassword).mockResolvedValueOnce({
|
|
70
|
-
token: 'jwt',
|
|
71
|
-
user: { id: 'user-1', email: 'alice@example.com', name: 'Alice' },
|
|
72
|
-
});
|
|
73
|
-
expect((await postLogin(base, 'alice@example.com', 'right')).status).toBe(200);
|
|
74
|
-
// Budget is fresh again — failures start counting from zero.
|
|
75
|
-
expect((await postLogin(base, 'alice@example.com')).status).toBe(401);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('429s change-password per account after repeated failures', async () => {
|
|
79
|
-
const base = await listen();
|
|
80
|
-
const post = () =>
|
|
81
|
-
fetch(`${base}/api/auth/change-password`, {
|
|
82
|
-
method: 'POST',
|
|
83
|
-
headers: { 'Content-Type': 'application/json' },
|
|
84
|
-
body: JSON.stringify({ currentPassword: 'wrong', newPassword: 'new-password-1' }),
|
|
85
|
-
});
|
|
86
|
-
for (let i = 0; i < 10; i++) {
|
|
87
|
-
expect((await post()).status).toBe(400);
|
|
88
|
-
}
|
|
89
|
-
expect((await post()).status).toBe(429);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import type { Server } from 'node:http';
|
|
4
|
+
import { createAuthRoutes } from '../auth.routes.js';
|
|
5
|
+
import type { AuthService } from '../auth.service.js';
|
|
6
|
+
|
|
7
|
+
const authService = {
|
|
8
|
+
loginWithPassword: vi.fn(async () => {
|
|
9
|
+
throw new Error('Invalid credentials');
|
|
10
|
+
}),
|
|
11
|
+
changePassword: vi.fn(async () => {
|
|
12
|
+
throw new Error('Current password is incorrect');
|
|
13
|
+
}),
|
|
14
|
+
getUserById: vi.fn(async () => null),
|
|
15
|
+
} as unknown as AuthService;
|
|
16
|
+
|
|
17
|
+
const fakeAuthMiddleware: express.RequestHandler = (req, _res, next) => {
|
|
18
|
+
req.userId = 'user-1';
|
|
19
|
+
req.userEmail = 'alice@example.com';
|
|
20
|
+
next();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let server: Server;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.mocked(authService.loginWithPassword).mockClear();
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
server?.close();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function listen(): Promise<string> {
|
|
32
|
+
const app = express();
|
|
33
|
+
app.use(express.json());
|
|
34
|
+
app.use('/api', createAuthRoutes(authService, fakeAuthMiddleware));
|
|
35
|
+
await new Promise<void>((resolve) => {
|
|
36
|
+
server = app.listen(0, resolve);
|
|
37
|
+
});
|
|
38
|
+
const address = server.address();
|
|
39
|
+
if (typeof address === 'string' || !address) throw new Error('no port');
|
|
40
|
+
return `http://127.0.0.1:${address.port}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function postLogin(base: string, email: string, password = 'wrong') {
|
|
44
|
+
return fetch(`${base}/api/auth/login`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({ email, password }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('auth routes — rate limiting', () => {
|
|
52
|
+
it('429s login for an (ip, email) pair after repeated failures; other emails unaffected', async () => {
|
|
53
|
+
const base = await listen();
|
|
54
|
+
for (let i = 0; i < 10; i++) {
|
|
55
|
+
expect((await postLogin(base, 'alice@example.com')).status).toBe(401);
|
|
56
|
+
}
|
|
57
|
+
const limited = await postLogin(base, 'alice@example.com');
|
|
58
|
+
expect(limited.status).toBe(429);
|
|
59
|
+
// The service is not even consulted once the limit is hit.
|
|
60
|
+
expect(authService.loginWithPassword).toHaveBeenCalledTimes(10);
|
|
61
|
+
// A different email from the same address still has pair budget (the
|
|
62
|
+
// wider per-ip budget is 50).
|
|
63
|
+
expect((await postLogin(base, 'bob@example.com')).status).toBe(401);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('a successful login resets the pair budget', async () => {
|
|
67
|
+
const base = await listen();
|
|
68
|
+
for (let i = 0; i < 9; i++) await postLogin(base, 'alice@example.com');
|
|
69
|
+
vi.mocked(authService.loginWithPassword).mockResolvedValueOnce({
|
|
70
|
+
token: 'jwt',
|
|
71
|
+
user: { id: 'user-1', email: 'alice@example.com', name: 'Alice' },
|
|
72
|
+
});
|
|
73
|
+
expect((await postLogin(base, 'alice@example.com', 'right')).status).toBe(200);
|
|
74
|
+
// Budget is fresh again — failures start counting from zero.
|
|
75
|
+
expect((await postLogin(base, 'alice@example.com')).status).toBe(401);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('429s change-password per account after repeated failures', async () => {
|
|
79
|
+
const base = await listen();
|
|
80
|
+
const post = () =>
|
|
81
|
+
fetch(`${base}/api/auth/change-password`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ currentPassword: 'wrong', newPassword: 'new-password-1' }),
|
|
85
|
+
});
|
|
86
|
+
for (let i = 0; i < 10; i++) {
|
|
87
|
+
expect((await post()).status).toBe(400);
|
|
88
|
+
}
|
|
89
|
+
expect((await post()).status).toBe(429);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { FixedWindowRateLimiter } from '../rate-limit.js';
|
|
3
|
-
|
|
4
|
-
describe('FixedWindowRateLimiter', () => {
|
|
5
|
-
it('allows up to max attempts per window, then refuses', () => {
|
|
6
|
-
let now = 0;
|
|
7
|
-
const limiter = new FixedWindowRateLimiter(3, 1000, () => now);
|
|
8
|
-
expect(limiter.consume('k')).toBe(true);
|
|
9
|
-
expect(limiter.consume('k')).toBe(true);
|
|
10
|
-
expect(limiter.consume('k')).toBe(true);
|
|
11
|
-
expect(limiter.consume('k')).toBe(false);
|
|
12
|
-
// A different key has its own budget.
|
|
13
|
-
expect(limiter.consume('other')).toBe(true);
|
|
14
|
-
// Window rollover restores the budget.
|
|
15
|
-
now = 1001;
|
|
16
|
-
expect(limiter.consume('k')).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('reset() clears a key immediately', () => {
|
|
20
|
-
const limiter = new FixedWindowRateLimiter(1, 60_000, () => 0);
|
|
21
|
-
expect(limiter.consume('k')).toBe(true);
|
|
22
|
-
expect(limiter.consume('k')).toBe(false);
|
|
23
|
-
limiter.reset('k');
|
|
24
|
-
expect(limiter.consume('k')).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('prunes expired windows so the map stays bounded', () => {
|
|
28
|
-
let now = 0;
|
|
29
|
-
const limiter = new FixedWindowRateLimiter(1, 1000, () => now);
|
|
30
|
-
for (let i = 0; i < 100; i++) limiter.consume(`k${i}`);
|
|
31
|
-
now = 2000;
|
|
32
|
-
limiter.consume('fresh');
|
|
33
|
-
// Internal map pruned to just the fresh key.
|
|
34
|
-
expect((limiter as unknown as { hits: Map<string, unknown> }).hits.size).toBe(1);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { FixedWindowRateLimiter } from '../rate-limit.js';
|
|
3
|
+
|
|
4
|
+
describe('FixedWindowRateLimiter', () => {
|
|
5
|
+
it('allows up to max attempts per window, then refuses', () => {
|
|
6
|
+
let now = 0;
|
|
7
|
+
const limiter = new FixedWindowRateLimiter(3, 1000, () => now);
|
|
8
|
+
expect(limiter.consume('k')).toBe(true);
|
|
9
|
+
expect(limiter.consume('k')).toBe(true);
|
|
10
|
+
expect(limiter.consume('k')).toBe(true);
|
|
11
|
+
expect(limiter.consume('k')).toBe(false);
|
|
12
|
+
// A different key has its own budget.
|
|
13
|
+
expect(limiter.consume('other')).toBe(true);
|
|
14
|
+
// Window rollover restores the budget.
|
|
15
|
+
now = 1001;
|
|
16
|
+
expect(limiter.consume('k')).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('reset() clears a key immediately', () => {
|
|
20
|
+
const limiter = new FixedWindowRateLimiter(1, 60_000, () => 0);
|
|
21
|
+
expect(limiter.consume('k')).toBe(true);
|
|
22
|
+
expect(limiter.consume('k')).toBe(false);
|
|
23
|
+
limiter.reset('k');
|
|
24
|
+
expect(limiter.consume('k')).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('prunes expired windows so the map stays bounded', () => {
|
|
28
|
+
let now = 0;
|
|
29
|
+
const limiter = new FixedWindowRateLimiter(1, 1000, () => now);
|
|
30
|
+
for (let i = 0; i < 100; i++) limiter.consume(`k${i}`);
|
|
31
|
+
now = 2000;
|
|
32
|
+
limiter.consume('fresh');
|
|
33
|
+
// Internal map pruned to just the fresh key.
|
|
34
|
+
expect((limiter as unknown as { hits: Map<string, unknown> }).hits.size).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny fixed-window in-memory rate limiter for the password endpoints —
|
|
3
|
-
* brute-force / scrypt-DoS protection, not traffic shaping. In-memory is the
|
|
4
|
-
* right scope for the single-instance deployment model (same assumption as
|
|
5
|
-
* the MCP session store); a multi-replica future moves this behind a shared
|
|
6
|
-
* store.
|
|
7
|
-
*
|
|
8
|
-
* Expired windows are pruned opportunistically on each hit so the map stays
|
|
9
|
-
* bounded by the number of DISTINCT active keys per window.
|
|
10
|
-
*/
|
|
11
|
-
export class FixedWindowRateLimiter {
|
|
12
|
-
private readonly hits = new Map<string, { count: number; resetAt: number }>();
|
|
13
|
-
|
|
14
|
-
constructor(
|
|
15
|
-
/** Attempts allowed per window per key. */
|
|
16
|
-
private readonly max: number,
|
|
17
|
-
private readonly windowMs: number,
|
|
18
|
-
/** Injectable clock for tests. */
|
|
19
|
-
private readonly now: () => number = Date.now,
|
|
20
|
-
) {}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Record one attempt for `key`; returns true when the attempt is ALLOWED
|
|
24
|
-
* (i.e. the key is within its budget for the current window).
|
|
25
|
-
*/
|
|
26
|
-
consume(key: string): boolean {
|
|
27
|
-
const now = this.now();
|
|
28
|
-
for (const [k, entry] of this.hits) {
|
|
29
|
-
if (entry.resetAt <= now) this.hits.delete(k);
|
|
30
|
-
}
|
|
31
|
-
const entry = this.hits.get(key);
|
|
32
|
-
if (!entry || entry.resetAt <= now) {
|
|
33
|
-
this.hits.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
entry.count += 1;
|
|
37
|
-
return entry.count <= this.max;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Clear a key's window — called after a SUCCESSFUL login so legitimate
|
|
41
|
-
* users who fumbled a few attempts aren't locked out of their next visit. */
|
|
42
|
-
reset(key: string): void {
|
|
43
|
-
this.hits.delete(key);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tiny fixed-window in-memory rate limiter for the password endpoints —
|
|
3
|
+
* brute-force / scrypt-DoS protection, not traffic shaping. In-memory is the
|
|
4
|
+
* right scope for the single-instance deployment model (same assumption as
|
|
5
|
+
* the MCP session store); a multi-replica future moves this behind a shared
|
|
6
|
+
* store.
|
|
7
|
+
*
|
|
8
|
+
* Expired windows are pruned opportunistically on each hit so the map stays
|
|
9
|
+
* bounded by the number of DISTINCT active keys per window.
|
|
10
|
+
*/
|
|
11
|
+
export class FixedWindowRateLimiter {
|
|
12
|
+
private readonly hits = new Map<string, { count: number; resetAt: number }>();
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
/** Attempts allowed per window per key. */
|
|
16
|
+
private readonly max: number,
|
|
17
|
+
private readonly windowMs: number,
|
|
18
|
+
/** Injectable clock for tests. */
|
|
19
|
+
private readonly now: () => number = Date.now,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Record one attempt for `key`; returns true when the attempt is ALLOWED
|
|
24
|
+
* (i.e. the key is within its budget for the current window).
|
|
25
|
+
*/
|
|
26
|
+
consume(key: string): boolean {
|
|
27
|
+
const now = this.now();
|
|
28
|
+
for (const [k, entry] of this.hits) {
|
|
29
|
+
if (entry.resetAt <= now) this.hits.delete(k);
|
|
30
|
+
}
|
|
31
|
+
const entry = this.hits.get(key);
|
|
32
|
+
if (!entry || entry.resetAt <= now) {
|
|
33
|
+
this.hits.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
entry.count += 1;
|
|
37
|
+
return entry.count <= this.max;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Clear a key's window — called after a SUCCESSFUL login so legitimate
|
|
41
|
+
* users who fumbled a few attempts aren't locked out of their next visit. */
|
|
42
|
+
reset(key: string): void {
|
|
43
|
+
this.hits.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { CodeModeUtcpClient } from '@utcp/code-mode';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `tools_info` containment of a THROWING name lookup. `findToolByName`
|
|
6
|
+
* (mcp-core) throws on an ambiguous sanitized name — sanitization is lossy
|
|
7
|
+
* (`a-b` and `a+b` both become `a_b`) — and one ambiguous entry in a batch
|
|
8
|
+
* must cost that entry an error message, never the whole response. The
|
|
9
|
+
* lookup is mocked at the module seam so the test pins THIS module's
|
|
10
|
+
* per-name containment, not mcp-core's collision detection.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
vi.mock('../code-mode-names.js', async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal<typeof import('../code-mode-names.js')>();
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
findToolByName: vi.fn(async (_client: unknown, name: string) => {
|
|
18
|
+
if (name === 'a_b.fetch') {
|
|
19
|
+
// The REAL sentinel class — the containment branches on instanceof,
|
|
20
|
+
// and a lookalike Error would test the wrong contract.
|
|
21
|
+
throw new actual.AmbiguousToolNameError(
|
|
22
|
+
'Tool name "a_b.fetch" is ambiguous: "a-b.fetch", "a+b.fetch" all sanitize to it. ' +
|
|
23
|
+
'Call the tool by its exact UTCP name instead.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (name === 'down.run') throw new Error('repository unavailable: ECONNREFUSED');
|
|
27
|
+
if (name === 'solo.run') return { tool: { name: 'solo.run' }, utcpName: 'solo.run' };
|
|
28
|
+
return null;
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { createToolsInfoTool } = await import('../code-mode.tool.js');
|
|
34
|
+
|
|
35
|
+
const client = {
|
|
36
|
+
toolToTypeScriptInterface: (tool: { name: string }) => `interface ${tool.name}`,
|
|
37
|
+
} as unknown as CodeModeUtcpClient;
|
|
38
|
+
|
|
39
|
+
type ToolsInfoResult = { interfaces: string; not_found: string[]; errors?: string[] };
|
|
40
|
+
|
|
41
|
+
async function run(toolNames: string[]): Promise<ToolsInfoResult> {
|
|
42
|
+
const tool = createToolsInfoTool(client) as unknown as {
|
|
43
|
+
execute: (input: { tool_names: string[] }) => Promise<ToolsInfoResult>;
|
|
44
|
+
};
|
|
45
|
+
return tool.execute({ tool_names: toolNames });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('tools_info', () => {
|
|
49
|
+
it('answers every resolvable name even when one in the batch throws as ambiguous', async () => {
|
|
50
|
+
const result = await run(['a_b.fetch', 'solo.run', 'nope']);
|
|
51
|
+
expect(result.interfaces).toContain('interface solo.run');
|
|
52
|
+
expect(result.not_found).toEqual(['nope']);
|
|
53
|
+
expect(result.errors).toHaveLength(1);
|
|
54
|
+
expect(result.errors?.[0]).toMatch(/^a_b\.fetch: .*ambiguous/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('omits the errors field entirely when every name resolves or misses cleanly', async () => {
|
|
58
|
+
const result = await run(['solo.run', 'nope']);
|
|
59
|
+
expect(result.interfaces).toBe('interface solo.run');
|
|
60
|
+
expect(result.not_found).toEqual(['nope']);
|
|
61
|
+
expect(result.errors).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('contains ONLY ambiguity — an outage rethrows instead of posing as partial success', async () => {
|
|
65
|
+
await expect(run(['solo.run', 'down.run'])).rejects.toThrow(/repository unavailable/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
import type { Tool } from '@utcp/sdk';
|
|
2
|
-
import type { CodeModeUtcpClient } from '@utcp/code-mode';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Re-export of the code-mode name helpers, which now live in
|
|
3
|
+
* `@bevel-software/platform-mcp-core` so the local MCP server can share them
|
|
4
|
+
* without depending on this package. Kept as a module here because the agent's
|
|
5
|
+
* Mastra tools and the MCP proxy both import it by this path.
|
|
8
6
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export function utcpNameToTsInterfaceName(utcpName: string): string {
|
|
16
|
-
if (utcpName.includes('.')) {
|
|
17
|
-
const [manualName, ...toolParts] = utcpName.split('.');
|
|
18
|
-
return `${sanitizeIdentifier(manualName!)}.${toolParts.map(sanitizeIdentifier).join('_')}`;
|
|
19
|
-
}
|
|
20
|
-
return sanitizeIdentifier(utcpName);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Resolve a tool by its raw UTCP name or its sanitized TS-accessible name. */
|
|
24
|
-
export async function findToolByName(
|
|
25
|
-
client: CodeModeUtcpClient,
|
|
26
|
-
name: string,
|
|
27
|
-
): Promise<{ tool: Tool; utcpName: string } | null> {
|
|
28
|
-
const direct = await client.config.tool_repository.getTool(name);
|
|
29
|
-
if (direct) return { tool: direct, utcpName: name };
|
|
30
|
-
|
|
31
|
-
const all = await client.config.tool_repository.getTools();
|
|
32
|
-
for (const tool of all) {
|
|
33
|
-
if (utcpNameToTsInterfaceName(tool.name) === name) {
|
|
34
|
-
return { tool, utcpName: tool.name };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
7
|
+
export {
|
|
8
|
+
sanitizeIdentifier,
|
|
9
|
+
utcpNameToTsInterfaceName,
|
|
10
|
+
findToolByName,
|
|
11
|
+
AmbiguousToolNameError,
|
|
12
|
+
} from '@bevel-software/platform-mcp-core';
|
|
@@ -3,7 +3,7 @@ import { createTool } from '@mastra/core/tools';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { CodeModeUtcpClient } from '@utcp/code-mode';
|
|
5
5
|
import type { SpillStore } from '../workspace/spill-store.js';
|
|
6
|
-
import { utcpNameToTsInterfaceName, findToolByName } from './code-mode-names.js';
|
|
6
|
+
import { utcpNameToTsInterfaceName, findToolByName, AmbiguousToolNameError } from './code-mode-names.js';
|
|
7
7
|
|
|
8
8
|
export function createCallToolChainTool(
|
|
9
9
|
client: CodeModeUtcpClient,
|
|
@@ -103,15 +103,35 @@ export function createToolsInfoTool(client: CodeModeUtcpClient) {
|
|
|
103
103
|
execute: async (input) => {
|
|
104
104
|
const interfaces: string[] = [];
|
|
105
105
|
const notFound: string[] = [];
|
|
106
|
+
const errors: string[] = [];
|
|
106
107
|
for (const name of input.tool_names) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
// Per-name, not per-call: `findToolByName` THROWS on an ambiguous
|
|
109
|
+
// sanitized name (two UTCP tools collapsing to one TS name), and one
|
|
110
|
+
// ambiguous entry aborting the whole batch would cost the agent every
|
|
111
|
+
// other answer in it. The error text says how to disambiguate, so it
|
|
112
|
+
// is the per-name answer, not a failure of the tool.
|
|
113
|
+
try {
|
|
114
|
+
const found = await findToolByName(client, name);
|
|
115
|
+
if (found) {
|
|
116
|
+
interfaces.push(client.toolToTypeScriptInterface(found.tool));
|
|
117
|
+
} else {
|
|
118
|
+
notFound.push(name);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// Only AMBIGUITY is a per-name answer (the message says how to
|
|
122
|
+
// disambiguate). Anything else — a repository outage, a catalog
|
|
123
|
+
// failure — must fail the call: containing it would dress an
|
|
124
|
+
// outage up as a successful lookup with partial data. Typed, not
|
|
125
|
+
// message-matched: the wording belongs to another package.
|
|
126
|
+
if (!(err instanceof AmbiguousToolNameError)) throw err;
|
|
127
|
+
errors.push(`${name}: ${err.message}`);
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
interfaces: interfaces.join('\n\n'),
|
|
132
|
+
not_found: notFound,
|
|
133
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
134
|
+
};
|
|
115
135
|
},
|
|
116
136
|
});
|
|
117
137
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
2
|
-
import pg from 'pg';
|
|
3
|
-
import * as schema from './schema.js';
|
|
4
|
-
|
|
5
|
-
let db: ReturnType<typeof drizzle<typeof schema>> | null = null;
|
|
6
|
-
|
|
7
|
-
export function getDb(databaseUrl: string) {
|
|
8
|
-
if (!db) {
|
|
9
|
-
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
10
|
-
db = drizzle(pool, { schema });
|
|
11
|
-
}
|
|
12
|
-
return db;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type Database = ReturnType<typeof getDb>;
|
|
1
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
2
|
+
import pg from 'pg';
|
|
3
|
+
import * as schema from './schema.js';
|
|
4
|
+
|
|
5
|
+
let db: ReturnType<typeof drizzle<typeof schema>> | null = null;
|
|
6
|
+
|
|
7
|
+
export function getDb(databaseUrl: string) {
|
|
8
|
+
if (!db) {
|
|
9
|
+
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
10
|
+
db = drizzle(pool, { schema });
|
|
11
|
+
}
|
|
12
|
+
return db;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Database = ReturnType<typeof getDb>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The database schema of the CORE platform.
|
|
3
|
-
*
|
|
4
|
-
* In this package `schema.ts` is a re-export of `core-schema.ts` ONLY — the
|
|
5
|
-
* enterprise tables live with the enterprise product, whose own schema file
|
|
6
|
-
* imports the FK targets (`users`, `apiTokens`) from
|
|
7
|
-
* `@bevel-software/platform-core-backend/schema`. Kept as a separate module (rather
|
|
8
|
-
* than folding the tables in here) so module code keeps importing
|
|
9
|
-
* `./schema.js` unchanged.
|
|
10
|
-
*/
|
|
11
|
-
export * from './core-schema.js';
|
|
1
|
+
/**
|
|
2
|
+
* The database schema of the CORE platform.
|
|
3
|
+
*
|
|
4
|
+
* In this package `schema.ts` is a re-export of `core-schema.ts` ONLY — the
|
|
5
|
+
* enterprise tables live with the enterprise product, whose own schema file
|
|
6
|
+
* imports the FK targets (`users`, `apiTokens`) from
|
|
7
|
+
* `@bevel-software/platform-core-backend/schema`. Kept as a separate module (rather
|
|
8
|
+
* than folding the tables in here) so module code keeps importing
|
|
9
|
+
* `./schema.js` unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export * from './core-schema.js';
|