buildmymcpserver/apps/api/src/routes/templates.ts

584 lines
21 KiB
TypeScript
Raw Normal View History

feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
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';
feat(marketplace): dashboard nav link + My-templates filter The logged-in user can now reach the marketplace and filter to their own templates. Dashboard nav: - Added 'Marketplace' item (Overview · Servers · Marketplace · Audit · Settings). /templates page — login-aware: - Detects session via /v1/auth/me. Logged-in users get a 'Dashboard' + '+ New server' header instead of 'Home' + 'Start building'. - New [All templates | My templates] scope toggle, shown only when logged in. - 'My templates' loads GET /v1/templates/mine and shows EVERY status the user owns (public / hidden / draft / takedown) with a colored status badge on each card — so a template you unshared doesn't appear to have vanished. - Sort tabs (trending/top/newest) hide in 'mine' scope — meaningless for a handful of own templates. Category filter + search still apply (client-side). - Takedown cards link to the source server's Publish tab instead of the detail route (which 410s); everything else opens the detail page. Backend: - GET /v1/templates/mine (requireAuth) — all own templates, any status, registered before /:slug so the static route always wins the match. - GET /v1/templates/:slug — now does an optional session check: the OWNER can view their own hidden/draft template (so a 'My templates' card click never dead-ends in a 404). takedown stays 410 for everyone, owner included — that's an admin decision, not the owner's to reverse. Detail page: - Fork CTA is gated on status === 'public'. For a non-public template the owner sees an amber 'not forkable — re-share from the Publish tab' notice plus a 'Manage in server' link, instead of a Fork button that would fail silently. Verified: - GET /v1/templates/mine → marco's 1 template; 401 without auth - Owner GET of a hidden template → 200 status:hidden; anon → 404 - Dashboard nav shows Marketplace (screenshot) - /templates 'My templates' toggle → only own template, public badge, sort tabs hidden (screenshot)
2026-05-20 17:18:58 +02:00
import { getSession } from '@bmm/auth';
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
import { requireAuth, requireAdmin } from '../plugins/session.js';
import { audit } from '../lib/audit.js';
import { cacheSpec, cachePrebuiltCode } from '../lib/preview-cache.js';
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
import { getRedis } from '../lib/redis.js';
import { stopContainer } from '../lib/docker.js';
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
const db = createDb();
const BANNED_PATTERNS = [
/\beval\s*\(/,
/\bnew\s+Function\s*\(/,
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
/\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*['"`]/,
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
/\bchild_process\b/,
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
/\bfs\s*\.\s*(unlink|rmdir|rm)\b/,
/\bprocess\s*\.\s*kill\b/,
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
/ignore\s+previous\s+instructions/i,
/disregard\s+(the\s+)?(above|previous)/i,
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
/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 },
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
];
function scanForInjection(code: string): void {
for (const pattern of BANNED_PATTERNS) {
if (pattern.test(code)) throw new Error(`banned_pattern: ${pattern.source}`);
}
}
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
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<void> {
await getRedis().set(`fork-ref:${previewId}`, templateId, 'EX', FORK_REF_TTL_SECONDS);
}
export async function getForkRefTemplate(previewId: string): Promise<string | null> {
return (await getRedis().get(`fork-ref:${previewId}`)) ?? null;
}
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
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<void> {
// ---- 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' });
}
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
// Re-validate code against banned patterns AND hardcoded secrets
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
try {
scanForInjection(build.generatedCode);
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
scanForLeakedSecrets(build.generatedCode);
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
} catch (err) {
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
return reply.code(422).send({ error: 'publish_blocked', detail: (err as Error).message });
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
}
// 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<string[]>(
() => ['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 });
});
feat(marketplace): default-on share in wizard + owner unshare anytime Goal: maximize template volume without a dark pattern and without leaking data. Wizard Done-page Share panel: - 'Share as template in the marketplace (recommended)' checkbox, default ON, rendered inline in the build-success flow where every user lands. - Honest copy — corrected a draft that claimed 'only abstracted code pattern is shared'. That is false: the FULL generated code becomes publicly viewable on the template detail page (by design, for pre-fork audit). The panel now says: 'Your secrets stay private ... but your generated code becomes publicly viewable so others can audit it before forking. Unshare anytime.' - When checked: inline minimal form — short description (prefilled from the spec), category select, optional per-secret credential hints. One 'Publish to marketplace' click. Not auto-published silently — that would be a consent dark pattern; one visible deliberate click keeps it clean. - Forked servers don't show the panel (re-publishing a fork is an edge case). Owner unshare/reshare: - GET /v1/servers/:id/template — owner lookup, drives the Publish tab UI. - PATCH /v1/templates/:slug/visibility { shared } — owner-only toggle between public and hidden. 403 for non-owners, 409 if an admin took it down (owner cannot resurrect an admin takedown). Audit-logged as template.unshare / template.reshare. - Server-detail Publish tab now detects an existing template and shows the shared status (public/hidden/takedown badge), fork count, a marketplace link and an Unshare/Re-share button — instead of the publish form. Why this is safe to default ON: - Secrets are architecturally bound to mcp_servers, never copied into templates. Publish reads tools_schema + generated_code only; the secrets table is never touched. Data leak is structurally impossible, not policy-dependent. - Publish re-scans the generated code for banned patterns AND hardcoded credentials (sovereign-audit hardening) before it can reach the marketplace. - The user sees a visible, pre-ticked checkbox and reads one honest sentence before publishing. Privacy-conscious users untick; everyone else contributes volume. Informed consent, GDPR-clean. Verified end-to-end via API: GET server/:id/template -> null (unpublished) POST /v1/templates -> published, slug share-test-server GET server/:id/template -> status public PATCH visibility {shared:false} -> hidden, drops out of public list PATCH visibility {shared:true} -> public again UI: Publish tab renders the shared-status panel with View + Unshare (screenshot confirmed). Also: hero badge date set to 2026-05-20. Changed 'MCP spec 2025-11-25' to 'updated 2026-05-20' — claiming an MCP spec dated today would be factually wrong (no such spec release exists); 'updated' is accurate and gives the requested fresh date. The real spec date is still cited correctly in /docs.
2026-05-20 17:04:46 +02:00
// ---- "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 });
});
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
// ---- 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 });
});
feat(marketplace): dashboard nav link + My-templates filter The logged-in user can now reach the marketplace and filter to their own templates. Dashboard nav: - Added 'Marketplace' item (Overview · Servers · Marketplace · Audit · Settings). /templates page — login-aware: - Detects session via /v1/auth/me. Logged-in users get a 'Dashboard' + '+ New server' header instead of 'Home' + 'Start building'. - New [All templates | My templates] scope toggle, shown only when logged in. - 'My templates' loads GET /v1/templates/mine and shows EVERY status the user owns (public / hidden / draft / takedown) with a colored status badge on each card — so a template you unshared doesn't appear to have vanished. - Sort tabs (trending/top/newest) hide in 'mine' scope — meaningless for a handful of own templates. Category filter + search still apply (client-side). - Takedown cards link to the source server's Publish tab instead of the detail route (which 410s); everything else opens the detail page. Backend: - GET /v1/templates/mine (requireAuth) — all own templates, any status, registered before /:slug so the static route always wins the match. - GET /v1/templates/:slug — now does an optional session check: the OWNER can view their own hidden/draft template (so a 'My templates' card click never dead-ends in a 404). takedown stays 410 for everyone, owner included — that's an admin decision, not the owner's to reverse. Detail page: - Fork CTA is gated on status === 'public'. For a non-public template the owner sees an amber 'not forkable — re-share from the Publish tab' notice plus a 'Manage in server' link, instead of a Fork button that would fail silently. Verified: - GET /v1/templates/mine → marco's 1 template; 401 without auth - Owner GET of a hidden template → 200 status:hidden; anon → 404 - Dashboard nav shows Marketplace (screenshot) - /templates 'My templates' toggle → only own template, public badge, sort tabs hidden (screenshot)
2026-05-20 17:18:58 +02:00
// ---- My templates (authed — all statuses, for the marketplace "Mine" filter) ----
// Registered before /:slug so the static segment always wins the router match.
app.get('/v1/templates/mine', { preHandler: requireAuth }, async (req, reply) => {
const user = req.user!;
const rows = await db
.select()
.from(templates)
.where(eq(templates.ownerUserId, user.userId))
.orderBy(desc(templates.createdAt));
const enriched = await Promise.all(
rows.map(async (t) => {
const [active] = await db
.select({ c: count() })
.from(mcpServers)
.where(and(eq(mcpServers.templateId, t.id), eq(mcpServers.status, 'live')));
return {
...t,
ownerName: user.email.split('@')[0],
ownerOrgName: null,
activeDeployments: Number(active?.c ?? 0),
};
}),
);
return reply.send({ templates: enriched, categories: CATEGORIES });
});
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
// ---- Detail ----
app.get('/v1/templates/:slug', async (req, reply) => {
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
const Params = z.object({ slug: z.string().regex(SLUG_REGEX) });
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
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') {
feat(marketplace): dashboard nav link + My-templates filter The logged-in user can now reach the marketplace and filter to their own templates. Dashboard nav: - Added 'Marketplace' item (Overview · Servers · Marketplace · Audit · Settings). /templates page — login-aware: - Detects session via /v1/auth/me. Logged-in users get a 'Dashboard' + '+ New server' header instead of 'Home' + 'Start building'. - New [All templates | My templates] scope toggle, shown only when logged in. - 'My templates' loads GET /v1/templates/mine and shows EVERY status the user owns (public / hidden / draft / takedown) with a colored status badge on each card — so a template you unshared doesn't appear to have vanished. - Sort tabs (trending/top/newest) hide in 'mine' scope — meaningless for a handful of own templates. Category filter + search still apply (client-side). - Takedown cards link to the source server's Publish tab instead of the detail route (which 410s); everything else opens the detail page. Backend: - GET /v1/templates/mine (requireAuth) — all own templates, any status, registered before /:slug so the static route always wins the match. - GET /v1/templates/:slug — now does an optional session check: the OWNER can view their own hidden/draft template (so a 'My templates' card click never dead-ends in a 404). takedown stays 410 for everyone, owner included — that's an admin decision, not the owner's to reverse. Detail page: - Fork CTA is gated on status === 'public'. For a non-public template the owner sees an amber 'not forkable — re-share from the Publish tab' notice plus a 'Manage in server' link, instead of a Fork button that would fail silently. Verified: - GET /v1/templates/mine → marco's 1 template; 401 without auth - Owner GET of a hidden template → 200 status:hidden; anon → 404 - Dashboard nav shows Marketplace (screenshot) - /templates 'My templates' toggle → only own template, public badge, sort tabs hidden (screenshot)
2026-05-20 17:18:58 +02:00
// Takedown is an admin decision — sealed for everyone, including the owner.
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
return reply.code(410).send({
error: 'taken_down',
reason: row.template.takedownReason,
});
}
if (row.template.status !== 'public') {
feat(marketplace): dashboard nav link + My-templates filter The logged-in user can now reach the marketplace and filter to their own templates. Dashboard nav: - Added 'Marketplace' item (Overview · Servers · Marketplace · Audit · Settings). /templates page — login-aware: - Detects session via /v1/auth/me. Logged-in users get a 'Dashboard' + '+ New server' header instead of 'Home' + 'Start building'. - New [All templates | My templates] scope toggle, shown only when logged in. - 'My templates' loads GET /v1/templates/mine and shows EVERY status the user owns (public / hidden / draft / takedown) with a colored status badge on each card — so a template you unshared doesn't appear to have vanished. - Sort tabs (trending/top/newest) hide in 'mine' scope — meaningless for a handful of own templates. Category filter + search still apply (client-side). - Takedown cards link to the source server's Publish tab instead of the detail route (which 410s); everything else opens the detail page. Backend: - GET /v1/templates/mine (requireAuth) — all own templates, any status, registered before /:slug so the static route always wins the match. - GET /v1/templates/:slug — now does an optional session check: the OWNER can view their own hidden/draft template (so a 'My templates' card click never dead-ends in a 404). takedown stays 410 for everyone, owner included — that's an admin decision, not the owner's to reverse. Detail page: - Fork CTA is gated on status === 'public'. For a non-public template the owner sees an amber 'not forkable — re-share from the Publish tab' notice plus a 'Manage in server' link, instead of a Fork button that would fail silently. Verified: - GET /v1/templates/mine → marco's 1 template; 401 without auth - Owner GET of a hidden template → 200 status:hidden; anon → 404 - Dashboard nav shows Marketplace (screenshot) - /templates 'My templates' toggle → only own template, public badge, sort tabs hidden (screenshot)
2026-05-20 17:18:58 +02:00
// hidden / draft — visible only to the owner (optional auth check).
const session = await getSession(req.cookies['bmm_session']);
const isOwner = session != null && session.userId === row.template.ownerUserId;
if (!isOwner) {
return reply.code(404).send({ error: 'not_found' });
}
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
}
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) => {
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
const Params = z.object({ slug: z.string().regex(SLUG_REGEX) });
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
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<string, unknown>;
}>;
const fullSpec = {
name: template.title,
description: template.shortDescription,
tools: toolsRaw.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema as Record<string, never>,
// 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);
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
// 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);
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
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));
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
// 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;
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
if (b.data.status === 'takedown') {
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
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');
}
}
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
await db
.update(mcpServers)
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
.set({
status: 'paused',
containerId: null,
publicUrl: null,
hostPort: null,
updatedAt: new Date(),
})
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
.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,
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
metadata: { ...b.data, stoppedContainers },
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
ipAddress: req.ip,
});
fix(security): template integration sovereign audit + critical fixes P0 — three critical issues found by tracing every attack vector on the template publish + fork + render path. All three fixed and verified with attack tests. FIX A — Takedown actually stops malicious containers PATCH /v1/admin/templates with status=takedown previously only updated mcp_servers.status to 'paused' in the DB. The Docker container kept running and serving traffic on its allocated port — takedown was cosmetic. Now the endpoint enumerates every fork's container, calls 'docker rm -f' on each, clears container_id/public_url/host_port in the DB, and returns the stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic. Verified: takedown stopped container f5632962, port 4109 connection refused. FIX B — Reject specEdit on fork A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would enter the spec-edit branch, merge edits into the cached spec, but the worker reads the pre-built template code (separate cache key), ignoring the merged spec entirely. User thinks they changed something; deployed container behaves as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer pointing to the Iterate flow. FIX C — templateId validation via Redis fork-ref templateId on POST /v1/servers was user-controlled and unvalidated: fork_count of any template could be pumped, mcp_servers got garbage template_id rows, takedown cascade would miss the bogus rows. Fork endpoint now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL). Server-create requires the ref to exist AND match the submitted templateId. Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired. DEFENSE-IN-DEPTH — Hardened static checks Banned patterns (added): Function\s*\(['"`] — Function('code')() form, no 'new' needed \bimport\s*\( — dynamic import escapes bundle scope \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form \bsetInterval\s*\(['"`] \bfs\s*\.\s*(unlink|rmdir|rm)\b \bprocess\s*\.\s*kill\b you are now in (developer|jailbreak|dan) mode — extra jailbreak markers Hardcoded-credential patterns (new — scanForLeakedSecrets): sk-ant-(api|sid)… — Anthropic sk-… — OpenAI sk_(live|test)_… — Stripe ghp_… — GitHub PAT github_pat_… — GitHub fine-grained xox[bpoasr]-… — Slack AKIA[0-9A-Z]{16} — AWS -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG Triggered when a publisher pasted their key into the prompt and Claude embedded it literally in the generated code. Publish-blocking. Verified attack: smuggled 'Function("return 1")' into a build's generated_code, attempted publish → 422 publish_blocked. Slug regex tightened — fork + detail routes now require ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through '../admin', long strings, mixed case). UI warning — Publish-as-template form now shows an amber callout listing what's scanned and explicitly stating egress allowlisting is roadmap, not enforced today (was misleading: the field was collected, never enforced). TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with severity, status, and rationale for what's deferred. UI polish globals.css — select/input/textarea/button get color-scheme: dark + custom chevron + option styling so Chrome's native popdown stops rendering as a white OS-themed widget on dark pages. The /templates category dropdown was the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
return reply.send({ ok: true, stoppedContainers });
feat(marketplace): template publish + fork + voting/ranking + admin moderation What this enables: - A user builds an MCP server. If others would benefit, they click 'Publish as template' on their server detail page. The spec + pre-rendered TypeScript snapshot is preserved. - Visitors browse /templates, filter by category, sort by trending/top/newest. Each template card shows fork count + active deployment count as natural manipulation-resistant popularity signal. - /templates/[slug] shows the full plan: tool list with input schemas, required-credential explanations (with 'how to get one' deep links), and a collapsible code preview so users can audit before forking. - Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and pre-fills Step 2 with the template's parsed spec. Forker only fills in their own credentials. mcp_servers.template_id is recorded; template.fork_count is bumped atomically. Each fork gets its own isolated container with its own port, its own AES-256 secrets — the template author has zero visibility into the fork's traffic or data. - Admin /admin/templates moderation: verify quality templates (shows shield badge in marketplace), hide low-effort ones, takedown anything malicious. Takedowns cascade-pause every fork container — owners must re-deploy. Why template+fork instead of shared-container: - Shared containers would mean the publisher's quota + their secrets + their logs are exposed to forkers. Bad ergonomics, bad security, bad ownership. - Templates/forks decouple the spec (shared, vouched-for) from the runtime (isolated per user). Network-effect moat without the trust collapse. Why no 5-star voting in v1: - Manipulation-anfällig, empty lists without adoption. We use fork count + active deploys + verified badge. Trending algorithm: score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1) Real signal, no brigading attack surface. Backend: - New schema: templates table (16 cols incl. tools_schema, generated_code, required_secrets, allowedDomains, status enum, verified, fork_count). - mcp_servers.template_id FK + idx for fork lookup. - @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId. - preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the template's full rendered server.ts alongside the spec. Generator worker detects this and skips the render step — uses the audited pre-built code verbatim. Banned-pattern re-scan at publish time. - routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via SQL. UI: - /templates marketplace with trending/top/newest tabs, category filter, search. Cards show forks + live count + author + verified badge. - /templates/[slug] full detail with tools, credentials-with-hints, expandable code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer. - /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec pre-filled, fork banner at top with link back to template. - /servers/[id] new Publish tab with title, category, descriptions, per-secret hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key). - /admin/templates moderation with verify/hide/takedown actions. - Marketing nav now includes /templates. Verified end-to-end: - Published Echo Demo Template from marco@test.local's live server - Marketplace lists it correctly with stats - Detail page renders with all sections - Fork CTA navigates to wizard with ?template= param - Wizard skips Step 1, shows fork banner, pre-fills spec - Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND render), container live on :4109 with proper OAuth 401 → token → 200 flow - DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id populated on the fork - /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
});
// unused-import guard
void gte;
void sql;
}