2026-05-19 00:26:53 +02:00
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
|
|
|
|
const Env = z.object({
|
|
|
|
|
DATABASE_URL: z.string(),
|
|
|
|
|
REDIS_URL: z.string().default('redis://localhost:6379'),
|
|
|
|
|
ANTHROPIC_API_KEY: z.string().optional(),
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
GLM_API_KEY: z.string().optional(),
|
2026-05-19 00:26:53 +02:00
|
|
|
RUNNER_HOST: z.string().default('localhost'),
|
|
|
|
|
RUNNER_PORT_RANGE_START: z.coerce.number().default(4100),
|
|
|
|
|
RUNNER_PORT_RANGE_END: z.coerce.number().default(4999),
|
|
|
|
|
CONTROL_PLANE_URL: z.string().default('http://host.docker.internal:4000'),
|
|
|
|
|
CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'),
|
2026-05-19 00:57:23 +02:00
|
|
|
OAUTH_ISSUER: z.string().optional(),
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
MODEL_GENERATE: z.string().default('glm-4.5'),
|
2026-05-19 00:26:53 +02:00
|
|
|
MODEL_FIX: z.string().default('claude-haiku-4-5-20251001'),
|
feat: oauth refresh-token grant + per-runner subdomain TLS plumbing
OAUTH REFRESH-TOKEN
- oauth_tokens.subject column added (migration applied to prod DB): stores
the JWT sub claim from the original authorization so refreshes can
re-mint with the same identity without re-walking the (consumed) code.
- Authorization-code branch now writes subject AND uses a 30-day
expires_at for the row (was 1h — same as access token, which killed
refresh after 1h).
- New refresh_token grant branch:
* looks up token by refresh-hash + expiry
* client_id must match, client_secret verified if confidential
* RFC 8707: requested resource must equal stored resource
* OAuth 2.1 rotation: atomic UPDATE WHERE old_hash → new access JWT,
new refresh token, extended expiry; loser of a race sees invalid_grant
- Access TTL (1h) and refresh TTL (30d) extracted as constants.
Clients no longer have to re-authorize hourly. Closes Zb-001.
PER-RUNNER SUBDOMAIN TLS (Z1-002)
Code path:
- New MCP_DOMAIN env (e.g. "mcp.buildmymcpserver.com") + RUNNER_MAP_DIR
(default /var/runner-map) in generator config.
- deployContainer: writes /var/runner-map/<slug>.conf with content
"slug.MCP_DOMAIN port;" and computes publicUrl as
https://<slug>.<MCP_DOMAIN>. Falls back to http://host:port when
MCP_DOMAIN is unset (zero behaviour change until host is configured).
- stopContainer (both api/lib/docker.ts and generator/lib/deploy.ts) now
accepts an optional slug arg and removes the map fragment. Callers
(DELETE /v1/servers/:id, admin template takedown) updated.
Infra path (one-time host setup — Marco runs as root):
- scripts/setup-runner-tls.sh:
1. nginx vhost matching *.mcp.buildmymcpserver.com via regex →
reads slug→port from /opt/buildmymcpserver/runner-map.combined
2. systemd inotify service watches the map dir, combines fragments
on any change, reloads nginx
3. installs inotify-tools if missing, idempotent
- Prereqs documented at top: Cloudflare wildcard DNS proxied, Origin CA
cert for *.mcp.buildmymcpserver.com, SSL mode Full (strict).
- After running: edit docker-compose.prod.yml to mount the map dir into
api + generator, set MCP_DOMAIN in env, recreate containers.
Closes Zb-001 fully. Closes Z1-002 on the code side; one Marco-on-host
action away from closing it on the infra side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:09:06 +02:00
|
|
|
// When set (e.g. "mcp.buildmymcpserver.com"), each deployed runner gets a
|
|
|
|
|
// public URL of the form https://<slug>.<MCP_DOMAIN> instead of the legacy
|
|
|
|
|
// http://<RUNNER_HOST>:<port> form. Requires host-side nginx + DNS setup
|
|
|
|
|
// (see scripts/setup-runner-tls.sh). When unset, falls back to plain HTTP.
|
|
|
|
|
MCP_DOMAIN: z.string().optional(),
|
|
|
|
|
// Directory the generator drops per-runner map fragments into. A host-side
|
|
|
|
|
// inotify service combines them and reloads nginx. Mounted as a volume by
|
|
|
|
|
// docker-compose (see setup-runner-tls.sh).
|
|
|
|
|
RUNNER_MAP_DIR: z.string().default('/var/runner-map'),
|
2026-05-19 00:26:53 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const config = Env.parse(process.env);
|