import crypto from 'node:crypto'; import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { and, builds, count, createDb, desc, eq, gte, mcpServers, organizations, sql, templates, users, } from '@bmm/db'; import { GeneratorSpec } from '@bmm/types'; import { requireAuth, requireAdmin } from '../plugins/session.js'; import { audit } from '../lib/audit.js'; import { cacheSpec, cachePrebuiltCode } from '../lib/preview-cache.js'; import { getRedis } from '../lib/redis.js'; import { stopContainer } from '../lib/docker.js'; const db = createDb(); const BANNED_PATTERNS = [ /\beval\s*\(/, /\bnew\s+Function\s*\(/, /\bFunction\s*\(\s*['"`]/, // Function('code')() — no `new` needed /\bimport\s*\(/, // dynamic import (escape from bundle scope) /\bsetTimeout\s*\(\s*['"`]/, // setTimeout('code', ms) eval form /\bsetInterval\s*\(\s*['"`]/, /\bchild_process\b/, /\bfs\s*\.\s*(unlink|rmdir|rm)\b/, /\bprocess\s*\.\s*kill\b/, /ignore\s+previous\s+instructions/i, /disregard\s+(the\s+)?(above|previous)/i, /you\s+are\s+now\s+(in\s+)?(developer|jailbreak|dan)\s+mode/i, ]; // Hardcoded-credential patterns. If Claude embedded a literal API key into the // generated code (publisher pasted it into the prompt), block the publish. const SECRET_PATTERNS = [ { name: 'anthropic_key', re: /\bsk-ant-(?:api|sid)\d+-[A-Za-z0-9_-]{20,}/ }, { name: 'openai_key', re: /\bsk-[A-Za-z0-9_-]{30,}/ }, { name: 'stripe_secret', re: /\bsk_(live|test)_[A-Za-z0-9]{20,}/ }, { name: 'github_pat', re: /\bghp_[A-Za-z0-9]{30,}/ }, { name: 'github_fine_grained', re: /\bgithub_pat_[A-Za-z0-9_]{30,}/ }, { name: 'slack_token', re: /\bxox[bpoasr]-[A-Za-z0-9-]{10,}/ }, { name: 'aws_access_key', re: /\bAKIA[0-9A-Z]{16}\b/ }, { name: 'rsa_private_key', re: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/i }, ]; function scanForInjection(code: string): void { for (const pattern of BANNED_PATTERNS) { if (pattern.test(code)) throw new Error(`banned_pattern: ${pattern.source}`); } } function scanForLeakedSecrets(code: string): void { for (const { name, re } of SECRET_PATTERNS) { if (re.test(code)) { throw new Error( `hardcoded_${name}_detected: a literal credential was found in the generated code; remove it before publishing`, ); } } } const SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/; // Per-fork link: ties a previewId back to the template it came from. Set during // fork, consumed by the create-server endpoint to prove the user actually went // through the fork flow before we accept templateId or bump forkCount. const FORK_REF_TTL_SECONDS = 5 * 60; async function setForkRef(previewId: string, templateId: string): Promise { await getRedis().set(`fork-ref:${previewId}`, templateId, 'EX', FORK_REF_TTL_SECONDS); } export async function getForkRefTemplate(previewId: string): Promise { return (await getRedis().get(`fork-ref:${previewId}`)) ?? null; } const CATEGORIES = [ 'productivity', 'developer-tools', 'data', 'communication', 'finance', 'crm', 'analytics', 'devops', 'demo', 'other', ] as const; const SecretHint = z.object({ key: z.string().regex(/^[A-Z][A-Z0-9_]*$/), description: z.string().min(1).max(280), howToGetUrl: z.string().url().optional(), }); const PublishInput = z.object({ serverId: z.string().uuid(), title: z.string().min(3).max(128), shortDescription: z.string().min(10).max(280), longDescription: z.string().max(8000).optional(), category: z.enum(CATEGORIES), secretHints: z.array(SecretHint).max(30).default([]), allowedDomains: z.array(z.string()).max(50).optional(), }); export async function templateRoutes(app: FastifyInstance): Promise { // ---- Publish (user → template) ---- app.post('/v1/templates', { preHandler: requireAuth }, async (req, reply) => { const user = req.user!; const parsed = PublishInput.safeParse(req.body); if (!parsed.success) { return reply.code(400).send({ error: 'invalid_input', issues: parsed.error.flatten() }); } const [server] = await db .select() .from(mcpServers) .where(and(eq(mcpServers.id, parsed.data.serverId), eq(mcpServers.orgId, user.orgId))) .limit(1); if (!server) return reply.code(404).send({ error: 'server_not_found' }); if (server.status !== 'live') { return reply .code(400) .send({ error: 'server_not_live', detail: 'Only live servers can be published.' }); } if (!server.toolsSchema) { return reply.code(400).send({ error: 'no_tools_schema' }); } // Get the last successful build's generated code (this is the implementation) const [build] = await db .select() .from(builds) .where(and(eq(builds.serverId, server.id), eq(builds.status, 'success'))) .orderBy(desc(builds.version)) .limit(1); if (!build || !build.generatedCode) { return reply.code(400).send({ error: 'no_generated_code' }); } // Re-validate code against banned patterns AND hardcoded secrets try { scanForInjection(build.generatedCode); scanForLeakedSecrets(build.generatedCode); } catch (err) { return reply.code(422).send({ error: 'publish_blocked', detail: (err as Error).message }); } // Build a unique template slug const baseSlug = parsed.data.title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, '') .slice(0, 48); let slug = baseSlug || `template-${crypto.randomBytes(3).toString('hex')}`; let attempt = 0; while (true) { const existing = await db.select({ id: templates.id }).from(templates).where(eq(templates.slug, slug)).limit(1); if (existing.length === 0) break; attempt++; slug = `${baseSlug}-${crypto.randomBytes(2).toString('hex')}`; if (attempt > 5) { return reply.code(500).send({ error: 'slug_conflict' }); } } const [template] = await db .insert(templates) .values({ ownerUserId: user.userId, ownerOrgId: user.orgId, sourceServerId: server.id, slug, title: parsed.data.title, shortDescription: parsed.data.shortDescription, longDescription: parsed.data.longDescription ?? null, category: parsed.data.category, toolsSchema: server.toolsSchema, generatedCode: build.generatedCode, requiredSecrets: parsed.data.secretHints, scopes: (server.toolsSchema as Array<{ scopes?: string[] }>).reduce( () => ['mcp:read'], [], ), allowedDomains: parsed.data.allowedDomains ?? null, }) .returning(); if (!template) return reply.code(500).send({ error: 'template_create_failed' }); await audit({ orgId: user.orgId, userId: user.userId, action: 'template.publish', resourceType: 'template', resourceId: template.id, metadata: { slug, title: parsed.data.title, fromServerId: server.id }, ipAddress: req.ip, }); return reply.send({ template }); }); // ---- "Is this server already published?" (owner lookup, drives the detail-page tab) ---- app.get('/v1/servers/:id/template', { preHandler: requireAuth }, async (req, reply) => { const user = req.user!; const Params = z.object({ id: z.string().uuid() }); const parsed = Params.safeParse(req.params); if (!parsed.success) return reply.code(400).send({ error: 'invalid_id' }); // Verify the server belongs to the caller's org const [server] = await db .select({ id: mcpServers.id }) .from(mcpServers) .where(and(eq(mcpServers.id, parsed.data.id), eq(mcpServers.orgId, user.orgId))) .limit(1); if (!server) return reply.code(404).send({ error: 'not_found' }); const [template] = await db .select({ id: templates.id, slug: templates.slug, title: templates.title, status: templates.status, verified: templates.verified, forkCount: templates.forkCount, }) .from(templates) .where(eq(templates.sourceServerId, parsed.data.id)) .orderBy(desc(templates.createdAt)) .limit(1); return reply.send({ template: template ?? null }); }); // ---- Owner visibility toggle (unshare / re-share anytime) ---- app.patch('/v1/templates/:slug/visibility', { preHandler: requireAuth }, async (req, reply) => { const user = req.user!; const Params = z.object({ slug: z.string().regex(SLUG_REGEX) }); const Body = z.object({ shared: z.boolean() }); const p = Params.safeParse(req.params); const b = Body.safeParse(req.body); if (!p.success || !b.success) return reply.code(400).send({ error: 'invalid_input' }); const [template] = await db .select() .from(templates) .where(eq(templates.slug, p.data.slug)) .limit(1); if (!template) return reply.code(404).send({ error: 'not_found' }); // Only the owner can toggle their own template. Admins use /v1/admin/templates. if (template.ownerUserId !== user.userId) { return reply.code(403).send({ error: 'forbidden' }); } // A template the admin took down cannot be re-shared by the owner. if (template.status === 'takedown') { return reply.code(409).send({ error: 'taken_down', detail: template.takedownReason }); } const nextStatus = b.data.shared ? 'public' : 'hidden'; await db .update(templates) .set({ status: nextStatus, updatedAt: new Date() }) .where(eq(templates.id, template.id)); await audit({ orgId: user.orgId, userId: user.userId, action: b.data.shared ? 'template.reshare' : 'template.unshare', resourceType: 'template', resourceId: template.id, metadata: { slug: template.slug }, ipAddress: req.ip, }); return reply.send({ ok: true, status: nextStatus }); }); // ---- Public list with ranking ---- app.get('/v1/templates', async (req, reply) => { const Query = z.object({ category: z.string().optional(), sort: z.enum(['trending', 'top', 'newest']).default('trending'), limit: z.coerce.number().min(1).max(100).default(50), }); const parsed = Query.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'invalid_query' }); const rows = await db .select({ template: templates, ownerName: users.name, ownerEmail: users.email, ownerOrgName: organizations.name, }) .from(templates) .leftJoin(users, eq(users.id, templates.ownerUserId)) .leftJoin(organizations, eq(organizations.id, templates.ownerOrgId)) .where(eq(templates.status, 'public')) .orderBy(desc(templates.createdAt)) .limit(parsed.data.limit); const filtered = parsed.data.category ? rows.filter((r) => r.template.category === parsed.data.category) : rows; // Augment with active deployment counts const enriched = await Promise.all( filtered.map(async (r) => { const [active] = await db .select({ c: count() }) .from(mcpServers) .where(and(eq(mcpServers.templateId, r.template.id), eq(mcpServers.status, 'live'))); return { ...r.template, ownerName: r.ownerName ?? r.ownerEmail?.split('@')[0] ?? null, ownerOrgName: r.ownerOrgName, activeDeployments: Number(active?.c ?? 0), }; }), ); // Sort const now = Date.now(); const ranked = [...enriched].sort((a, b) => { if (parsed.data.sort === 'newest') { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); } if (parsed.data.sort === 'top') { return b.forkCount - a.forkCount; } // trending: (active*3 + forks) / sqrt(ageDays + 1) const score = (t: (typeof enriched)[number]): number => { const ageDays = Math.max(1, (now - new Date(t.createdAt).getTime()) / 86_400_000); return (t.activeDeployments * 3 + t.forkCount) / Math.sqrt(ageDays); }; return score(b) - score(a); }); return reply.send({ templates: ranked, categories: CATEGORIES }); }); // ---- Detail ---- app.get('/v1/templates/:slug', async (req, reply) => { const Params = z.object({ slug: z.string().regex(SLUG_REGEX) }); const parsed = Params.safeParse(req.params); if (!parsed.success) return reply.code(400).send({ error: 'invalid_slug' }); const [row] = await db .select({ template: templates, ownerName: users.name, ownerEmail: users.email, ownerOrgName: organizations.name, }) .from(templates) .leftJoin(users, eq(users.id, templates.ownerUserId)) .leftJoin(organizations, eq(organizations.id, templates.ownerOrgId)) .where(eq(templates.slug, parsed.data.slug)) .limit(1); if (!row) return reply.code(404).send({ error: 'not_found' }); if (row.template.status === 'takedown') { return reply.code(410).send({ error: 'taken_down', reason: row.template.takedownReason, }); } if (row.template.status !== 'public') { // hidden / draft — only owner can view // Note: viewing endpoint is public, so we 404 to non-owners. // (Owner UI would use a separate auth'd endpoint; out of scope for v1.) return reply.code(404).send({ error: 'not_found' }); } const [active] = await db .select({ c: count() }) .from(mcpServers) .where(and(eq(mcpServers.templateId, row.template.id), eq(mcpServers.status, 'live'))); return reply.send({ template: { ...row.template, ownerName: row.ownerName ?? row.ownerEmail?.split('@')[0] ?? null, ownerOrgName: row.ownerOrgName, activeDeployments: Number(active?.c ?? 0), }, }); }); // ---- Fork (returns previewId so wizard can complete with user's secrets) ---- app.post('/v1/templates/:slug/fork', { preHandler: requireAuth }, async (req, reply) => { const Params = z.object({ slug: z.string().regex(SLUG_REGEX) }); const parsed = Params.safeParse(req.params); if (!parsed.success) return reply.code(400).send({ error: 'invalid_slug' }); const [template] = await db .select() .from(templates) .where(eq(templates.slug, parsed.data.slug)) .limit(1); if (!template || template.status !== 'public') { return reply.code(404).send({ error: 'not_found' }); } // Reconstruct a GeneratorSpec from the template that the worker can render const toolsRaw = template.toolsSchema as Array<{ name: string; description: string; inputSchema: Record; }>; const fullSpec = { name: template.title, description: template.shortDescription, tools: toolsRaw.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema as Record, // The actual implementation is in template.generatedCode, but we have to // satisfy the per-tool GeneratorSpec shape. The worker uses the // cached spec only for non-impl fields; render extracts code from full spec. // For fork we ship a placeholder impl — the worker will see previewId // and would normally regenerate. We need a different mechanism: // we use the generatedCode field directly through the cache. implementation: '// implementation provided by template', })), resources: [], prompts: [], requiredSecrets: (template.requiredSecrets as Array<{ key: string }>).map((s) => s.key), scopes: template.scopes as string[], dependencies: {}, }; const validation = GeneratorSpec.safeParse(fullSpec); if (!validation.success) { return reply.code(500).send({ error: 'template_spec_invalid', detail: validation.error.flatten() }); } const previewId = await cacheSpec(validation.data); // Persist the pre-rendered code under the same previewId so the worker uses it // verbatim instead of re-rendering (which would lose the template's per-tool impls). await cachePrebuiltCode(previewId, template.generatedCode); // Record the fork→template link so /v1/servers can verify the user actually // went through this endpoint before accepting templateId. await setForkRef(previewId, template.id); return reply.send({ previewId, templateId: template.id, template: { slug: template.slug, title: template.title, shortDescription: template.shortDescription, tools: toolsRaw, requiredSecrets: template.requiredSecrets, }, }); }); // ---- Admin moderation ---- app.get('/v1/admin/templates', { preHandler: requireAdmin }, async (_req, reply) => { const rows = await db .select({ template: templates, ownerEmail: users.email, ownerOrgName: organizations.name, }) .from(templates) .leftJoin(users, eq(users.id, templates.ownerUserId)) .leftJoin(organizations, eq(organizations.id, templates.ownerOrgId)) .orderBy(desc(templates.createdAt)); const enriched = await Promise.all( rows.map(async (r) => { const [active] = await db .select({ c: count() }) .from(mcpServers) .where(and(eq(mcpServers.templateId, r.template.id), eq(mcpServers.status, 'live'))); return { ...r.template, ownerEmail: r.ownerEmail, ownerOrgName: r.ownerOrgName, activeDeployments: Number(active?.c ?? 0), }; }), ); return reply.send({ templates: enriched }); }); app.patch('/v1/admin/templates/:id', { preHandler: requireAdmin }, async (req, reply) => { const Params = z.object({ id: z.string().uuid() }); const Body = z.object({ verified: z.boolean().optional(), status: z.enum(['draft', 'public', 'hidden', 'takedown']).optional(), takedownReason: z.string().max(500).nullable().optional(), }); const p = Params.safeParse(req.params); const b = Body.safeParse(req.body); if (!p.success || !b.success) return reply.code(400).send({ error: 'invalid_input' }); await db .update(templates) .set({ ...b.data, updatedAt: new Date() }) .where(eq(templates.id, p.data.id)); // Takedown cascade: stop every fork's container, then mark them paused. // Just flipping the DB status leaves the container running and serving // traffic; we MUST hard-stop them or the takedown is cosmetic. let stoppedContainers = 0; if (b.data.status === 'takedown') { const forkedServers = await db .select({ id: mcpServers.id, containerId: mcpServers.containerId }) .from(mcpServers) .where(eq(mcpServers.templateId, p.data.id)); for (const fork of forkedServers) { if (fork.containerId) { const result = await stopContainer(fork.containerId); if (result.ok) stoppedContainers++; else app.log.warn({ containerId: fork.containerId, detail: result.detail }, 'takedown: stop failed'); } } await db .update(mcpServers) .set({ status: 'paused', containerId: null, publicUrl: null, hostPort: null, updatedAt: new Date(), }) .where(eq(mcpServers.templateId, p.data.id)); } await audit({ orgId: req.user!.orgId, userId: req.user!.userId, action: 'admin.template.update', resourceType: 'template', resourceId: p.data.id, metadata: { ...b.data, stoppedContainers }, ipAddress: req.ip, }); return reply.send({ ok: true, stoppedContainers }); }); // unused-import guard void gte; void sql; }