buildmymcpserver/scripts/setup-runner-tls.sh

218 lines
8.3 KiB
Bash
Raw Normal View History

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
#!/usr/bin/env bash
# setup-runner-tls.sh
#
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# One-time host setup for per-runner TLS via path-routing on
# mcp.buildmymcpserver.com. Run as root on the BMM host AFTER you've done
# the prereqs below. Idempotent — safe to re-run.
#
# Why path-routing instead of per-subdomain: Cloudflare's free Universal
# SSL only covers ONE-level wildcards (*.buildmymcpserver.com). A
# two-level wildcard (*.mcp.buildmymcpserver.com) needs CF Advanced
# Certificate Manager ($10/mo) OR a Let's Encrypt wildcard via certbot
# + DNS-01. To keep recurring cost at zero, we serve every runner under
# the single subdomain mcp.buildmymcpserver.com — which IS covered by
# Universal SSL — and route by path: https://mcp../<slug>/mcp.
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
#
# What it does:
# 1. Creates /opt/buildmymcpserver/runner-map/ (volume-mounted into bmm-api
# and bmm-generator — they drop one .conf fragment per live runner)
# 2. Installs an nginx vhost that catches *.mcp.buildmymcpserver.com,
# reads slug→port from a combined map file, and reverse-proxies to the
# runner on localhost
# 3. Installs a systemd service that inotify-watches the map dir, combines
# all fragments into a single map file, and reloads nginx on any change
#
# After this script:
# - In docker-compose.prod.yml, add a volume mount to BOTH api and generator:
# volumes:
# - /opt/buildmymcpserver/runner-map:/var/runner-map
# - In .env.production, add:
# MCP_DOMAIN=mcp.buildmymcpserver.com
# - docker compose up -d --force-recreate api generator
# - From now on every deployed runner gets https://<slug>.mcp.buildmymcpserver.com
#
# ─── PREREQS (do these in Cloudflare dashboard before running) ───────────
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# A. DNS: Add an A-record 'mcp.buildmymcpserver.com' → 213.239.213.217
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
# Proxy status: Proxied (orange cloud)
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# (The wildcard *.mcp record from earlier setup is no longer
# needed for path-routing — safe to leave or delete.)
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
# B. SSL: Cloudflare → SSL/TLS → Origin Server → Create Certificate
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# (likely already done — the same Origin cert from the prior
# setup attempt covers mcp.buildmymcpserver.com too, since
# you generated it with all four hostnames in the SAN list).
# Path: /etc/ssl/buildmymcpserver/mcp-runners.crt (644)
# /etc/ssl/buildmymcpserver/mcp-runners.key (600 root:root)
# C. SSL mode: Cloudflare → SSL/TLS → Overview → "Full (strict)"
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
#
# Run: sudo bash scripts/setup-runner-tls.sh
set -euo pipefail
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root (sudo bash $0)."
exit 1
fi
MAP_DIR="/opt/buildmymcpserver/runner-map"
COMBINED="/opt/buildmymcpserver/runner-map.combined"
VHOST_DST="/etc/nginx/sites-available/bmm-mcp-runners"
VHOST_LNK="/etc/nginx/sites-enabled/bmm-mcp-runners"
CERT="/etc/ssl/buildmymcpserver/mcp-runners.crt"
KEY="/etc/ssl/buildmymcpserver/mcp-runners.key"
echo "─── checking prereqs ───────────────────────────────────────"
for f in "$CERT" "$KEY"; do
if [[ ! -f "$f" ]]; then
echo "MISSING: $f — see PREREQS at the top of this script."
exit 1
fi
done
if ! command -v inotifywait >/dev/null; then
echo "Installing inotify-tools…"
apt-get update -qq
apt-get install -y -qq inotify-tools
fi
echo "─── creating map dir + initial combined file ──────────────"
mkdir -p "$MAP_DIR"
chmod 755 "$MAP_DIR"
touch "$COMBINED"
chmod 644 "$COMBINED"
echo "─── writing nginx vhost ──────────────────────────────────"
cat > "$VHOST_DST" <<'NGINX'
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# BMM per-runner proxy via PATH routing on mcp.buildmymcpserver.com.
# The combined snippet file (regenerated by the bmm-api and bmm-generator
# containers + reloaded by the systemd inotify watcher) is a sequence of
# `if ($bmm_slug = "<slug>") { set $bmm_port <port>; }` lines that map the
# slug captured from the URL path to the local runner port.
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
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
server_name mcp.buildmymcpserver.com;
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
ssl_certificate /etc/ssl/buildmymcpserver/mcp-runners.crt;
ssl_certificate_key /etc/ssl/buildmymcpserver/mcp-runners.key;
client_max_body_size 4M;
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# Cheap health probe for monitoring — doesn't go through the slug router.
location = /health {
return 200 "ok\n";
add_header Content-Type text/plain;
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
}
# RFC 9728 for path-routed resources:
# /<slug>/mcp derives metadata at /.well-known/oauth-protected-resource/<slug>/mcp.
# Route that well-known URL back to the same runner while preserving only the
# resource sub-path after the slug.
location ~ ^/\.well-known/oauth-protected-resource/(?<bmm_slug>[a-z0-9][a-z0-9-]*)(?<bmm_path>/.*)?$ {
set $bmm_port "";
include /opt/buildmymcpserver/runner-map.combined;
if ($bmm_port = "") {
return 404;
}
proxy_pass http://127.0.0.1:$bmm_port/.well-known/oauth-protected-resource$bmm_path;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
}
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
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# /<slug>/<rest> → 127.0.0.1:<port>/<rest>
location ~ ^/(?<bmm_slug>[a-z0-9][a-z0-9-]*)(?<bmm_path>/.*)?$ {
set $bmm_port "";
include /opt/buildmymcpserver/runner-map.combined;
# Unknown slug — return 404 instead of a confusing default.
if ($bmm_port = "") {
return 404;
}
# Default sub-path to / when client asked for /<slug> with no trailing slash.
set $bmm_target_path $bmm_path;
if ($bmm_target_path = "") {
set $bmm_target_path "/";
}
proxy_pass http://127.0.0.1:$bmm_port$bmm_target_path;
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
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# MCP uses Streamable HTTP — disable buffering so response chunks flow.
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
}
fix(tls): pivot per-runner TLS to path-routing on single subdomain The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:30 +02:00
# Root of mcp.buildmymcpserver.com — nothing to serve here.
location = / {
return 404;
}
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
}
NGINX
ln -sf "$VHOST_DST" "$VHOST_LNK"
echo "─── writing systemd watcher service ──────────────────────"
cat > /etc/systemd/system/bmm-runner-map.service <<EOF
[Unit]
Description=BMM runner-map combiner + nginx reload
After=network.target
[Service]
Type=simple
ExecStartPre=/bin/bash -c 'cat ${MAP_DIR}/*.conf 2>/dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload || true'
ExecStart=/bin/bash -c 'while inotifywait -q -e create,modify,delete,moved_to,moved_from ${MAP_DIR}; do cat ${MAP_DIR}/*.conf 2>/dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload; done'
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now bmm-runner-map
echo "─── verifying nginx config + reload ──────────────────────"
nginx -t
nginx -s reload || true
echo ""
echo "─── DONE ─────────────────────────────────────────────────"
echo ""
echo "Next steps (one-time):"
echo ""
echo "1) Edit /opt/buildmymcpserver/docker-compose.prod.yml — add to BOTH"
echo " the 'api' and 'generator' services:"
echo ""
echo " volumes:"
echo " - /opt/buildmymcpserver/runner-map:/var/runner-map"
echo ""
echo "2) Edit /opt/buildmymcpserver/.env.production — add:"
echo ""
echo " MCP_DOMAIN=mcp.buildmymcpserver.com"
echo ""
echo "3) Restart api + generator so they pick up the env + volume:"
echo ""
echo " cd /opt/buildmymcpserver"
echo " docker compose --env-file .env.production -f docker-compose.prod.yml \\"
echo " up -d --force-recreate api generator"
echo ""
echo "Test (after at least one runner has been deployed):"
echo " curl -I https://<slug>.mcp.buildmymcpserver.com/health"
echo ""
echo "If you ever need to verify the map state:"
echo " cat ${COMBINED}"
echo " systemctl status bmm-runner-map"