All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Add NotifyProvisioner (port + adapter) using real notify admin API - Create notify account + send key + host grant per project - Inject NOTIFY_API_KEY/HOST/FROM into component deployments - Store NOTIFY_URL, NOTIFY_ADMIN_KEY, RESEND_API_KEY in credential store - Add setup-notify.sh for one-time host/provider/domain setup - Add NOTIFY_ADMIN_KEY constant to domain/credential.go - Wire provisioner in main.go with connection test guard - Add .claude/guides/services/notify.md and CLAUDE.md entry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
170 lines
7.8 KiB
Bash
Executable File
170 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# setup-notify.sh - One-time host and provider setup for the notify service.
|
|
#
|
|
# Creates the threesix.ai host, adds Resend as provider, registers noreply@threesix.ai,
|
|
# and adds Resend DNS records to Cloudflare for domain verification.
|
|
#
|
|
# Idempotent: safe to run multiple times.
|
|
#
|
|
# Usage:
|
|
# ./scripts/setup-notify.sh
|
|
# NOTIFY_URL=... NOTIFY_ADMIN_KEY=... RESEND_API_KEY=... ./scripts/setup-notify.sh
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
|
|
|
|
# ─── Load secrets ────────────────────────────────────────────────────────────
|
|
|
|
SECRETS_FILE="${SECRETS_FILE:-.secrets}"
|
|
if [[ -f "$SECRETS_FILE" ]]; then
|
|
while IFS='=' read -r key val || [[ -n "$key" ]]; do
|
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
export "$key"="${val}"
|
|
done < "$SECRETS_FILE"
|
|
fi
|
|
|
|
NOTIFY_URL="${NOTIFY_URL:-}"
|
|
NOTIFY_ADMIN_KEY="${NOTIFY_ADMIN_KEY:-}"
|
|
RESEND_API_KEY="${RESEND_API_KEY:-}"
|
|
CF_TOKEN="${CLOUDFLARE_API_TOKEN:-}"
|
|
CF_ZONE="${CLOUDFLARE_ZONE_ID:-}"
|
|
|
|
if [[ -z "$NOTIFY_URL" ]]; then log_error "NOTIFY_URL required"; exit 1; fi
|
|
if [[ -z "$NOTIFY_ADMIN_KEY" ]]; then log_error "NOTIFY_ADMIN_KEY required"; exit 1; fi
|
|
if [[ -z "$RESEND_API_KEY" ]]; then log_error "RESEND_API_KEY required"; exit 1; fi
|
|
|
|
HOST=threesix.ai
|
|
FROM=noreply@threesix.ai
|
|
|
|
log_info "Notify URL: $NOTIFY_URL"
|
|
log_info "Host: $HOST"
|
|
log_info "From: $FROM"
|
|
|
|
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
notify() {
|
|
local method="$1" path="$2" body="${3:-}"
|
|
local args=(-s -X "$method" "$NOTIFY_URL$path"
|
|
-H "Authorization: Bearer $NOTIFY_ADMIN_KEY"
|
|
-H "Content-Type: application/json")
|
|
[[ -n "$body" ]] && args+=(-d "$body")
|
|
curl "${args[@]}"
|
|
}
|
|
|
|
resend_api() {
|
|
local method="$1" path="$2" body="${3:-}"
|
|
local args=(-s -X "$method" "https://api.resend.com$path"
|
|
-H "Authorization: Bearer $RESEND_API_KEY"
|
|
-H "Content-Type: application/json")
|
|
[[ -n "$body" ]] && args+=(-d "$body")
|
|
curl "${args[@]}"
|
|
}
|
|
|
|
cf_dns() {
|
|
local method="$1" path="$2" body="${3:-}"
|
|
local args=(-s -X "$method" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE$path"
|
|
-H "Authorization: Bearer $CF_TOKEN"
|
|
-H "Content-Type: application/json")
|
|
[[ -n "$body" ]] && args+=(-d "$body")
|
|
curl "${args[@]}"
|
|
}
|
|
|
|
# ─── Step 1: Create host ──────────────────────────────────────────────────────
|
|
|
|
log_step "1. Setting up notify host: $HOST"
|
|
existing=$(notify GET "/admin/hosts" | python3 -c "import sys,json; items=json.load(sys.stdin).get('items',[]); print(next((x['host'] for x in items if x['host']=='$HOST'),''))" 2>/dev/null || true)
|
|
if [[ "$existing" == "$HOST" ]]; then
|
|
log_info " Host already exists — skipping"
|
|
else
|
|
notify POST "/admin/hosts" "{\"host\":\"$HOST\",\"strategy\":\"failover\"}" | python3 -m json.tool
|
|
log_info " Host created"
|
|
fi
|
|
|
|
# ─── Step 2: Add Resend provider ─────────────────────────────────────────────
|
|
|
|
log_step "2. Adding Resend provider"
|
|
providers=$(notify GET "/admin/hosts/$HOST/providers" | python3 -c "import sys,json; items=json.load(sys.stdin); print(next((str(x['id']) for x in items if x['provider']=='resend'),''))" 2>/dev/null || true)
|
|
if [[ -n "$providers" ]]; then
|
|
log_info " Resend provider already configured (id: $providers) — skipping"
|
|
else
|
|
notify POST "/admin/hosts/$HOST/providers" \
|
|
"{\"provider\":\"resend\",\"config\":{\"api_key\":\"$RESEND_API_KEY\"},\"priority\":1,\"retry_attempts\":3,\"retry_backoff_ms\":1000}" | python3 -m json.tool
|
|
log_info " Resend provider added"
|
|
fi
|
|
|
|
# ─── Step 3: Register from-address ───────────────────────────────────────────
|
|
|
|
log_step "3. Registering from-address: $FROM"
|
|
addrs=$(notify GET "/admin/hosts/$HOST/from-addresses" | python3 -c "import sys,json; items=json.load(sys.stdin).get('items',[]); print(next((x['email'] for x in items if x['email']=='$FROM'),''))" 2>/dev/null || true)
|
|
if [[ "$addrs" == "$FROM" ]]; then
|
|
log_info " From-address already registered — skipping"
|
|
else
|
|
notify POST "/admin/hosts/$HOST/from-addresses" \
|
|
"{\"email\":\"$FROM\",\"display_name\":\"threesix.ai\"}" | python3 -m json.tool
|
|
log_info " From-address registered"
|
|
fi
|
|
|
|
# ─── Step 4: Resend domain + Cloudflare DNS ───────────────────────────────────
|
|
|
|
log_step "4. Setting up Resend domain for $HOST"
|
|
existing_domain=$(resend_api GET "/domains" | python3 -c "import sys,json; data=json.load(sys.stdin); print(next((x['id'] for x in data.get('data',[]) if x['name']=='$HOST'),''))" 2>/dev/null || true)
|
|
|
|
if [[ -n "$existing_domain" ]]; then
|
|
log_info " Resend domain already exists (id: $existing_domain)"
|
|
DOMAIN_ID="$existing_domain"
|
|
DOMAIN_RECORDS=$(resend_api GET "/domains/$DOMAIN_ID" | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('records',[])))")
|
|
else
|
|
log_info " Creating Resend domain..."
|
|
domain_resp=$(resend_api POST "/domains" "{\"name\":\"$HOST\",\"region\":\"us-east-1\"}")
|
|
DOMAIN_ID=$(echo "$domain_resp" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
DOMAIN_RECORDS=$(echo "$domain_resp" | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('records',[])))")
|
|
log_info " Domain created (id: $DOMAIN_ID)"
|
|
fi
|
|
|
|
# Add DNS records if Cloudflare is configured
|
|
if [[ -n "$CF_TOKEN" && -n "$CF_ZONE" ]]; then
|
|
log_step "5. Adding Resend DNS records to Cloudflare"
|
|
echo "$DOMAIN_RECORDS" | python3 -c "
|
|
import sys, json
|
|
records = json.load(sys.stdin)
|
|
for r in records:
|
|
print(r['type'], r['name'], r.get('value',''), r.get('priority',''))
|
|
" | while read -r rtype rname rvalue rpriority; do
|
|
# Check if record already exists
|
|
existing_rec=$(cf_dns GET "/dns_records?type=$rtype&name=$rname.$HOST" | python3 -c "import sys,json; result=json.load(sys.stdin).get('result',[]); print(result[0]['id'] if result else '')" 2>/dev/null || true)
|
|
if [[ -n "$existing_rec" ]]; then
|
|
log_info " $rtype $rname already exists — skipping"
|
|
else
|
|
if [[ "$rtype" == "MX" ]]; then
|
|
cf_dns POST "/dns_records" "{\"type\":\"MX\",\"name\":\"$rname\",\"content\":\"$rvalue\",\"priority\":$rpriority,\"ttl\":1}" > /dev/null
|
|
else
|
|
cf_dns POST "/dns_records" "{\"type\":\"$rtype\",\"name\":\"$rname\",\"content\":\"$rvalue\",\"ttl\":1,\"proxied\":false}" > /dev/null
|
|
fi
|
|
log_info " Added $rtype $rname"
|
|
fi
|
|
done
|
|
|
|
# Trigger verification
|
|
log_step "6. Triggering Resend domain verification"
|
|
resend_api POST "/domains/$DOMAIN_ID/verify" > /dev/null
|
|
log_info " Verification triggered (DNS propagation takes ~60s)"
|
|
else
|
|
log_warn " CLOUDFLARE_API_TOKEN or CLOUDFLARE_ZONE_ID not set — add DNS records manually:"
|
|
echo "$DOMAIN_RECORDS" | python3 -m json.tool
|
|
fi
|
|
|
|
echo ""
|
|
log_info "Setup complete."
|
|
log_info "Check Resend domain status: curl -s https://api.resend.com/domains/$DOMAIN_ID -H 'Authorization: Bearer \$RESEND_API_KEY' | python3 -m json.tool"
|