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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user