@
All checks were successful
Deploy to Production / deploy (push) Successful in 1m22s

feat(billing): in-app embedded Stripe checkout + webhook hardening

Checkout previously used hosted ui_mode → window.location to checkout.stripe.com,
which pops out of the installed PWA into the system browser. Switch to embedded:

- API: ui_mode embedded_page (stripe-node v22 / API 2025-10 renamed the enum),
  return_url instead of success/cancel_url, returns client_secret.
- web: @stripe/react-stripe-js EmbeddedCheckout mounted in an in-app modal;
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY baked at build (Dockerfile arg + compose arg).
- .env.production.example: full Stripe section (was missing) + admin-email
  placeholder (INF-001).

Also bundled (same files): BILL-002 invoice.paid resets quota only on
subscription_cycle; BILL-003 webhook dedup rolled back on handler failure;
BILL-001 change-plan writes plan locally; BILL-004 webhook cross-checks
sub.customer before trusting metadata.orgId; INF-003 API routed off the raw
docker.sock through a locked-down tecnativa/docker-socket-proxy (CONTAINERS+POST).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Marco Sadjadi
2026-05-29 20:56:40 +02:00
parent 9d5386ccba
commit cf423de3d5
8 changed files with 256 additions and 20 deletions

View File

@@ -54,6 +54,36 @@ services:
timeout: 5s
retries: 20
# Restricted Docker API gateway for the control plane. The API only needs to
# stop/remove generated containers (`docker rm -f`), so it talks to this proxy
# — which exposes ONLY the containers endpoints + write methods — instead of
# mounting the raw root-equivalent /var/run/docker.sock. A compromised API can
# no longer build images, create privileged containers, exec, or mount host
# paths. (INF-003) NOTE: the generator still mounts the raw socket because it
# legitimately builds+runs containers (an inherently privileged operation);
# that residual is tracked in the audit backlog (rootless buildkit / build VM).
docker-socket-proxy:
image: tecnativa/docker-socket-proxy:0.2.0
container_name: bmm-docker-proxy
restart: unless-stopped
environment:
CONTAINERS: 1
POST: 1
# everything else stays at the image default (0 = blocked)
IMAGES: 0
BUILD: 0
NETWORKS: 0
VOLUMES: 0
EXEC: 0
INFO: 0
AUTH: 0
SECRETS: 0
SWARM: 0
SYSTEM: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [bmm-network]
api:
build:
context: .
@@ -61,10 +91,13 @@ services:
container_name: bmm-api
restart: unless-stopped
env_file: .env.production
environment:
# Route docker CLI calls through the restricted proxy instead of a raw
# socket mount. (INF-003)
DOCKER_HOST: tcp://docker-socket-proxy:2375
ports:
- "127.0.0.1:${API_PORT:-4000}:4000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- bmm_keys:/app/apps/api/keys
# Per-runner nginx snippets — written by the generator, deleted by the
# api when a server is removed. The host-side systemd watcher combines
@@ -79,6 +112,8 @@ services:
condition: service_healthy
redis:
condition: service_healthy
docker-socket-proxy:
condition: service_started
web:
build:
@@ -86,6 +121,9 @@ services:
dockerfile: apps/web/Dockerfile
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:?set NEXT_PUBLIC_API_URL in .env.production}
# Publishable (not secret) — baked into the client bundle for embedded
# checkout. Default empty so the build never fails when it's unset.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:-}
container_name: bmm-web
restart: unless-stopped
env_file: .env.production
@@ -109,6 +147,12 @@ services:
DATABASE_URL: postgresql://${POSTGRES_USER:-bmm}:${POSTGRES_PASSWORD}@127.0.0.1:${POSTGRES_PORT:-5440}/${POSTGRES_DB:-bmm}
REDIS_URL: redis://127.0.0.1:${REDIS_PORT:-6390}
volumes:
# SECURITY (INF-003): the generator mounts the RAW docker socket because it
# builds images and runs containers — inherently root-equivalent on this
# host, and a socket-proxy can't filter that (container-create with host
# binds is the dangerous primitive it legitimately needs). It is NOT
# internet-facing (driven only by the Redis build queue). Real remediation
# = rootless buildkit or a dedicated build VM; tracked in the audit backlog.
- /var/run/docker.sock:/var/run/docker.sock
- bmm_build_context:/app/build-context
# Same runner-map mount as the api — generator drops the snippet on