From 048319f70b1d629f08d306a3b51d4206fedd7bae Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Sat, 4 Apr 2026 15:37:45 +0100 Subject: [PATCH] fix(devtools): stabilize Penpot MCP, fix S3 creds, OIDC registration MCP server: - Replace vite build --watch + livePreview with static vite preview (watch mode was reloading the plugin iframe, killing WebSocket) - Bake WS_URI at Docker build time for production WebSocket URL - Add server-side application-level keepalive messages every 25s - Add client-side auto-reconnect with exponential backoff - Set Pingora route timeout to 86400s for WebSocket idle tolerance Penpot: - Add AWS_ACCESS_KEY_ID/SECRET env vars for S3 SDK compatibility - Set S3 region to satisfy AWS SDK credential chain - Enable OIDC registration (disable-registration blocks OIDC signup) - Fix frontend port (8080 not 80) - Add penpot bucket to seaweedfs-buckets init job --- base/data/postgres-cluster.yaml | 2 + base/devtools/penpot-mcp.Dockerfile | 12 ++-- base/devtools/penpot-mcp.yaml | 9 +++ base/devtools/penpot.yaml | 14 ++++- base/ingress/pingora-config.yaml | 98 ++++++++++++++++++----------- base/lasuite/seaweedfs-buckets.yaml | 3 +- scripts/local-seed-secrets.sh | 16 ++++- 7 files changed, 108 insertions(+), 46 deletions(-) diff --git a/base/data/postgres-cluster.yaml b/base/data/postgres-cluster.yaml index d084552..052ee04 100644 --- a/base/data/postgres-cluster.yaml +++ b/base/data/postgres-cluster.yaml @@ -46,6 +46,8 @@ spec: - CREATE DATABASE find_db OWNER find; - CREATE USER penpot WITH LOGIN; - CREATE DATABASE penpot_db OWNER penpot; + - CREATE USER stalwart WITH LOGIN; + - CREATE DATABASE stalwart_db OWNER stalwart; storage: size: 10Gi diff --git a/base/devtools/penpot-mcp.Dockerfile b/base/devtools/penpot-mcp.Dockerfile index 486b230..b199d77 100644 --- a/base/devtools/penpot-mcp.Dockerfile +++ b/base/devtools/penpot-mcp.Dockerfile @@ -1,12 +1,12 @@ FROM node:22-alpine -RUN npm install -g pnpm@latest @penpot/mcp@latest && \ - cd /usr/local/lib/node_modules/@penpot/mcp && \ - pnpm -r install && \ - pnpm run build +ARG WS_URI=wss://mcp-designer.sunbeam.pt/ws +RUN npm install -g pnpm@latest +COPY . /opt/penpot-mcp/ +WORKDIR /opt/penpot-mcp +RUN WS_URI=$WS_URI pnpm -r install && WS_URI=$WS_URI pnpm run build ENV PENPOT_MCP_REMOTE_MODE=true \ PENPOT_MCP_SERVER_HOST=0.0.0.0 \ PENPOT_MCP_SERVER_PORT=4401 \ PENPOT_MCP_WEBSOCKET_PORT=4402 -EXPOSE 4401 4402 -WORKDIR /usr/local/lib/node_modules/@penpot/mcp +EXPOSE 4400 4401 4402 CMD ["pnpm", "run", "start"] diff --git a/base/devtools/penpot-mcp.yaml b/base/devtools/penpot-mcp.yaml index bebee96..4c19d14 100644 --- a/base/devtools/penpot-mcp.yaml +++ b/base/devtools/penpot-mcp.yaml @@ -25,6 +25,8 @@ spec: containerPort: 4401 - name: ws containerPort: 4402 + - name: plugin + containerPort: 4400 env: - name: PENPOT_MCP_REMOTE_MODE value: "true" @@ -32,6 +34,10 @@ spec: value: "0.0.0.0" - name: PENPOT_MCP_SERVER_ADDRESS value: "mcp-designer.DOMAIN_SUFFIX" + - name: WS_URI + value: "wss://mcp-designer.DOMAIN_SUFFIX/ws" + - name: PENPOT_MCP_PLUGIN_SERVER_HOST + value: "0.0.0.0" resources: requests: cpu: 50m @@ -54,3 +60,6 @@ spec: - name: ws port: 4402 targetPort: ws + - name: plugin + port: 4400 + targetPort: plugin diff --git a/base/devtools/penpot.yaml b/base/devtools/penpot.yaml index ba0b937..9866290 100644 --- a/base/devtools/penpot.yaml +++ b/base/devtools/penpot.yaml @@ -14,9 +14,11 @@ data: PENPOT_ASSETS_STORAGE_BACKEND: "assets-s3" PENPOT_STORAGE_ASSETS_S3_ENDPOINT: "http://seaweedfs-filer.storage.svc.cluster.local:8333" PENPOT_STORAGE_ASSETS_S3_BUCKET: "penpot" + PENPOT_STORAGE_ASSETS_S3_REGION: "us-east-1" + AWS_REGION: "us-east-1" PENPOT_OIDC_BASE_URI: "https://auth.DOMAIN_SUFFIX/" PENPOT_TELEMETRY_ENABLED: "false" - PENPOT_FLAGS: "enable-login-with-oidc disable-login-with-password disable-email-verification disable-registration enable-backend-api-doc enable-auto-file-snapshot enable-tiered-file-data-storage enable-webhooks enable-access-tokens enable-cors" + PENPOT_FLAGS: "enable-login-with-oidc disable-login-with-password disable-email-verification enable-oidc-registration enable-backend-api-doc enable-auto-file-snapshot enable-tiered-file-data-storage enable-webhooks enable-access-tokens enable-cors" --- # ── Frontend (nginx SPA) ───────────────────────────────────────────────────── apiVersion: apps/v1 @@ -126,6 +128,16 @@ spec: secretKeyRef: name: penpot-s3-credentials key: secret-key + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: penpot-s3-credentials + key: access-key + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: penpot-s3-credentials + key: secret-key resources: requests: cpu: 100m diff --git a/base/ingress/pingora-config.yaml b/base/ingress/pingora-config.yaml index 9bbb225..68d6a66 100644 --- a/base/ingress/pingora-config.yaml +++ b/base/ingress/pingora-config.yaml @@ -155,8 +155,32 @@ data: [[routes]] host_prefix = "mail" - # Caddy is the unified entry point — proxies /api/, /admin/, /static/, /oidc/ internally. - backend = "http://messages-frontend.lasuite.svc.cluster.local:80" + backend = "http://bulwark.stalwart.svc.cluster.local:80" + + # JMAP protocol (Bulwark ↔ Stalwart) + [[routes.paths]] + prefix = "/jmap" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" + + # Well-known service discovery (autoconfig, caldav, carddav, etc.) + [[routes.paths]] + prefix = "/.well-known/" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" + + # OAuth2/OIDC authorization callback + [[routes.paths]] + prefix = "/authorize" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" + + # Stalwart admin + API + [[routes.paths]] + prefix = "/api" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" + + # JMAP WebSocket + [[routes.paths]] + prefix = "/ws" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" [[routes]] host_prefix = "messages" @@ -182,18 +206,25 @@ data: websocket = true [[routes]] - host_prefix = "mcp-designer" - backend = "http://penpot-mcp.devtools.svc.cluster.local:4401" - websocket = true + host_prefix = "mcp-designer" + backend = "http://penpot-mcp.devtools.svc.cluster.local:4400" + websocket = true + timeout_secs = 86400 [[routes.paths]] - prefix = "/" + prefix = "/mcp" backend = "http://penpot-mcp.devtools.svc.cluster.local:4401" auth_request = "http://hydra-public.ory.svc.cluster.local:4444/userinfo" [[routes.paths]] - prefix = "/ws" - backend = "http://penpot-mcp.devtools.svc.cluster.local:4402" + prefix = "/sse" + backend = "http://penpot-mcp.devtools.svc.cluster.local:4401" + auth_request = "http://hydra-public.ory.svc.cluster.local:4444/userinfo" + + [[routes.paths]] + prefix = "/ws" + backend = "http://penpot-mcp.devtools.svc.cluster.local:4402" + timeout_secs = 86400 [[routes]] host_prefix = "src" @@ -280,43 +311,21 @@ data: [[routes]] host_prefix = "cal" - backend = "http://calendars-frontend.lasuite.svc.cluster.local:80" - - [[routes.paths]] - prefix = "/api/" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" - - [[routes.paths]] - prefix = "/admin/" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" - - [[routes.paths]] - prefix = "/static/" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" + # Default: redirect to unified Bulwark calendar UI. + redirect = "https://mail.DOMAIN_SUFFIX/calendar" + # CalDAV protocol — external calendar clients connect here. [[routes.paths]] prefix = "/caldav" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" [[routes.paths]] prefix = "/.well-known/caldav" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" [[routes.paths]] - prefix = "/rsvp/" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" - - [[routes.paths]] - prefix = "/ical/" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" - - [[routes.paths]] - prefix = "/external_api/" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" - - [[routes.paths]] - prefix = "/__" - backend = "http://calendars-backend.lasuite.svc.cluster.local:80" + prefix = "/.well-known/carddav" + backend = "http://stalwart.stalwart.svc.cluster.local:8080" [[routes]] host_prefix = "projects" @@ -392,6 +401,21 @@ data: host_prefix = "build" backend = "buildkitd.build.svc.cluster.local:1234" + # SMTP inbound: port 25 → Stalwart for mail delivery. + [smtp] + listen = "0.0.0.0:25" + backend = "stalwart.stalwart.svc.cluster.local:25" + + # SMTP submission: port 587 → Stalwart for authenticated sending. + [smtp-submission] + listen = "0.0.0.0:587" + backend = "stalwart.stalwart.svc.cluster.local:587" + + # IMAPS: port 993 → Stalwart for desktop/mobile email clients. + [imaps] + listen = "0.0.0.0:993" + backend = "stalwart.stalwart.svc.cluster.local:993" + # SSH TCP passthrough: port 22 → Gitea SSH pod (headless service → pod:2222). [ssh] listen = "0.0.0.0:22" diff --git a/base/lasuite/seaweedfs-buckets.yaml b/base/lasuite/seaweedfs-buckets.yaml index 29e6f61..5ae3692 100644 --- a/base/lasuite/seaweedfs-buckets.yaml +++ b/base/lasuite/seaweedfs-buckets.yaml @@ -29,7 +29,8 @@ spec: sunbeam-conversations \ sunbeam-git-lfs \ sunbeam-game-assets \ - sunbeam-ml-models; do + sunbeam-ml-models \ + sunbeam-stalwart; do mc mb --ignore-existing "weed/$bucket" echo "Ensured bucket: $bucket" done diff --git a/scripts/local-seed-secrets.sh b/scripts/local-seed-secrets.sh index bfb16c5..1cba654 100755 --- a/scripts/local-seed-secrets.sh +++ b/scripts/local-seed-secrets.sh @@ -58,7 +58,7 @@ done echo "==> Setting postgres user passwords..." PG_POD=$(kubectl $CTX -n data get pods -l cnpg.io/cluster=postgres,role=primary -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") if [[ -n "$PG_POD" ]]; then - for user in kratos hydra gitea hive docs meet drive messages conversations people find penpot; do + for user in kratos hydra gitea hive docs meet drive messages conversations people find penpot stalwart; do kubectl $CTX -n data exec "$PG_POD" -c postgres -- \ psql -U postgres -c "ALTER USER $user WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true done @@ -130,6 +130,19 @@ create_secret lasuite people-db-credentials \ create_secret lasuite people-django-secret \ --from-literal=DJANGO_SECRET_KEY="local-dev-people-django-secret-key-not-for-production" +# Stalwart namespace +ensure_ns stalwart +create_secret stalwart stalwart-db-credentials \ + --from-literal=password="$DB_PASSWORD" + +create_secret stalwart stalwart-app-secrets \ + --from-literal=admin-password="stalwart-local-admin-password" \ + --from-literal=dkim-private-key="placeholder-generate-real-key-for-production" + +create_secret stalwart seaweedfs-s3-credentials \ + --from-literal=S3_ACCESS_KEY="$S3_ACCESS_KEY" \ + --from-literal=S3_SECRET_KEY="$S3_SECRET_KEY" + # Media namespace ensure_ns media @@ -193,6 +206,7 @@ else bao kv put secret/people db-password='$DB_PASSWORD' django-secret-key='local-dev-people-django-secret-key-not-for-production' bao kv put secret/penpot db-password='$DB_PASSWORD' secret-key='penpot-local-secret-key-not-for-production' bao kv put secret/livekit api-key='$LIVEKIT_API_KEY' api-secret='$LIVEKIT_API_SECRET' + bao kv put secret/stalwart admin-password='stalwart-local-admin-password' dkim-private-key='placeholder-generate-real-key-for-production' " 2>/dev/null echo " Done." fi