Files
Stalwart-webui/scripts/dev-token.sh
T
Steven RYDELL f4c8f8f21c feat(dev): add one-command local Stalwart test server + workflow docs
Add docker-compose.yml: a disposable Stalwart instance with a fixed dev
admin account (STALWART_RECOVERY_ADMIN), matching the credentials
scripts/dev-token.ps1 already expected. Wired up via new npm scripts
(dev:server, dev:server:down, dev:server:logs).

scripts/dev-token.ps1 was previously untracked (the whole scripts/
directory was gitignored) even though it's part of the documented dev
workflow — un-ignored it, and added scripts/dev-token.sh, a POSIX
equivalent for non-Windows shells and AI agents without PowerShell.

New DEVELOPMENT.md documents the full loop end-to-end (start server,
get a token, run the dev server, verify), written so it's actionable by
both humans and AI coding agents without needing a browser. Linked from
AGENTS.md (Commands) and README.md (Getting started).

Verified manually: docker compose up brings the server to a healthy
state, /api/auth + /auth/token issue a working bearer token, and
/jmap/session returns 200 with it end-to-end.
2026-08-01 18:05:43 +02:00

64 lines
2.5 KiB
Bash

#!/usr/bin/env bash
# Local development only. Generates a fresh OAuth access token from the local
# Stalwart dev container (see docker-compose.yml) and writes it to
# .env.development.local (gitignored). Bash equivalent of dev-token.ps1, for
# non-Windows shells (and AI agents without PowerShell).
#
# Tokens expire after 1 hour; re-run this script and restart "npm run dev"
# when the UI starts returning 401s.
# The credentials below belong to the disposable local Stalwart container.
set -euo pipefail
API_BASE_URL="${1:-http://localhost:8080}"
ACCOUNT_NAME="${2:-admin@example.org}"
ACCOUNT_SECRET="${3:-c8321iEscHDy0GWV}"
REDIRECT_URI="http://localhost:3005/oauth/callback"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
b64url() {
base64 | tr '+/' '-_' | tr -d '=\n'
}
VERIFIER="$(head -c 48 /dev/urandom | b64url | head -c 64)"
CHALLENGE="$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | b64url)"
STATE="$(head -c 16 /dev/urandom | xxd -p)"
AUTH_PAYLOAD=$(cat <<JSON
{"type":"authCode","accountName":"$ACCOUNT_NAME","accountSecret":"$ACCOUNT_SECRET","clientId":"stalwart-webui","redirectUri":"$REDIRECT_URI","scope":"openid email profile offline_access","state":"$STATE","codeChallenge":"$CHALLENGE","codeChallengeMethod":"S256"}
JSON
)
AUTH_RESPONSE=$(curl -sf "$API_BASE_URL/api/auth" -X POST -H "Content-Type: application/json" -d "$AUTH_PAYLOAD")
CLIENT_CODE=$(printf '%s' "$AUTH_RESPONSE" | grep -o '"client_code":"[^"]*"' | cut -d'"' -f4)
if [ -z "$CLIENT_CODE" ]; then
echo "Unexpected /api/auth response: $AUTH_RESPONSE" >&2
exit 1
fi
TOKEN_RESPONSE=$(curl -sf "$API_BASE_URL/auth/token" -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CLIENT_CODE" \
--data-urlencode "code_verifier=$VERIFIER" \
--data-urlencode "client_id=stalwart-webui" \
--data-urlencode "redirect_uri=$REDIRECT_URI")
ACCESS_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
EXPIRES_IN=$(printf '%s' "$TOKEN_RESPONSE" | grep -o '"expires_in":[0-9]*' | cut -d':' -f2)
if [ -z "$ACCESS_TOKEN" ]; then
echo "Unexpected /auth/token response: $TOKEN_RESPONSE" >&2
exit 1
fi
ENV_PATH="$ROOT_DIR/.env.development.local"
cat > "$ENV_PATH" <<EOF
# Generated by scripts/dev-token.sh - gitignored, do not commit.
# Empty base URL: API calls stay same-origin and go through the Vite proxy.
VITE_API_BASE_URL=
VITE_ACCESS_TOKEN=$ACCESS_TOKEN
EOF
echo "Token written to $ENV_PATH (expires in ${EXPIRES_IN}s). Restart 'npm run dev' to pick it up."