From a2d6f09ab3e85ef0437ea1988b9a4e4f70455f52 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 10 May 2026 05:27:30 -0400 Subject: [PATCH] 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). --- .../canary-token-generator/.env.example | 103 ++++++++++++++++++ .../canary-token-generator/scripts/init.sh | 84 ++++++++++++++ .../scripts/randomize-ports.sh | 56 ++++++++++ 3 files changed, 243 insertions(+) create mode 100644 PROJECTS/beginner/canary-token-generator/.env.example create mode 100755 PROJECTS/beginner/canary-token-generator/scripts/init.sh create mode 100755 PROJECTS/beginner/canary-token-generator/scripts/randomize-ports.sh diff --git a/PROJECTS/beginner/canary-token-generator/.env.example b/PROJECTS/beginner/canary-token-generator/.env.example new file mode 100644 index 00000000..003d2f07 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/.env.example @@ -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 ` 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= diff --git a/PROJECTS/beginner/canary-token-generator/scripts/init.sh b/PROJECTS/beginner/canary-token-generator/scripts/init.sh new file mode 100755 index 00000000..2f240ae7 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/scripts/init.sh @@ -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'." diff --git a/PROJECTS/beginner/canary-token-generator/scripts/randomize-ports.sh b/PROJECTS/beginner/canary-token-generator/scripts/randomize-ports.sh new file mode 100755 index 00000000..95348d01 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/scripts/randomize-ports.sh @@ -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."