Initial release — Sprout Nostr relay with enterprise extensions (#2)

This commit is contained in:
tlongwell-block
2026-03-09 15:15:25 -04:00
committed by GitHub
parent f158b72586
commit 058c4b92a9
153 changed files with 33675 additions and 24 deletions

54
scripts/dev-reset.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# =============================================================================
# dev-reset.sh — Tear down everything and recreate a clean environment
# =============================================================================
# Usage: ./scripts/dev-reset.sh
#
# Stops all services, removes ALL volumes (data is lost!), brings everything
# back up fresh, and runs migrations.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${BLUE}[dev-reset]${NC} $*"; }
success(){ echo -e "${GREEN}[dev-reset]${NC} ✅ $*"; }
warn() { echo -e "${YELLOW}[dev-reset]${NC} ⚠️ $*"; }
error() { echo -e "${RED}[dev-reset]${NC} ❌ $*" >&2; }
cd "${REPO_ROOT}"
# ---- Confirm ----------------------------------------------------------------
if [[ "${1:-}" != "--yes" ]]; then
echo -e "${YELLOW}⚠️ WARNING: This will DELETE all local data (mysql, typesense volumes).${NC}"
echo -e " Redis data is ephemeral and always wiped on restart."
echo ""
read -r -p "Are you sure? [y/N] " confirm
case "${confirm}" in
[yY][eE][sS]|[yY]) ;;
*)
log "Aborted."
exit 0
;;
esac
fi
# ---- Tear down --------------------------------------------------------------
log "Stopping and removing containers + volumes..."
docker compose down -v --remove-orphans 2>/dev/null || true
success "Containers and volumes removed"
# ---- Bring back up ----------------------------------------------------------
log "Recreating environment..."
exec "${SCRIPT_DIR}/dev-setup.sh"

155
scripts/dev-setup.sh Executable file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env bash
# =============================================================================
# dev-setup.sh — One-shot local dev environment setup
# =============================================================================
# Usage: ./scripts/dev-setup.sh
#
# Starts all Docker services, waits for healthy, runs migrations, prints
# connection info and next steps.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
TIMEOUT=120 # seconds to wait for services to become healthy
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log() { echo -e "${BLUE}[dev-setup]${NC} $*"; }
success(){ echo -e "${GREEN}[dev-setup]${NC} ✅ $*"; }
warn() { echo -e "${YELLOW}[dev-setup]${NC} ⚠️ $*"; }
error() { echo -e "${RED}[dev-setup]${NC} ❌ $*" >&2; }
# ---- Preflight checks -------------------------------------------------------
if ! command -v docker &>/dev/null; then
error "Docker not found. Install Docker Desktop: https://www.docker.com/products/docker-desktop/"
exit 1
fi
if ! docker info &>/dev/null; then
error "Docker daemon is not running. Start Docker Desktop and try again."
exit 1
fi
cd "${REPO_ROOT}"
# ---- Start services ---------------------------------------------------------
log "Starting services..."
docker compose up -d
# ---- Wait for healthy -------------------------------------------------------
wait_healthy() {
local service="$1"
local container="$2"
local elapsed=0
local interval=3
log "Waiting for ${service} to be healthy..."
while true; do
local status
status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found")
case "${status}" in
healthy)
success "${service} is healthy"
return 0
;;
unhealthy)
error "${service} is unhealthy. Check logs: docker logs ${container}"
return 1
;;
not_found)
error "Container ${container} not found"
return 1
;;
esac
if [[ ${elapsed} -ge ${TIMEOUT} ]]; then
error "Timed out waiting for ${service} (${TIMEOUT}s). Check: docker logs ${container}"
return 1
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
echo -n "."
done
}
echo ""
wait_healthy "MySQL" "sprout-mysql"
wait_healthy "Redis" "sprout-redis"
wait_healthy "Typesense" "sprout-typesense"
echo ""
# ---- Run migrations ---------------------------------------------------------
log "Running database migrations..."
MIGRATION_DIR="${REPO_ROOT}/migrations"
if [[ ! -d "${MIGRATION_DIR}" ]]; then
warn "No migrations directory found at ${MIGRATION_DIR}. Skipping."
else
# Check if sqlx CLI is available (preferred)
if command -v sqlx &>/dev/null; then
log "Using sqlx CLI for migrations..."
DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \
sqlx migrate run --source "${MIGRATION_DIR}"
success "Migrations applied via sqlx"
else
# Fallback: run SQL files directly via mysql in the container
log "sqlx CLI not found — applying migrations via mysql CLI..."
shopt -s nullglob
SQL_FILES=("${MIGRATION_DIR}"/*.sql)
shopt -u nullglob
if [[ ${#SQL_FILES[@]} -eq 0 ]]; then
warn "No .sql files found in ${MIGRATION_DIR}. Skipping."
else
for sql_file in "${SQL_FILES[@]}"; do
filename="$(basename "${sql_file}")"
log " Applying ${filename}..."
docker exec -i sprout-mysql \
mysql -u sprout -psprout_dev sprout \
< "${sql_file}"
done
success "Migrations applied via mysql"
fi
fi
fi
# ---- Print connection info --------------------------------------------------
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Sprout dev environment is ready! 🌱${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BLUE}MySQL${NC} mysql://sprout:sprout_dev@localhost:3306/sprout"
echo -e " ${BLUE}Redis${NC} redis://localhost:6379"
echo -e " ${BLUE}Typesense${NC} http://localhost:8108 (key: sprout_dev_key)"
echo -e " ${BLUE}Adminer${NC} http://localhost:8082 (DB browser)"
echo -e " ${BLUE}Keycloak${NC} http://localhost:8180 (admin / admin — local OAuth testing)"
echo ""
echo -e " ${YELLOW}Next steps:${NC}"
echo -e " cp .env.example .env # configure your environment"
echo -e " bash scripts/setup-keycloak.sh # configure Keycloak for OAuth testing (optional)"
echo -e " cargo run -p sprout-relay # start the relay server"
echo -e " ./scripts/run-tests.sh # run all tests"
echo ""
echo -e " ${YELLOW}Useful commands:${NC}"
echo -e " docker compose ps # check service status"
echo -e " docker compose logs -f # tail all logs"
echo -e " docker compose down # stop services (keep data)"
echo -e " ./scripts/dev-reset.sh # wipe and start fresh"
echo ""
exit 0

208
scripts/run-tests.sh Executable file
View File

@@ -0,0 +1,208 @@
#!/usr/bin/env bash
# =============================================================================
# run-tests.sh — Run Sprout test suite
# =============================================================================
# Usage:
# ./scripts/run-tests.sh # run all tests (default)
# ./scripts/run-tests.sh unit # unit tests only (no infra needed)
# ./scripts/run-tests.sh integration # integration tests only
# ./scripts/run-tests.sh all # explicit all
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
MODE="${1:-all}"
TIMEOUT=60 # seconds to wait for services if starting them
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${BLUE}[run-tests]${NC} $*"; }
success(){ echo -e "${GREEN}[run-tests]${NC} ✅ $*"; }
warn() { echo -e "${YELLOW}[run-tests]${NC} ⚠️ $*"; }
error() { echo -e "${RED}[run-tests]${NC} ❌ $*" >&2; }
section(){ echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; }
cd "${REPO_ROOT}"
# ---- Load .env if present ---------------------------------------------------
if [[ -f ".env" ]]; then
log "Loading .env..."
set -o allexport
# shellcheck disable=SC1091
source .env
set +o allexport
else
# Use defaults matching docker-compose.yml
export DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout"
export REDIS_URL="redis://localhost:6379"
export TYPESENSE_API_KEY="sprout_dev_key"
export TYPESENSE_URL="http://localhost:8108"
fi
# ---- Track results ----------------------------------------------------------
declare -a PASSED=()
declare -a FAILED=()
run_test_step() {
local name="$1"
shift
log "Running: ${name}"
if "$@"; then
success "${name} passed"
PASSED+=("${name}")
else
error "${name} FAILED"
FAILED+=("${name}")
fi
}
# ---- Check / start infra (for integration tests) ----------------------------
services_healthy() {
local mysql_ok redis_ok
mysql_ok=$(docker inspect --format='{{.State.Health.Status}}' sprout-mysql 2>/dev/null || echo "not_found")
redis_ok=$(docker inspect --format='{{.State.Health.Status}}' sprout-redis 2>/dev/null || echo "not_found")
[[ "${mysql_ok}" == "healthy" && "${redis_ok}" == "healthy" ]]
}
ensure_services() {
if services_healthy; then
success "Services already healthy"
return 0
fi
warn "Services not running — starting them..."
docker compose up -d
local elapsed=0
local interval=3
while ! services_healthy; do
if [[ ${elapsed} -ge ${TIMEOUT} ]]; then
error "Timed out waiting for services (${TIMEOUT}s)"
return 1
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
echo -n "."
done
echo ""
success "Services healthy"
# Ensure migrations are current
ensure_migrations
}
ensure_migrations() {
log "Ensuring migrations are current..."
local migration_dir="${REPO_ROOT}/migrations"
if [[ ! -d "${migration_dir}" ]]; then
warn "No migrations directory. Skipping."
return 0
fi
if command -v sqlx &>/dev/null; then
DATABASE_URL="${DATABASE_URL}" sqlx migrate run --source "${migration_dir}" 2>/dev/null \
&& success "Migrations current" \
|| warn "sqlx migrate run failed — DB may be out of date"
else
# Fallback: apply all SQL files (idempotent if schema uses IF NOT EXISTS)
shopt -s nullglob
local sql_files=("${migration_dir}"/*.sql)
shopt -u nullglob
for sql_file in "${sql_files[@]}"; do
docker exec -i sprout-mysql \
mysql -u sprout -psprout_dev sprout \
< "${sql_file}" &>/dev/null || true
done
success "Migrations applied (mysql fallback)"
fi
}
# ---- Unit tests (no infra needed) -------------------------------------------
run_unit_tests() {
section "Unit Tests (no infra required)"
run_test_step "sprout-core tests" \
cargo test -p sprout-core --lib -- --nocapture
run_test_step "sprout-auth unit tests" \
cargo test -p sprout-auth --lib -- --nocapture
}
# ---- DB / integration tests (infra required) --------------------------------
run_integration_tests() {
section "Integration Tests (requires running services)"
ensure_services
run_test_step "sprout-db tests" \
cargo test -p sprout-db -- --nocapture
run_test_step "sprout-auth integration tests" \
cargo test -p sprout-auth --test '*' -- --nocapture 2>/dev/null || \
run_test_step "sprout-auth (no integration tests found)" true
run_test_step "workspace integration tests" \
cargo test --test '*' -- --nocapture 2>/dev/null || \
run_test_step "workspace integration tests (none found)" true
}
# ---- Main -------------------------------------------------------------------
START_TIME=$(date +%s)
case "${MODE}" in
unit)
run_unit_tests
;;
integration)
run_integration_tests
;;
all|*)
run_unit_tests
run_integration_tests
;;
esac
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
# ---- Summary ----------------------------------------------------------------
section "Test Summary"
echo ""
echo -e " Duration: ${ELAPSED}s"
echo ""
if [[ ${#PASSED[@]} -gt 0 ]]; then
echo -e " ${GREEN}Passed (${#PASSED[@]}):${NC}"
for t in "${PASSED[@]}"; do
echo -e " ${GREEN}✅${NC} ${t}"
done
fi
if [[ ${#FAILED[@]} -gt 0 ]]; then
echo ""
echo -e " ${RED}Failed (${#FAILED[@]}):${NC}"
for t in "${FAILED[@]}"; do
echo -e " ${RED}❌${NC} ${t}"
done
echo ""
exit 1
fi
echo ""
success "All tests passed! 🎉"
exit 0

273
scripts/setup-keycloak.sh Executable file
View File

@@ -0,0 +1,273 @@
#!/usr/bin/env bash
# =============================================================================
# setup-keycloak.sh — Configure Keycloak for local OAuth testing
# =============================================================================
# Usage: ./scripts/setup-keycloak.sh
#
# Creates the `sprout` realm with:
# - sprout-desktop client (public, direct access grants)
# - Test users: tyler, alice, bob, charlie (password: password123)
# - nostr_pubkey custom attribute on each user
# - Protocol mapper: nostr_pubkey → JWT access token claim
#
# Keycloak is a LOCAL DEV STAND-IN for Okta/generic OIDC providers.
# It is NOT a production dependency.
#
# Prerequisites:
# - Keycloak running at http://localhost:8180 (docker compose up -d)
# - curl and jq installed
# =============================================================================
set -euo pipefail
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8180}"
ADMIN_USER="${KEYCLOAK_ADMIN:-admin}"
ADMIN_PASS="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
REALM="sprout"
CLIENT_ID="sprout-desktop"
TIMEOUT=120 # seconds to wait for Keycloak
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${BLUE}[keycloak-setup]${NC} $*"; }
success(){ echo -e "${GREEN}[keycloak-setup]${NC} ✅ $*"; }
warn() { echo -e "${YELLOW}[keycloak-setup]${NC} ⚠️ $*"; }
error() { echo -e "${RED}[keycloak-setup]${NC} ❌ $*" >&2; }
# ---- Preflight --------------------------------------------------------------
for cmd in curl jq; do
if ! command -v "$cmd" &>/dev/null; then
error "Required tool not found: $cmd"
exit 1
fi
done
# ---- Wait for Keycloak ------------------------------------------------------
log "Waiting for Keycloak at ${KEYCLOAK_URL}..."
elapsed=0
interval=5
until curl -sf "${KEYCLOAK_URL}/health/ready" -o /dev/null 2>/dev/null; do
if [[ ${elapsed} -ge ${TIMEOUT} ]]; then
error "Timed out waiting for Keycloak (${TIMEOUT}s). Is it running?"
error " docker compose up -d keycloak"
exit 1
fi
echo -n "."
sleep "${interval}"
elapsed=$((elapsed + interval))
done
echo ""
success "Keycloak is ready"
# ---- Get admin token --------------------------------------------------------
log "Authenticating as admin..."
ADMIN_TOKEN=$(curl -sf \
-X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=admin-cli" \
-d "username=${ADMIN_USER}" \
-d "password=${ADMIN_PASS}" \
-d "grant_type=password" \
| jq -r '.access_token')
if [[ -z "${ADMIN_TOKEN}" || "${ADMIN_TOKEN}" == "null" ]]; then
error "Failed to get admin token. Check KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD."
exit 1
fi
success "Admin token obtained"
# Helper: authenticated API call
kc() {
local method="$1"; shift
local path="$1"; shift
curl -sf \
-X "${method}" \
"${KEYCLOAK_URL}/admin/realms${path}" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"$@"
}
kc_root() {
local method="$1"; shift
local path="$1"; shift
curl -sf \
-X "${method}" \
"${KEYCLOAK_URL}/admin${path}" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"$@"
}
# ---- Create realm -----------------------------------------------------------
log "Checking for realm '${REALM}'..."
REALM_EXISTS=$(curl -sf \
"${KEYCLOAK_URL}/admin/realms/${REALM}" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o /dev/null -w "%{http_code}" 2>/dev/null || true)
if [[ "${REALM_EXISTS}" == "200" ]]; then
warn "Realm '${REALM}' already exists — skipping creation"
else
log "Creating realm '${REALM}'..."
curl -sf \
-X POST "${KEYCLOAK_URL}/admin/realms" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"realm": "'"${REALM}"'",
"displayName": "Sprout",
"enabled": true,
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": false,
"editUsernameAllowed": false,
"bruteForceProtected": false
}'
success "Realm '${REALM}' created"
fi
# ---- Create client ----------------------------------------------------------
log "Checking for client '${CLIENT_ID}'..."
EXISTING_CLIENT=$(kc GET "/${REALM}/clients?clientId=${CLIENT_ID}" | jq -r '.[0].id // empty')
if [[ -n "${EXISTING_CLIENT}" ]]; then
warn "Client '${CLIENT_ID}' already exists (id: ${EXISTING_CLIENT}) — skipping creation"
CLIENT_UUID="${EXISTING_CLIENT}"
else
log "Creating client '${CLIENT_ID}'..."
kc POST "/${REALM}/clients" -d '{
"clientId": "'"${CLIENT_ID}"'",
"name": "Sprout Desktop",
"enabled": true,
"publicClient": true,
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"http://localhost:*",
"sprout://*"
],
"webOrigins": ["*"],
"protocol": "openid-connect"
}'
CLIENT_UUID=$(kc GET "/${REALM}/clients?clientId=${CLIENT_ID}" | jq -r '.[0].id')
success "Client '${CLIENT_ID}' created (id: ${CLIENT_UUID})"
fi
# ---- Add nostr_pubkey protocol mapper ---------------------------------------
log "Checking for nostr_pubkey protocol mapper..."
MAPPER_EXISTS=$(kc GET "/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models" \
| jq -r '.[] | select(.name == "nostr_pubkey") | .id // empty')
if [[ -n "${MAPPER_EXISTS}" ]]; then
warn "Protocol mapper 'nostr_pubkey' already exists — skipping"
else
log "Creating nostr_pubkey → JWT claim mapper..."
kc POST "/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models" -d '{
"name": "nostr_pubkey",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "nostr_pubkey",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "nostr_pubkey",
"jsonType.label": "String"
}
}'
success "Protocol mapper 'nostr_pubkey' created"
fi
# ---- Create users -----------------------------------------------------------
# Format: "username:nostr_pubkey"
declare -a USERS=(
"tyler:e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
"alice:953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"
"bob:bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"
"charlie:554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"
)
for entry in "${USERS[@]}"; do
username="${entry%%:*}"
pubkey="${entry##*:}"
log "Checking for user '${username}'..."
EXISTING_USER=$(kc GET "/${REALM}/users?username=${username}&exact=true" | jq -r '.[0].id // empty')
if [[ -n "${EXISTING_USER}" ]]; then
warn "User '${username}' already exists (id: ${EXISTING_USER}) — updating nostr_pubkey attribute"
kc PUT "/${REALM}/users/${EXISTING_USER}" -d '{
"attributes": {
"nostr_pubkey": ["'"${pubkey}"'"]
}
}'
success "User '${username}' updated"
else
log "Creating user '${username}'..."
kc POST "/${REALM}/users" -d '{
"username": "'"${username}"'",
"email": "'"${username}"'@sprout.local",
"firstName": "'"${username^}"'",
"lastName": "Test",
"enabled": true,
"emailVerified": true,
"credentials": [{
"type": "password",
"value": "password123",
"temporary": false
}],
"attributes": {
"nostr_pubkey": ["'"${pubkey}"'"]
}
}'
success "User '${username}' created (nostr_pubkey: ${pubkey:0:16}...)"
fi
done
# ---- Summary ----------------------------------------------------------------
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Keycloak realm setup complete! 🔑${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BLUE}Admin UI${NC} http://localhost:8180 (admin / admin)"
echo -e " ${BLUE}Realm${NC} ${REALM}"
echo -e " ${BLUE}Client${NC} ${CLIENT_ID} (public, direct access grants)"
echo ""
echo -e " ${BLUE}Test users${NC} (password: password123)"
echo -e " tyler e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
echo -e " alice 953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"
echo -e " bob bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"
echo -e " charlie 554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"
echo ""
echo -e " ${YELLOW}Relay env vars for Keycloak:${NC}"
echo -e " OKTA_JWKS_URI=http://localhost:8180/realms/sprout/protocol/openid-connect/certs"
echo -e " OKTA_ISSUER=http://localhost:8180/realms/sprout"
echo -e " OKTA_AUDIENCE=sprout-desktop"
echo -e " OKTA_PUBKEY_CLAIM=nostr_pubkey"
echo ""
echo -e " ${YELLOW}Get a token (direct grant):${NC}"
echo -e " curl -s -X POST http://localhost:8180/realms/sprout/protocol/openid-connect/token \\"
echo -e " -d 'client_id=sprout-desktop&grant_type=password&username=tyler&password=password123' \\"
echo -e " | jq -r .access_token"
echo ""
exit 0