fix(ungated-global): create settings secret with a valid backend FK #138
@@ -15,6 +15,7 @@
|
||||
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
import { SYSTEM_SETTINGS_SECRET } from './system-settings.js';
|
||||
import { DEFAULT_PLAINTEXT_BACKEND_NAME } from './secret-backends.js';
|
||||
|
||||
/** Well-known project whose servers are exposed under every project, pre-gate. */
|
||||
export const UNGATED_GLOBAL_PROJECT_NAME = 'ungated-global';
|
||||
@@ -61,10 +62,22 @@ async function markSeeded(prisma: PrismaClient): Promise<void> {
|
||||
data: { data, keyNames, version: existing.version + 1 },
|
||||
});
|
||||
} else {
|
||||
// backendId '' is repointed at the default plaintext backend by
|
||||
// bootstrapSecretBackends — same convention as pre-existing rows.
|
||||
// The Secret.backendId FK is enforced at insert time, so a fresh row
|
||||
// must point at a real backend. bootstrapUngatedGlobal runs after
|
||||
// bootstrapSecretBackends, so the default plaintext backend exists;
|
||||
// fall back to any backend if the name ever changes.
|
||||
const backend =
|
||||
(await prisma.secretBackend.findUnique({ where: { name: DEFAULT_PLAINTEXT_BACKEND_NAME } }))
|
||||
?? (await prisma.secretBackend.findFirst({ where: { isDefault: true } }))
|
||||
?? (await prisma.secretBackend.findFirst());
|
||||
if (!backend) {
|
||||
// No backend at all — skip marking rather than crash startup. The seed
|
||||
// above still ran; worst case it re-runs (idempotent upserts) next boot.
|
||||
console.warn('ungated-global: no SecretBackend found, seed marker not written');
|
||||
return;
|
||||
}
|
||||
await prisma.secret.create({
|
||||
data: { name: SYSTEM_SETTINGS_SECRET, data, keyNames },
|
||||
data: { name: SYSTEM_SETTINGS_SECRET, data, keyNames, backendId: backend.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,8 +414,6 @@ async function main(): Promise<void> {
|
||||
await bootstrapSystemProject(prisma);
|
||||
// PR-4: bootstrap system-owned global skills (e.g. propose-learnings).
|
||||
await bootstrapSystemSkills(prisma);
|
||||
// Seed-once ungated-global project (default calcmcp server, deletable).
|
||||
await bootstrapUngatedGlobal(prisma);
|
||||
|
||||
// Repositories
|
||||
const serverRepo = new McpServerRepository(prisma);
|
||||
@@ -438,6 +436,11 @@ async function main(): Promise<void> {
|
||||
// pre-existing `Secret` rows are pointed at it. Idempotent per run.
|
||||
await bootstrapSecretBackends(prisma);
|
||||
|
||||
// Seed-once ungated-global project (default calcmcp server, deletable).
|
||||
// MUST run after bootstrapSecretBackends: its seed marker lives in the
|
||||
// mcpctl-system-settings Secret, whose creation needs a valid backend FK.
|
||||
await bootstrapUngatedGlobal(prisma);
|
||||
|
||||
// CUID detection for RBAC name resolution
|
||||
const CUID_RE = /^c[^\s-]{8,}$/i;
|
||||
const nameResolvers: Record<string, { findById(id: string): Promise<{ name: string } | null> }> = {
|
||||
|
||||
@@ -18,6 +18,10 @@ function mockPrisma(opts: { settingsData?: Record<string, unknown> | null } = {}
|
||||
update: vi.fn(async () => ({})),
|
||||
create: vi.fn(async () => ({})),
|
||||
},
|
||||
secretBackend: {
|
||||
findUnique: vi.fn(async () => ({ id: 'backend-default', name: 'default', isDefault: true })),
|
||||
findFirst: vi.fn(async () => ({ id: 'backend-default', name: 'default', isDefault: true })),
|
||||
},
|
||||
user: {
|
||||
upsert: vi.fn(async () => ({ id: 'sys-user-id', email: 'system@mcpctl.local' })),
|
||||
},
|
||||
@@ -68,11 +72,12 @@ describe('bootstrapUngatedGlobal', () => {
|
||||
it('marks the seed as done in the system settings secret', async () => {
|
||||
await bootstrapUngatedGlobal(prisma);
|
||||
|
||||
// Settings secret didn't exist → created with the marker key
|
||||
// Settings secret didn't exist → created with the marker key and a valid backend FK
|
||||
expect(prisma.secret.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
name: SYSTEM_SETTINGS_SECRET,
|
||||
backendId: 'backend-default',
|
||||
data: expect.objectContaining({ [SETTING_UNGATED_GLOBAL_SEEDED]: true }),
|
||||
}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user