feat(canary): add project-root .env.example + idempotent init.sh
.env.example covers every env var the canary stack reads: - Public: APP_NAME, NGINX_HOST_PORT, PUBLIC_BASE_URL, VITE_APP_TITLE, VITE_API_URL - Anti-bot: TURNSTILE_SITE_KEY/SECRET + VITE_TURNSTILE_SITE_KEY (frontend mirror) - Operator: OPERATOR_TOKEN (admin endpoints) - DB: POSTGRES_PASSWORD + POSTGRES_DEV_PORT - Cache: REDIS_DEV_PORT - GeoIP: MAXMIND_ACCOUNT_ID/LICENSE_KEY (optional) - Webhooks: WEBHOOK_HMAC_SECRET (optional) - Fake MySQL: MYSQL_FAKE_ENABLED + MYSQL_HOST_PORT (optional, off by default) - Logging: LOG_LEVEL, LOG_FORMAT - Telemetry: OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT + Jaeger ports - Tunnel: CLOUDFLARE_TUNNEL_TOKEN (for cloudflared.compose.yml) scripts/init.sh rewritten as idempotent setup helper: - Copies .env.example -> .env on first run - Generates POSTGRES_PASSWORD + OPERATOR_TOKEN via openssl rand -hex 32 if blank - Skips already-set values (idempotent) - Conditionally fetches GeoLite2-City.mmdb when MAXMIND creds present - All sed usage is operator-side (script run-time), not Claude tool-time scripts/randomize-ports.sh kept as-is (operator helper for spinning up sibling projects on non-conflicting ports).
This commit is contained in:
parent
369c954892
commit
a2d6f09ab3
|
|
@ -0,0 +1,103 @@
|
||||||
|
# =============================================================================
|
||||||
|
# ©AngelaMos | 2026
|
||||||
|
# .env.example
|
||||||
|
# =============================================================================
|
||||||
|
# Copy this file to .env and fill in values. .env is gitignored.
|
||||||
|
# Run `just init` to auto-generate POSTGRES_PASSWORD + OPERATOR_TOKEN if empty.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public-facing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
APP_NAME=canary-token-generator
|
||||||
|
NGINX_HOST_PORT=22784
|
||||||
|
|
||||||
|
# Used to construct trigger URLs embedded in artifacts (e.g. canary docs).
|
||||||
|
# Must be the externally-reachable URL of the deployed service.
|
||||||
|
PUBLIC_BASE_URL=https://canary.your.domain
|
||||||
|
|
||||||
|
# Frontend build-time vars (baked into the bundle by Vite)
|
||||||
|
VITE_APP_TITLE=Canary Token Generator
|
||||||
|
VITE_API_URL=/api
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cloudflare Turnstile (free anti-bot)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Get keys from https://dash.cloudflare.com/?to=/:account/turnstile
|
||||||
|
# Leave blank to disable Turnstile in development.
|
||||||
|
TURNSTILE_SITE_KEY=
|
||||||
|
TURNSTILE_SECRET=
|
||||||
|
# Mirror of TURNSTILE_SITE_KEY for frontend (must be the same value)
|
||||||
|
VITE_TURNSTILE_SITE_KEY=
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Operator (you) — for /api/admin/* endpoints
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Auto-generated by scripts/init.sh if blank.
|
||||||
|
# Send as `Authorization: Bearer <token>` to access admin endpoints.
|
||||||
|
OPERATOR_TOKEN=
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Postgres
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Auto-generated by scripts/init.sh if blank.
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# Dev-only Postgres host port (preserves randomized assignment across projects)
|
||||||
|
POSTGRES_DEV_PORT=5447
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Redis (dev host port)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
REDIS_DEV_PORT=6022
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# GeoIP (optional — geolocation enrichment of triggered events)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Register free at https://www.maxmind.com/en/geolite2/signup
|
||||||
|
# scripts/init.sh fetches GeoLite2-City.mmdb when both vars are set.
|
||||||
|
MAXMIND_ACCOUNT_ID=
|
||||||
|
MAXMIND_LICENSE_KEY=
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Webhooks (optional)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# When set, webhook payloads are signed with HMAC-SHA256 and an
|
||||||
|
# X-Canary-Signature header is added.
|
||||||
|
WEBHOOK_HMAC_SECRET=
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fake MySQL TCP server (optional)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cloudflare Tunnel does not carry raw TCP — only enable when the canary
|
||||||
|
# container's port 3306 is reachable directly (e.g. on a VPS).
|
||||||
|
MYSQL_FAKE_ENABLED=false
|
||||||
|
MYSQL_HOST_PORT=33606
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FORMAT=json
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OpenTelemetry (optional — dev compose runs Jaeger)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
OTEL_ENABLED=false
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
|
|
||||||
|
# Dev port overrides for jaeger (preserve randomized values)
|
||||||
|
JAEGER_UI_PORT=16686
|
||||||
|
JAEGER_OTLP_GRPC_PORT=4317
|
||||||
|
JAEGER_OTLP_HTTP_PORT=4318
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Frontend dev (vite host port)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FRONTEND_HOST_PORT=15723
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cloudflare Tunnel (overlay)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Required if using cloudflared.compose.yml; obtain from Cloudflare Zero Trust dashboard.
|
||||||
|
CLOUDFLARE_TUNNEL_TOKEN=
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# ©AngelaMos | 2026
|
||||||
|
# init.sh
|
||||||
|
# =============================================================================
|
||||||
|
# Idempotent setup helper.
|
||||||
|
# 1. Copies .env.example -> .env if .env is missing
|
||||||
|
# 2. Generates POSTGRES_PASSWORD and OPERATOR_TOKEN if their values are blank
|
||||||
|
# 3. Downloads GeoLite2-City.mmdb when MAXMIND_ACCOUNT_ID + MAXMIND_LICENSE_KEY are set
|
||||||
|
#
|
||||||
|
# Safe to run repeatedly: skips already-set values and existing mmdb.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENV_FILE="$DIR/.env"
|
||||||
|
EXAMPLE_FILE="$DIR/.env.example"
|
||||||
|
DATA_DIR="$DIR/data"
|
||||||
|
MMDB_PATH="$DATA_DIR/GeoLite2-City.mmdb"
|
||||||
|
|
||||||
|
# ── ensure .env exists ────────────────────────────────────────────────────────
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
if [[ ! -f "$EXAMPLE_FILE" ]]; then
|
||||||
|
echo "Error: $EXAMPLE_FILE missing — cannot bootstrap .env" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp "$EXAMPLE_FILE" "$ENV_FILE"
|
||||||
|
echo " created .env from .env.example"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── secret generator ──────────────────────────────────────────────────────────
|
||||||
|
gen_secret() {
|
||||||
|
local key="$1"
|
||||||
|
local current
|
||||||
|
current="$(grep -E "^${key}=" "$ENV_FILE" | head -1 | cut -d= -f2- || true)"
|
||||||
|
|
||||||
|
if [[ -z "$current" ]]; then
|
||||||
|
local newval
|
||||||
|
newval="$(openssl rand -hex 32)"
|
||||||
|
if grep -qE "^${key}=" "$ENV_FILE"; then
|
||||||
|
sed -i "s|^${key}=.*|${key}=${newval}|" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
printf '\n%s=%s\n' "$key" "$newval" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
echo " generated $key"
|
||||||
|
else
|
||||||
|
echo " $key already set, skipping"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_secret POSTGRES_PASSWORD
|
||||||
|
gen_secret OPERATOR_TOKEN
|
||||||
|
|
||||||
|
# ── geoip database ────────────────────────────────────────────────────────────
|
||||||
|
ACCT="$(grep -E "^MAXMIND_ACCOUNT_ID=" "$ENV_FILE" | head -1 | cut -d= -f2- || true)"
|
||||||
|
KEY="$(grep -E "^MAXMIND_LICENSE_KEY=" "$ENV_FILE" | head -1 | cut -d= -f2- || true)"
|
||||||
|
|
||||||
|
if [[ -n "$ACCT" && -n "$KEY" ]]; then
|
||||||
|
if [[ ! -f "$MMDB_PATH" ]]; then
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
echo " downloading GeoLite2-City.mmdb from MaxMind..."
|
||||||
|
local_tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$local_tmp"' EXIT
|
||||||
|
if curl -sSf -u "$ACCT:$KEY" \
|
||||||
|
"https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz" \
|
||||||
|
-o "$local_tmp/geolite.tar.gz"; then
|
||||||
|
tar -xzf "$local_tmp/geolite.tar.gz" -C "$local_tmp" --strip-components=1 \
|
||||||
|
"*/GeoLite2-City.mmdb"
|
||||||
|
mv "$local_tmp/GeoLite2-City.mmdb" "$MMDB_PATH"
|
||||||
|
echo " GeoLite2-City.mmdb downloaded to $MMDB_PATH"
|
||||||
|
else
|
||||||
|
echo " Warning: GeoLite2 download failed (check MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY)" >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " GeoLite2-City.mmdb already present, skipping"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " MaxMind credentials not set in .env — geolocation will be disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── done ──────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "Setup complete. Edit .env to set PUBLIC_BASE_URL + Turnstile keys, then 'just up'."
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# AngelaMos | 2026
|
||||||
|
# randomize-ports.sh
|
||||||
|
# =============================================================================
|
||||||
|
# Picks 4 unique random ports (10000-65000) and updates:
|
||||||
|
# .env -> prod nginx + frontend ports
|
||||||
|
# .env.development -> dev nginx + frontend ports
|
||||||
|
# compose.yml -> prod default fallbacks
|
||||||
|
# dev.compose.yml -> dev default fallbacks
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
mapfile -t ports < <(python3 -c "
|
||||||
|
import random
|
||||||
|
sample = random.sample(range(10000, 65001), 4)
|
||||||
|
for p in sample: print(p)
|
||||||
|
")
|
||||||
|
|
||||||
|
PROD_NGINX=${ports[0]}
|
||||||
|
PROD_FRONTEND=${ports[1]}
|
||||||
|
DEV_NGINX=${ports[2]}
|
||||||
|
DEV_FRONTEND=${ports[3]}
|
||||||
|
|
||||||
|
echo "New ports:"
|
||||||
|
echo " prod nginx: $PROD_NGINX"
|
||||||
|
echo " prod frontend: $PROD_FRONTEND"
|
||||||
|
echo " dev nginx: $DEV_NGINX"
|
||||||
|
echo " dev frontend: $DEV_FRONTEND"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
update_env() {
|
||||||
|
local file="$1" nginx_port="$2" frontend_port="$3"
|
||||||
|
sed -i "s/^NGINX_HOST_PORT=.*/NGINX_HOST_PORT=$nginx_port/" "$file"
|
||||||
|
sed -i "s/^FRONTEND_HOST_PORT=.*/FRONTEND_HOST_PORT=$frontend_port/" "$file"
|
||||||
|
echo " updated $file"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_compose_defaults() {
|
||||||
|
local file="$1" nginx_port="$2" frontend_port="${3:-}"
|
||||||
|
sed -i "s/\${NGINX_HOST_PORT:-[0-9]\+}/\${NGINX_HOST_PORT:-$nginx_port}/g" "$file"
|
||||||
|
if [[ -n "$frontend_port" ]]; then
|
||||||
|
sed -i "s/\${FRONTEND_HOST_PORT:-[0-9]\+}/\${FRONTEND_HOST_PORT:-$frontend_port}/g" "$file"
|
||||||
|
fi
|
||||||
|
echo " updated $file"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_env "$DIR/.env" "$PROD_NGINX" "$PROD_FRONTEND"
|
||||||
|
update_env "$DIR/.env.development" "$DEV_NGINX" "$DEV_FRONTEND"
|
||||||
|
update_compose_defaults "$DIR/compose.yml" "$PROD_NGINX"
|
||||||
|
update_compose_defaults "$DIR/dev.compose.yml" "$DEV_NGINX" "$DEV_FRONTEND"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done."
|
||||||
Loading…
Reference in New Issue