feat: replace profiles with kubernetes-style secrets
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled

Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).

- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-22 18:40:58 +00:00
parent 02254f2aac
commit ca02340a4c
77 changed files with 1014 additions and 1931 deletions

View File

@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProjectService } from '../src/services/project.service.js';
import { NotFoundError, ConflictError } from '../src/services/mcp-server.service.js';
import type { IProjectRepository } from '../src/repositories/project.repository.js';
import type { IMcpProfileRepository, IMcpServerRepository } from '../src/repositories/interfaces.js';
import type { IMcpServerRepository } from '../src/repositories/interfaces.js';
function mockProjectRepo(): IProjectRepository {
return {
@@ -23,19 +23,6 @@ function mockProjectRepo(): IProjectRepository {
createdAt: new Date(), updatedAt: new Date(),
})),
delete: vi.fn(async () => {}),
setProfiles: vi.fn(async () => {}),
getProfileIds: vi.fn(async () => []),
};
}
function mockProfileRepo(): IMcpProfileRepository {
return {
findAll: vi.fn(async () => []),
findById: vi.fn(async () => null),
findByServerAndName: vi.fn(async () => null),
create: vi.fn(async () => ({} as never)),
update: vi.fn(async () => ({} as never)),
delete: vi.fn(async () => {}),
};
}
@@ -52,15 +39,13 @@ function mockServerRepo(): IMcpServerRepository {
describe('ProjectService', () => {
let projectRepo: ReturnType<typeof mockProjectRepo>;
let profileRepo: ReturnType<typeof mockProfileRepo>;
let serverRepo: ReturnType<typeof mockServerRepo>;
let service: ProjectService;
beforeEach(() => {
projectRepo = mockProjectRepo();
profileRepo = mockProfileRepo();
serverRepo = mockServerRepo();
service = new ProjectService(projectRepo, profileRepo, serverRepo);
service = new ProjectService(projectRepo, serverRepo);
});
describe('create', () => {
@@ -86,55 +71,6 @@ describe('ProjectService', () => {
});
});
describe('setProfiles', () => {
it('sets profile associations', async () => {
vi.mocked(projectRepo.findById).mockResolvedValue({ id: 'p1' } as never);
vi.mocked(profileRepo.findById).mockResolvedValue({ id: 'prof-1' } as never);
const result = await service.setProfiles('p1', { profileIds: ['prof-1'] });
expect(result).toEqual(['prof-1']);
expect(projectRepo.setProfiles).toHaveBeenCalledWith('p1', ['prof-1']);
});
it('throws NotFoundError for missing profile', async () => {
vi.mocked(projectRepo.findById).mockResolvedValue({ id: 'p1' } as never);
await expect(service.setProfiles('p1', { profileIds: ['missing'] })).rejects.toThrow(NotFoundError);
});
it('throws NotFoundError for missing project', async () => {
await expect(service.setProfiles('missing', { profileIds: [] })).rejects.toThrow(NotFoundError);
});
});
describe('getMcpConfig', () => {
it('returns empty config for project with no profiles', async () => {
vi.mocked(projectRepo.findById).mockResolvedValue({ id: 'p1' } as never);
const result = await service.getMcpConfig('p1');
expect(result).toEqual({ mcpServers: {} });
});
it('generates config from profiles', async () => {
vi.mocked(projectRepo.findById).mockResolvedValue({ id: 'p1' } as never);
vi.mocked(projectRepo.getProfileIds).mockResolvedValue(['prof-1']);
vi.mocked(profileRepo.findById).mockResolvedValue({
id: 'prof-1', name: 'default', serverId: 's1',
permissions: [], envOverrides: {},
version: 1, createdAt: new Date(), updatedAt: new Date(),
});
vi.mocked(serverRepo.findById).mockResolvedValue({
id: 's1', name: 'slack', description: '', packageName: '@anthropic/slack-mcp',
dockerImage: null, transport: 'STDIO', repositoryUrl: null, envTemplate: [],
version: 1, createdAt: new Date(), updatedAt: new Date(),
});
const result = await service.getMcpConfig('p1');
expect(result.mcpServers['slack--default']).toBeDefined();
});
it('throws NotFoundError for missing project', async () => {
await expect(service.getMcpConfig('missing')).rejects.toThrow(NotFoundError);
});
});
describe('delete', () => {
it('deletes project', async () => {
vi.mocked(projectRepo.findById).mockResolvedValue({ id: 'p1' } as never);