#!/bin/bash set -e # ============= ROTATED SECRETS OVERLAY ============= # On emulator snapshot resume, the host injects freshly-generated secrets into # /run/stack-auth/rotated-secrets.env before supervisorctl restarts us. Sourcing # here lets a fast-restart pick up new values without a full container restart. if [ -f /run/stack-auth/rotated-secrets.env ]; then set -a # shellcheck disable=SC1091 source /run/stack-auth/rotated-secrets.env set +a fi # ============= FORWARD MOCK OAUTH SERVER ============= # Start socat to forward port 32202 for mock-oauth-server if enabled if [ "$STACK_FORWARD_MOCK_OAUTH_SERVER" = "true" ]; then socat TCP-LISTEN:32202,fork,reuseaddr TCP:host.docker.internal:32202 & fi # ============= ENV VARS ============= if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ]; then for v in STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do if [ -z "${!v:-}" ]; then echo "$v must be set in local-emulator mode (injected by the QEMU VM)." >&2 exit 1 fi done export STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY else export STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)} export STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)} export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)} fi export NEXT_PUBLIC_STACK_PROJECT_ID=internal export NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY} if [ -n "${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY:-}" ]; then export STACK_SECRET_SERVER_KEY=${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY} fi if [ -n "${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-}" ]; then export STACK_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY} fi # ============= HEXCLAVE ↔ STACK URL ENV MIRROR ============= # The dashboard bundle inlines BOTH process.env.NEXT_PUBLIC_HEXCLAVE_* and # process.env.NEXT_PUBLIC_STACK_* references as sentinels (dual-read). At # runtime the sentinel-replace loop only substitutes a sentinel when the # corresponding env var is set — but the dashboard's fallback chain # (`HEXCLAVE_X ?? STACK_X`) treats an unreplaced sentinel as truthy, so it # would pick the literal sentinel string instead of the real URL whenever # only one of the two env names is set by the self-host operator. # Mirror the URL trio HEXCLAVE → STACK and STACK → HEXCLAVE before the # sentinel-replace runs, so both sentinels resolve to the same real value # regardless of which name the operator chose. for _legacy in STACK_API_URL STACK_DASHBOARD_URL STACK_SVIX_SERVER_URL; do _new=HEXCLAVE_${_legacy#STACK_} _legacy_full=NEXT_PUBLIC_${_legacy} _new_full=NEXT_PUBLIC_${_new} _legacy_val=${!_legacy_full:-} _new_val=${!_new_full:-} if [ -n "$_new_val" ] && [ -z "$_legacy_val" ]; then export "$_legacy_full=$_new_val" elif [ -n "$_legacy_val" ] && [ -z "$_new_val" ]; then export "$_new_full=$_legacy_val" fi done export NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL=${NEXT_PUBLIC_STACK_DASHBOARD_URL} # Hexclave rebrand: the port-prefix var was renamed outright to # NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX. The dashboard bundle's post-build sentinel # is STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX, and the sentinel # substitution loop below derives the env var name from the sentinel — so this # MUST export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX or the sentinel never resolves. # Accept the legacy NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX as input for back-compat with # existing self-host configs. export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}} PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX} export NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL="http://localhost:${PORT_PREFIX}01" export NEXT_PUBLIC_BROWSER_STACK_API_URL=${NEXT_PUBLIC_STACK_API_URL} export NEXT_PUBLIC_SERVER_STACK_API_URL="http://localhost:${PORT_PREFIX}02" export BACKEND_PORT=${BACKEND_PORT:-${PORT_PREFIX}02} export DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT_PREFIX}01} export USE_INLINE_ENV_VARS=true if [ -z "${NEXT_PUBLIC_STACK_SVIX_SERVER_URL}" ]; then export NEXT_PUBLIC_STACK_SVIX_SERVER_URL=${STACK_SVIX_SERVER_URL} fi # ============= MIGRATIONS ============= should_run_migrations=true if [ "$STACK_SKIP_MIGRATIONS" = "true" ] || [ "$STACK_RUN_MIGRATIONS" = "false" ]; then should_run_migrations=false fi if [ "$should_run_migrations" = "false" ]; then echo "Skipping migrations." else echo "Running migrations..." cd apps/backend node dist/db-migrations.mjs migrate cd ../.. fi should_run_seed_script=true if [ "$STACK_SKIP_SEED_SCRIPT" = "true" ] || [ "$STACK_RUN_SEED_SCRIPT" = "false" ]; then should_run_seed_script=false fi if [ "$should_run_seed_script" = "false" ]; then echo "Skipping seed script." else echo "Running seed script..." cd apps/backend node dist/db-migrations.mjs seed cd ../.. fi # ============= LOCAL EMULATOR: BOOTSTRAP INTERNAL API KEY SET ============= # The build-time seed ran without any keys (the VM generates random ones on # first boot). The slim image strips apps/backend/dist so we can't re-run the # full seed here. Instead, targeted-upsert the internal api key set with the # VM-supplied keys: # - pck: used by stack-cli to auth against /api/v1/internal/local-emulator/project # - ssk/sak: required by the emulator's own dashboard (StackServerApp ctor # throws without ssk). User-app flows don't use these — per-project # credentials come from the /local-emulator/project route. if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ] && [ -n "${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-}" ] && [ -n "${STACK_DATABASE_CONNECTION_STRING:-}" ]; then # Validate the keys are hex-only to defuse any SQL-injection risk (the VM # generates them via `openssl rand -hex 32`, so this is an assert, not a filter). for varname in STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do val="${!varname:-}" if [ -z "$val" ]; then echo "ERROR: $varname is not set; refusing to bootstrap internal api key set." >&2 exit 1 fi if ! printf '%s' "$val" | grep -Eq '^[0-9a-fA-F]+$'; then echo "ERROR: $varname is not hex-only; refusing to bootstrap internal api key set." >&2 exit 1 fi done echo "Bootstrapping internal API key set (emulator runtime)..." psql "$STACK_DATABASE_CONNECTION_STRING" -v ON_ERROR_STOP=1 </dev/null || true) if [ -n "$files" ]; then echo "$files" | xargs sed -i "s${delimiter}${escaped_sentinel}${delimiter}${escaped_value}${delimiter}g" fi done echo "Sentinel replacement complete." touch "$SENTINEL_MARKER" fi # ============= START BACKEND AND DASHBOARD ============= echo "Starting backend on port $BACKEND_PORT..." cd "$WORK_DIR" PORT=$BACKEND_PORT HOSTNAME=0.0.0.0 node apps/backend/server.js & echo "Starting dashboard on port $DASHBOARD_PORT..." PORT=$DASHBOARD_PORT HOSTNAME=0.0.0.0 node apps/dashboard/server.js & # Wait for both to finish wait -n