diff --git a/README.md b/README.md index 2bdd054e..4bc47e79 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,38 @@ bun run tauri:build # builds the app — needs Rust, the Tauri CLI and webvie --- +## Security controls + +**Bundle integrity.** Every bundle ships with a SHA-384 hash in `public/dx-maps/manifest.json` +(`bundles[].integrity`), and every copy-paste script snippet (`profiles[].embed.script`) already +uses it: + +```html + +``` + +If the file on the host is changed by anyone, even by one byte, the browser refuses to run it. +`crossorigin="anonymous"` is required for the check to work across sites, so the host must send +`Access-Control-Allow-Origin: *`. Copy the snippet again after each release, because the hash changes +with the bundle. `bun run check:sri` keeps the manifest and the built pages in step. + +
+The backend in the cloud container (v1, experimental) + +- **Locked by default.** A new PocketBase collection starts with admin-only rules; you open it + deliberately, collection by collection. +- **Dashboards hidden.** The production instance exposes only its `/api/`; its admin dashboard is + blocked at the proxy. The development dashboard is open only to the admin group. +- **No server-side code.** PocketBase's JavaScript hooks are switched off in both instances. +- **Not built yet:** per-plugin locks between plugin authors (who may edit which plugin's backend) + and isolated edge functions. Until they exist, everyone with dev access is trusted with every + plugin's backend. + +
+ +--- + ## Languages **Every map speaks 11 languages** — add `?lang=` to a map's page address, or, when a map is embedded in diff --git a/deployment/cloud/Dockerfile b/deployment/cloud/Dockerfile index 651c1a81..519b6744 100644 --- a/deployment/cloud/Dockerfile +++ b/deployment/cloud/Dockerfile @@ -7,6 +7,9 @@ FROM oven/bun:1-debian RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl unzip \ && rm -rf /var/lib/apt/lists/* COPY --from=oauth2proxy /bin/oauth2-proxy /usr/local/bin/oauth2-proxy +# Go, to build Rudder's local engine (backend/rudder-engine, go 1.22) when RUDDER_ENGINE=1. +COPY --from=golang:1.22-bookworm /usr/local/go /usr/local/go +ENV PATH="/usr/local/go/bin:${PATH}" # PocketBase, pinned and checked: the backend for plugins (a dev and a prod instance, see entry.sh). ARG PB_VERSION=0.40.4 ARG PB_SHA256=9042ec818570e79c3628dadcd0a756c1496d9e1173918ec409d133c02f82e5fa diff --git a/deployment/cloud/README.md b/deployment/cloud/README.md index 6b6ea35b..e5f0c1d3 100644 --- a/deployment/cloud/README.md +++ b/deployment/cloud/README.md @@ -67,7 +67,9 @@ there. Only then does it swap the new build in. | `PB_SUPERUSER_EMAIL`, `PB_SUPERUSER_PASSWORD` | PocketBase's superuser, set on start in both instances | | `PUBLIC_API_BASE` | the public API URL written into the build's `runtime-config.js` (e.g. `https:///api-prod`) | | `BUILD_MAX_OLD_SPACE_MB` | Node heap cap for a build (default `1536`) | +| `RUDDER_ENGINE` | `1` runs Rudder's voice-to-text engine here, fed by the browser's microphone (about 1 GB more; its model, ~742 MB, downloads once to `RUDDER_HOME`, default `/data/rudder`). Give the container at least 2 vCPUs, or transcription falls behind speech | +| `RUDDER_ALLOW_HOST` | the public host the engine accepts (it refuses others); defaults to the host in `OIDC_REDIRECT_URL` | Mount a persistent volume at **`/data`** (PocketBase data and published builds). Memory measured on the running container: about 0.45 GB idle, up to about 1.5 GB more during a build, so give it -**2.5 GB**. +**2.5 GB** (**3.5 GB** with `RUDDER_ENGINE=1`). diff --git a/deployment/cloud/entry.sh b/deployment/cloud/entry.sh index ead83270..7f2e64ec 100755 --- a/deployment/cloud/entry.sh +++ b/deployment/cloud/entry.sh @@ -15,8 +15,14 @@ bun install --frozen-lockfile || bun install if git fetch -q --depth 1 origin "$BRANCH"; then NEW=$(git rev-parse FETCH_HEAD) if [ "$OLD" != "$NEW" ]; then + CHANGED=$(git diff --name-only "$OLD" "$NEW" 2>/dev/null) git reset -q --hard FETCH_HEAD echo "$(date -u +%FT%TZ) PULLED ${OLD:0:8} -> ${NEW:0:8}" >> /tmp/pull.log + # the engine binary is untracked, so rebuild it when its source changed (next start picks it up) + if [ "${RUDDER_ENGINE:-0}" = 1 ] && echo "$CHANGED" | grep -q '^backend/rudder-engine/'; then + (cd backend/rudder-engine && go build -o ../bin/rudder-engine .) >> /tmp/pull.log 2>&1 \ + && echo "$(date -u +%FT%TZ) rudder-engine rebuilt" >> /tmp/pull.log + fi fi fi done @@ -24,6 +30,22 @@ bun install --frozen-lockfile || bun install CONFIG=/app/repo/deployment/cloud/vite.cloud.config.mjs +# Rudder (optional, RUDDER_ENGINE=1): its voice-to-text engine runs here, fed by the BROWSER's +# microphone (no server mic). The runtime and the pinned model (~742 MB) go to RUDDER_HOME, which should +# be on the persistent volume so they download once. Vite's backend plugin starts the engine and serves +# /api/rudder, which stays behind the sign-in (it is not a public route). About 1 GB resident. +if [ "${RUDDER_ENGINE:-0}" = 1 ]; then + export RUDDER_HOME="${RUDDER_HOME:-/data/rudder}" RUDDER_AUDIO=browser + # The engine refuses a non-loopback Host header, and Vite's proxy passes the public host through: + # allow the public host, defaulting to the one in OIDC_REDIRECT_URL. + if [ -z "${RUDDER_ALLOW_HOST:-}" ] && [ -n "${OIDC_REDIRECT_URL:-}" ]; then + RUDDER_ALLOW_HOST=$(printf '%s' "$OIDC_REDIRECT_URL" | sed -E 's#^[a-z]+://([^/:]+).*#\1#') + fi + export RUDDER_ALLOW_HOST="${RUDDER_ALLOW_HOST:-}" + bash /app/repo/backend/rudder-engine/setup-runtime.sh \ + || echo "[rudder] setup failed: the widget will report the engine offline" +fi + if [ -z "${OIDC_ISSUER:-}" ]; then # Open, like a laptop: no sign-in. export VITE_HOST=0.0.0.0 VITE_PORT="$PORT" @@ -53,8 +75,11 @@ for inst in dev prod; do && echo "[pb-$inst] superuser set" || echo "[pb-$inst] superuser upsert failed" fi done -pocketbase serve --http 127.0.0.1:8090 --dir /data/pb-dev --migrationsDir "$MIG" & -pocketbase serve --http 127.0.0.1:8091 --dir /data/pb-prod --migrationsDir "$MIG" & +# Server-side JS hooks OFF: point --hooksDir at a path that never exists, so a pb_hooks/ folder +# added to the repo can't run code in either instance (v1 has no per-plugin isolation for hooks). +NOHOOKS=/var/empty/pb_hooks_disabled +pocketbase serve --http 127.0.0.1:8090 --dir /data/pb-dev --migrationsDir "$MIG" --hooksDir "$NOHOOKS" & +pocketbase serve --http 127.0.0.1:8091 --dir /data/pb-prod --migrationsDir "$MIG" --hooksDir "$NOHOOKS" & bun /app/repo/deployment/cloud/admin.mjs & VITE_HOST=127.0.0.1 VITE_PORT=5174 bun x vite --config "$CONFIG" &