feat(api,generator): preview endpoint + spec cache + audit-log writes
- POST /v1/servers/preview runs Claude synchronously, validates output, caches spec in Redis under preview:<id> with 5min TTL, returns previewId+spec+detectedSecrets. - POST /v1/servers accepts optional previewId; worker reuses the cached spec if the entry is still present, otherwise regenerates fresh. Skips the second Claude round-trip (~30s saved on the demoable path). - audit() helper writes auth.login, auth.logout, server.create, server.iterate, server.delete to audit_log with ip, metadata, resourceId. - GET /v1/me/org returns organization + members list for the settings page. - GET /v1/audit?limit=&action=&resourceType= returns scoped audit entries.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import { Redis } from 'ioredis';
|
||||
import { GeneratorSpec } from '@bmm/types';
|
||||
import { builds, createDb, eq, mcpServers } from '@bmm/db';
|
||||
import { config } from './config.js';
|
||||
import { generateSpec } from './lib/claude.js';
|
||||
@@ -10,6 +11,7 @@ import { emitDone, emitError, emitLog, emitStatus } from './lib/emit.js';
|
||||
|
||||
const db = createDb();
|
||||
const connection = new Redis(config.REDIS_URL, { maxRetriesPerRequest: null });
|
||||
const cacheReader = new Redis(config.REDIS_URL, { maxRetriesPerRequest: null });
|
||||
|
||||
interface JobData {
|
||||
buildId: string;
|
||||
@@ -20,22 +22,52 @@ interface JobData {
|
||||
slug: string;
|
||||
serverName: string;
|
||||
secrets: Record<string, string>;
|
||||
previewId?: string;
|
||||
}
|
||||
|
||||
async function loadCachedSpec(previewId: string): Promise<GeneratorSpec | null> {
|
||||
const raw = await cacheReader.get(`preview:${previewId}`);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = GeneratorSpec.safeParse(JSON.parse(raw));
|
||||
return parsed.success ? parsed.data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const worker = new Worker<JobData>(
|
||||
'build',
|
||||
async (job) => {
|
||||
const { buildId, serverId, prompt, version, slug, secrets } = job.data;
|
||||
const { buildId, serverId, prompt, version, slug, secrets, previewId } = job.data;
|
||||
const log = (level: 'info' | 'warn' | 'error', msg: string) => emitLog(buildId, level, msg);
|
||||
|
||||
try {
|
||||
await db.update(builds).set({ status: 'generating', startedAt: new Date() }).where(eq(builds.id, buildId));
|
||||
await db.update(mcpServers).set({ status: 'generating', updatedAt: new Date() }).where(eq(mcpServers.id, serverId));
|
||||
await emitStatus(buildId, 'generating');
|
||||
await log('info', 'Generating MCP server spec...');
|
||||
|
||||
const { spec, source } = await generateSpec(prompt);
|
||||
await log('info', `Spec generated via ${source} (${spec.tools.length} tool(s))`);
|
||||
let spec: GeneratorSpec | null = null;
|
||||
let source: 'claude' | 'mock' | 'cached' = 'mock';
|
||||
|
||||
if (previewId) {
|
||||
spec = await loadCachedSpec(previewId);
|
||||
if (spec) {
|
||||
source = 'cached';
|
||||
await log('info', `Re-using preview spec ${previewId} (skipping Claude call)`);
|
||||
} else {
|
||||
await log('warn', `Preview ${previewId} cache miss — regenerating`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!spec) {
|
||||
await log('info', 'Generating MCP server spec...');
|
||||
const result = await generateSpec(prompt);
|
||||
spec = result.spec;
|
||||
source = result.source;
|
||||
}
|
||||
|
||||
await log('info', `Spec ready via ${source} (${spec.tools.length} tool(s))`);
|
||||
const generatedCode = renderServerCode(spec);
|
||||
await db
|
||||
.update(builds)
|
||||
|
||||
Reference in New Issue
Block a user