buildmymcpserver/apps/api/src/config.ts

52 lines
2.0 KiB
TypeScript
Raw Normal View History

import { z } from 'zod';
const Env = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
DATABASE_URL: z.string(),
REDIS_URL: z.string().default('redis://localhost:6379'),
PORT: z.coerce.number().default(4000),
NEXT_PUBLIC_APP_URL: z.string().default('http://localhost:3001'),
OAUTH_KEY_DIR: z.string().default('./keys'),
ANTHROPIC_API_KEY: z.string().optional(),
SECRETS_ENCRYPTION_KEY: z
.string()
.min(64, '32 bytes hex required')
.default('0000000000000000000000000000000000000000000000000000000000000000'),
CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'),
feat(admin): password-auth admin panel with 8 pages + 15 API endpoints Schema migrations: - users.is_admin boolean - users.password_hash text (scrypt N=16384, 16-byte salt) - users.last_login_at timestamp - organizations.suspended + suspended_reason - admin_settings table (DB-stored prompt override + future settings) Auth (@bmm/auth): - hashPassword + verifyPassword via node:crypto scrypt (no extra dep) - loginWithPassword: scrypt-verifies, issues 30-day session, updates last_login_at - seedAdmin: idempotent upsert keyed on email; creates org + membership on first run - AuthedUser now carries isAdmin flag API: - POST /v1/auth/admin/login (email + password) — 300ms throttle on failure - requireAdmin preHandler — 401 if no session, 403 if non-admin - Bootstrap: api on boot calls seedAdmin(ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_NAME) if env present. Idempotent. Admin API routes (all gated by requireAdmin): - GET /v1/admin/overview (totals, trends 7d, server-status breakdown, builds 24h, recent activity) - GET /v1/admin/users (search, per-row org + plan + serverCount) - PATCH /v1/admin/users/:id (isAdmin, name) - DELETE /v1/admin/users/:id (self-delete blocked) - GET /v1/admin/orgs (member + server counts) - PATCH /v1/admin/orgs/:id (plan, quota, suspended; cascades to mcp_servers.status=paused on suspend) - GET /v1/admin/servers (cross-org with status filter) - POST /v1/admin/servers/:id/rebuild (re-queues build using last prompt) - DELETE /v1/admin/servers/:id - GET /v1/admin/builds (status filter, error messages, prompt previews) - GET /v1/admin/builds/:id/logs - GET /v1/admin/audit (system-wide with user email join) - GET /v1/admin/system (DB ping, Redis ping, BullMQ queue depth, docker ps count) - GET /v1/admin/prompt (builtin + override + updatedAt) - PATCH /v1/admin/prompt (value: string | null) — saves DB override or drops it UI (apps/web/app/admin/*): - /admin/login — password form, separate from /login magic-link - AdminLayout — Linear-style sidebar (8 nav items), bottom panel with user email + 'user view' shortcut + logout, client-side requireAdmin guard with redirect - /admin — overview dashboard with 4 metric cards, 2 panels (status + 24h builds), recent activity table linking to full audit - /admin/users — search + admin toggle + delete (self-delete blocked) - /admin/orgs — plan/quota/suspend actions via prompts - /admin/servers — cross-org table with rebuild + delete actions, status filter - /admin/builds — every build cross-fleet with error vs prompt preview - /admin/audit — system-wide log + CSV export + filter dropdowns - /admin/system — auto-refreshing 5s health probes for Postgres, Redis, queue, Docker - /admin/prompt — live editor for the LLM system prompt with built-in baseline, override-state badge, drop-override action, diff preview, save-as-override End-to-end verified: login as marco.frangiskatos@gmail.com + Melusa112233.*, every admin page returns 200, admin login + overview tested via screenshot, docker probe returns true count of running MCP containers.
2026-05-19 23:01:26 +02:00
ADMIN_EMAIL: z.string().email().optional(),
ADMIN_PASSWORD: z.string().min(8).optional(),
ADMIN_NAME: z.string().optional(),
GOOGLE_OAUTH_ID: z.string().optional(),
GOOGLE_OAUTH_SECRET: z.string().optional(),
});
export const config = Env.parse({
NODE_ENV: process.env.NODE_ENV,
DATABASE_URL: process.env.DATABASE_URL,
REDIS_URL: process.env.REDIS_URL,
PORT: process.env.PORT,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
OAUTH_KEY_DIR: process.env.OAUTH_KEY_DIR,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
SECRETS_ENCRYPTION_KEY: process.env.SECRETS_ENCRYPTION_KEY,
feat(admin): password-auth admin panel with 8 pages + 15 API endpoints Schema migrations: - users.is_admin boolean - users.password_hash text (scrypt N=16384, 16-byte salt) - users.last_login_at timestamp - organizations.suspended + suspended_reason - admin_settings table (DB-stored prompt override + future settings) Auth (@bmm/auth): - hashPassword + verifyPassword via node:crypto scrypt (no extra dep) - loginWithPassword: scrypt-verifies, issues 30-day session, updates last_login_at - seedAdmin: idempotent upsert keyed on email; creates org + membership on first run - AuthedUser now carries isAdmin flag API: - POST /v1/auth/admin/login (email + password) — 300ms throttle on failure - requireAdmin preHandler — 401 if no session, 403 if non-admin - Bootstrap: api on boot calls seedAdmin(ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_NAME) if env present. Idempotent. Admin API routes (all gated by requireAdmin): - GET /v1/admin/overview (totals, trends 7d, server-status breakdown, builds 24h, recent activity) - GET /v1/admin/users (search, per-row org + plan + serverCount) - PATCH /v1/admin/users/:id (isAdmin, name) - DELETE /v1/admin/users/:id (self-delete blocked) - GET /v1/admin/orgs (member + server counts) - PATCH /v1/admin/orgs/:id (plan, quota, suspended; cascades to mcp_servers.status=paused on suspend) - GET /v1/admin/servers (cross-org with status filter) - POST /v1/admin/servers/:id/rebuild (re-queues build using last prompt) - DELETE /v1/admin/servers/:id - GET /v1/admin/builds (status filter, error messages, prompt previews) - GET /v1/admin/builds/:id/logs - GET /v1/admin/audit (system-wide with user email join) - GET /v1/admin/system (DB ping, Redis ping, BullMQ queue depth, docker ps count) - GET /v1/admin/prompt (builtin + override + updatedAt) - PATCH /v1/admin/prompt (value: string | null) — saves DB override or drops it UI (apps/web/app/admin/*): - /admin/login — password form, separate from /login magic-link - AdminLayout — Linear-style sidebar (8 nav items), bottom panel with user email + 'user view' shortcut + logout, client-side requireAdmin guard with redirect - /admin — overview dashboard with 4 metric cards, 2 panels (status + 24h builds), recent activity table linking to full audit - /admin/users — search + admin toggle + delete (self-delete blocked) - /admin/orgs — plan/quota/suspend actions via prompts - /admin/servers — cross-org table with rebuild + delete actions, status filter - /admin/builds — every build cross-fleet with error vs prompt preview - /admin/audit — system-wide log + CSV export + filter dropdowns - /admin/system — auto-refreshing 5s health probes for Postgres, Redis, queue, Docker - /admin/prompt — live editor for the LLM system prompt with built-in baseline, override-state badge, drop-override action, diff preview, save-as-override End-to-end verified: login as marco.frangiskatos@gmail.com + Melusa112233.*, every admin page returns 200, admin login + overview tested via screenshot, docker probe returns true count of running MCP containers.
2026-05-19 23:01:26 +02:00
CONTROL_PLANE_PUBLIC_URL: process.env.CONTROL_PLANE_PUBLIC_URL,
ADMIN_EMAIL: process.env.ADMIN_EMAIL,
ADMIN_PASSWORD: process.env.ADMIN_PASSWORD,
ADMIN_NAME: process.env.ADMIN_NAME,
GOOGLE_OAUTH_ID: process.env.GOOGLE_OAUTH_ID,
GOOGLE_OAUTH_SECRET: process.env.GOOGLE_OAUTH_SECRET,
});
fix(security): sovereign-audit — close 2 HIGH + 3 MEDIUM findings Full reasoning-based audit of all 10 zones. 11 findings, all confirmed real, zero false positives. 5 fixed now, 6 deferred to a justified backlog. API-SERVERS-001 (HIGH) — DELETE /v1/servers/:id orphaned the container The route deleted the DB row but never stopped the Docker container — it kept running forever on its host port, still serving traffic with the user's secrets baked into its env. The takedown path got stopContainer in an earlier commit; this sibling path was missed. DELETE now tears the container down first. Verified: deleted 'gfgfg' — container 23e0c55c gone, :4110 connection-refused after. INFRA-001 (HIGH) — SECRETS_ENCRYPTION_KEY zero-default usable in production The AES-256-GCM key defaults to 64 zeros and passes the min(64) check. A prod deploy that forgot to set it booted silently with every secret encrypted under a public key. config.ts now throws on boot when NODE_ENV=production and the key is still the placeholder. Verified: prod boot with the zero key is REFUSED. API-SERVERS-002 (MEDIUM) — WS build stream had no authorization GET /v1/builds/:id/stream streamed build logs with no auth, while its REST twin checks orgId. Now authenticates from the session cookie and rejects builds outside the caller's org. Verified: no cookie -> 'unauthorized'; cross-org build -> 'not_found'; own build -> streams (no regression). OAUTH-001 (MEDIUM) — authorization code consumption was not atomic The 'already used?' check and the 'mark used' write were separate statements — two requests racing the same code could both mint tokens. Now a conditional UPDATE ... WHERE consumed_at IS NULL RETURNING; the loser of the race gets zero rows and invalid_grant. OAUTH-002 (MEDIUM) — 'plain' PKCE accepted, contradicting AS metadata The AS metadata advertises code_challenge_methods_supported: ['S256'] but /oauth/authorize accepted 'plain'. Authorize is now z.literal('S256') and pkceVerify dropped the plain branch. Verified: authorize with plain -> 400. Deferred to backlog (documented in TEMPLATE_SECURITY_AUDIT.md is template-only; this audit's findings are in the commit + certification): GENERATOR-001 — secrets via docker -e (visible in docker inspect); needs --env-file rework RUNNER-001 — generated containers run as root; needs USER node + build re-test AUTH-001 — no rate limit on magic-link / oauth register; needs @fastify/rate-limit GENERATOR-002— allocatePort check/bind race; low, self-heals on rebuild AUTH-002 — expired magic_links/sessions/oauth rows never purged; needs a cron FEATURES-001 — tool-call metering not wired (metrics always 0); Sprint 4 by plan
2026-05-20 18:15:03 +02:00
// INFRA-001: refuse to boot in production with the placeholder encryption key.
// The zero-key passes the min(64) length check but would render every stored
// secret effectively plaintext.
const ZERO_KEY = '0'.repeat(64);
if (config.NODE_ENV === 'production' && config.SECRETS_ENCRYPTION_KEY === ZERO_KEY) {
throw new Error(
'SECRETS_ENCRYPTION_KEY is the all-zero placeholder. Set a real 32-byte hex key ' +
'(openssl rand -hex 32) before running in production.',
);
}
export type Config = z.infer<typeof Env>;