520 lines
21 KiB
Bash
Executable File
520 lines
21 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
|
||
# Copyright (C) 2026 Fluxer Contributors
|
||
#
|
||
# This file is part of Fluxer.
|
||
#
|
||
# Fluxer is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU Affero General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# Fluxer is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU Affero General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License
|
||
# along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
set -eu
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
|
||
RED='\033[0;31m'
|
||
YELLOW='\033[1;33m'
|
||
GREEN='\033[0;32m'
|
||
NC='\033[0m'
|
||
|
||
info() { printf "%b\n" "${GREEN}[INFO]${NC} $1"; }
|
||
warn() { printf "%b\n" "${YELLOW}[WARN]${NC} $1"; }
|
||
error() { printf "%b\n" "${RED}[ERROR]${NC} $1"; }
|
||
|
||
prepare_log_dir() {
|
||
info "Ensuring dev log directory exists..."
|
||
mkdir -p "$REPO_ROOT/dev/logs"
|
||
}
|
||
|
||
check_config() {
|
||
config_path="${FLUXER_CONFIG:-$REPO_ROOT/config/config.json}"
|
||
template_path="$REPO_ROOT/config/config.dev.template.json"
|
||
|
||
if [ ! -f "$config_path" ]; then
|
||
if [ -f "$template_path" ]; then
|
||
info "No config found, creating from development template..."
|
||
cp "$template_path" "$config_path"
|
||
else
|
||
error "Configuration file not found: $config_path"
|
||
error "Template file also missing: $template_path"
|
||
exit 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
random_hex() {
|
||
byte_count="$1"
|
||
|
||
node - "$byte_count" <<'NODE'
|
||
const {randomBytes} = require('node:crypto');
|
||
|
||
const byteCount = Number(process.argv[2]);
|
||
if (!Number.isInteger(byteCount) || byteCount <= 0) {
|
||
process.exit(1);
|
||
}
|
||
|
||
process.stdout.write(randomBytes(byteCount).toString('hex'));
|
||
NODE
|
||
}
|
||
|
||
is_empty_or_placeholder() {
|
||
value="$1"
|
||
shift
|
||
|
||
if [ -z "$value" ]; then
|
||
return 0
|
||
fi
|
||
|
||
for placeholder in "$@"; do
|
||
if [ "$value" = "$placeholder" ]; then
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
seed_hex_secret() {
|
||
current_value="$1"
|
||
byte_count="$2"
|
||
shift 2
|
||
|
||
if is_empty_or_placeholder "$current_value" "$@"; then
|
||
random_hex "$byte_count"
|
||
else
|
||
printf '%s' "$current_value"
|
||
fi
|
||
}
|
||
|
||
sync_meilisearch_key_file() {
|
||
has_search="$1"
|
||
api_key="$2"
|
||
|
||
if [ "$has_search" != "true" ]; then
|
||
return 0
|
||
fi
|
||
|
||
meilisearch_key_path="$REPO_ROOT/dev/meilisearch_master_key"
|
||
meilisearch_key_file_value="$(cat "$meilisearch_key_path" 2>/dev/null || true)"
|
||
|
||
if [ "$meilisearch_key_file_value" != "$api_key" ]; then
|
||
printf '%s' "$api_key" > "$meilisearch_key_path"
|
||
chmod 600 "$meilisearch_key_path" 2>/dev/null || true
|
||
fi
|
||
|
||
mkdir -p "$REPO_ROOT/dev/data/meilisearch"
|
||
}
|
||
|
||
ensure_core_secrets() {
|
||
config_path="${FLUXER_CONFIG:-$REPO_ROOT/config/config.json}"
|
||
|
||
info "Checking development secret configuration..."
|
||
|
||
if [ ! -f "$config_path" ]; then
|
||
warn "Config file not found, skipping secret generation"
|
||
return 0
|
||
fi
|
||
|
||
current_s3_access_key_id=$(jq -r '.s3.access_key_id // empty' "$config_path" 2>/dev/null || true)
|
||
current_s3_secret_access_key=$(jq -r '.s3.secret_access_key // empty' "$config_path" 2>/dev/null || true)
|
||
current_media_proxy_secret_key=$(jq -r '.services.media_proxy.secret_key // empty' "$config_path" 2>/dev/null || true)
|
||
current_admin_secret_key_base=$(jq -r '.services.admin.secret_key_base // empty' "$config_path" 2>/dev/null || true)
|
||
current_admin_oauth_client_secret=$(jq -r '.services.admin.oauth_client_secret // empty' "$config_path" 2>/dev/null || true)
|
||
current_marketing_secret_key_base=$(jq -r '.services.marketing.secret_key_base // empty' "$config_path" 2>/dev/null || true)
|
||
current_gateway_admin_reload_secret=$(jq -r '.services.gateway.admin_reload_secret // empty' "$config_path" 2>/dev/null || true)
|
||
current_queue_secret=$(jq -r '.services.queue.secret // empty' "$config_path" 2>/dev/null || true)
|
||
current_meilisearch_api_key=$(jq -r '.integrations.search.api_key // empty' "$config_path" 2>/dev/null || true)
|
||
has_deprecated_gateway_config=$(jq -r '.gateway != null' "$config_path" 2>/dev/null || echo "false")
|
||
current_sudo_mode_secret=$(jq -r '.auth.sudo_mode_secret // empty' "$config_path" 2>/dev/null || true)
|
||
current_connection_initiation_secret=$(jq -r '.auth.connection_initiation_secret // empty' "$config_path" 2>/dev/null || true)
|
||
current_smtp_password=$(jq -r '.integrations.email.smtp.password // empty' "$config_path" 2>/dev/null || true)
|
||
current_voice_api_key=$(jq -r '.integrations.voice.api_key // empty' "$config_path" 2>/dev/null || true)
|
||
current_voice_api_secret=$(jq -r '.integrations.voice.api_secret // empty' "$config_path" 2>/dev/null || true)
|
||
has_smtp=$(jq -r '.integrations.email.smtp != null' "$config_path" 2>/dev/null || echo "false")
|
||
has_marketing=$(jq -r '.services.marketing != null' "$config_path" 2>/dev/null || echo "false")
|
||
has_queue=$(jq -r '.services.queue != null' "$config_path" 2>/dev/null || echo "false")
|
||
has_search=$(jq -r '.integrations.search != null' "$config_path" 2>/dev/null || echo "false")
|
||
has_voice=$(jq -r '.integrations.voice != null' "$config_path" 2>/dev/null || echo "false")
|
||
|
||
seeded_s3_access_key_id=$(seed_hex_secret "$current_s3_access_key_id" 16 "dev-access-key" "fluxer-dev-access-key")
|
||
seeded_s3_secret_access_key=$(seed_hex_secret "$current_s3_secret_access_key" 32 "dev-secret-key" "fluxer-dev-secret-key")
|
||
seeded_media_proxy_secret_key=$(seed_hex_secret "$current_media_proxy_secret_key" 32 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||
seeded_admin_secret_key_base=$(seed_hex_secret "$current_admin_secret_key_base" 32 "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
|
||
seeded_admin_oauth_client_secret=$(seed_hex_secret "$current_admin_oauth_client_secret" 32 "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210")
|
||
seeded_marketing_secret_key_base="$current_marketing_secret_key_base"
|
||
if [ "$has_marketing" = "true" ]; then
|
||
seeded_marketing_secret_key_base=$(seed_hex_secret "$current_marketing_secret_key_base" 32 "marketing0123456789abcdef0123456789abcdef0123456789abcdef01234567")
|
||
fi
|
||
seeded_gateway_admin_reload_secret=$(seed_hex_secret "$current_gateway_admin_reload_secret" 32 "deadbeef0123456789abcdef0123456789abcdef0123456789abcdef01234567")
|
||
seeded_queue_secret="$current_queue_secret"
|
||
if [ "$has_queue" = "true" ]; then
|
||
seeded_queue_secret=$(seed_hex_secret "$current_queue_secret" 32 "queue00123456789abcdef0123456789abcdef0123456789abcdef0123456789")
|
||
fi
|
||
seeded_meilisearch_api_key="$current_meilisearch_api_key"
|
||
if [ "$has_search" = "true" ]; then
|
||
seeded_meilisearch_api_key=$(seed_hex_secret "$current_meilisearch_api_key" 32 "meilisearch0123456789abcdef0123456789abcdef0123456789abcdef012345")
|
||
fi
|
||
seeded_sudo_mode_secret=$(seed_hex_secret "$current_sudo_mode_secret" 32 "c0ffee000123456789abcdef0123456789abcdef0123456789abcdef01234567")
|
||
seeded_connection_initiation_secret=$(seed_hex_secret "$current_connection_initiation_secret" 32 "d0d0ca000123456789abcdef0123456789abcdef0123456789abcdef01234567")
|
||
seeded_smtp_password="$current_smtp_password"
|
||
if [ "$has_smtp" = "true" ]; then
|
||
seeded_smtp_password=$(seed_hex_secret "$current_smtp_password" 16 "dev")
|
||
fi
|
||
seeded_voice_api_key="$current_voice_api_key"
|
||
seeded_voice_api_secret="$current_voice_api_secret"
|
||
if [ "$has_voice" = "true" ]; then
|
||
seeded_voice_api_key=$(seed_hex_secret "$current_voice_api_key" 32 "5VCKLGhj3Yz0q2GIBnuumpOP1GlSTSw5mLPZDvZNIvQpiocQXDQIwTS5CRrnOhe7" "devkey")
|
||
seeded_voice_api_secret=$(seed_hex_secret "$current_voice_api_secret" 32 "devsecret")
|
||
fi
|
||
|
||
has_changes=false
|
||
if [ "$seeded_s3_access_key_id" != "$current_s3_access_key_id" ]; then has_changes=true; fi
|
||
if [ "$seeded_s3_secret_access_key" != "$current_s3_secret_access_key" ]; then has_changes=true; fi
|
||
if [ "$seeded_media_proxy_secret_key" != "$current_media_proxy_secret_key" ]; then has_changes=true; fi
|
||
if [ "$seeded_admin_secret_key_base" != "$current_admin_secret_key_base" ]; then has_changes=true; fi
|
||
if [ "$seeded_admin_oauth_client_secret" != "$current_admin_oauth_client_secret" ]; then has_changes=true; fi
|
||
if [ "$has_marketing" = "true" ] && [ "$seeded_marketing_secret_key_base" != "$current_marketing_secret_key_base" ]; then has_changes=true; fi
|
||
if [ "$seeded_gateway_admin_reload_secret" != "$current_gateway_admin_reload_secret" ]; then has_changes=true; fi
|
||
if [ "$has_queue" = "true" ] && [ "$seeded_queue_secret" != "$current_queue_secret" ]; then has_changes=true; fi
|
||
if [ "$has_search" = "true" ] && [ "$seeded_meilisearch_api_key" != "$current_meilisearch_api_key" ]; then has_changes=true; fi
|
||
if [ "$seeded_sudo_mode_secret" != "$current_sudo_mode_secret" ]; then has_changes=true; fi
|
||
if [ "$seeded_connection_initiation_secret" != "$current_connection_initiation_secret" ]; then has_changes=true; fi
|
||
if [ "$has_smtp" = "true" ] && [ "$seeded_smtp_password" != "$current_smtp_password" ]; then has_changes=true; fi
|
||
if [ "$has_voice" = "true" ] && [ "$seeded_voice_api_key" != "$current_voice_api_key" ]; then has_changes=true; fi
|
||
if [ "$has_voice" = "true" ] && [ "$seeded_voice_api_secret" != "$current_voice_api_secret" ]; then has_changes=true; fi
|
||
if [ "$has_deprecated_gateway_config" = "true" ]; then has_changes=true; fi
|
||
|
||
if [ "$has_changes" = false ]; then
|
||
info "Development secrets already configured"
|
||
sync_meilisearch_key_file "$has_search" "$seeded_meilisearch_api_key"
|
||
return 0
|
||
fi
|
||
|
||
# Development secrets are generated locally during bootstrap to avoid
|
||
# committing placeholder values that look like real credentials.
|
||
info "Generating local development secrets..."
|
||
|
||
temp_config="$config_path.tmp"
|
||
jq \
|
||
--arg s3_access_key_id "$seeded_s3_access_key_id" \
|
||
--arg s3_secret_access_key "$seeded_s3_secret_access_key" \
|
||
--arg media_proxy_secret_key "$seeded_media_proxy_secret_key" \
|
||
--arg admin_secret_key_base "$seeded_admin_secret_key_base" \
|
||
--arg admin_oauth_client_secret "$seeded_admin_oauth_client_secret" \
|
||
--arg marketing_secret_key_base "$seeded_marketing_secret_key_base" \
|
||
--arg gateway_admin_reload_secret "$seeded_gateway_admin_reload_secret" \
|
||
--arg queue_secret "$seeded_queue_secret" \
|
||
--arg meilisearch_api_key "$seeded_meilisearch_api_key" \
|
||
--arg sudo_mode_secret "$seeded_sudo_mode_secret" \
|
||
--arg connection_initiation_secret "$seeded_connection_initiation_secret" \
|
||
--arg smtp_password "$seeded_smtp_password" \
|
||
--arg voice_api_key "$seeded_voice_api_key" \
|
||
--arg voice_api_secret "$seeded_voice_api_secret" \
|
||
'.s3.access_key_id = $s3_access_key_id |
|
||
.s3.secret_access_key = $s3_secret_access_key |
|
||
.services.media_proxy.secret_key = $media_proxy_secret_key |
|
||
.services.admin.secret_key_base = $admin_secret_key_base |
|
||
.services.admin.oauth_client_secret = $admin_oauth_client_secret |
|
||
(if .services.marketing != null then .services.marketing.secret_key_base = $marketing_secret_key_base else . end) |
|
||
.services.gateway.admin_reload_secret = $gateway_admin_reload_secret |
|
||
(if .services.queue != null then .services.queue.secret = $queue_secret else . end) |
|
||
(if .integrations.search != null then .integrations.search.api_key = $meilisearch_api_key else . end) |
|
||
del(.gateway) |
|
||
.auth.sudo_mode_secret = $sudo_mode_secret |
|
||
.auth.connection_initiation_secret = $connection_initiation_secret |
|
||
(if .integrations.email.smtp != null then .integrations.email.smtp.password = $smtp_password else . end) |
|
||
(if .integrations.voice != null then .integrations.voice.api_key = $voice_api_key | .integrations.voice.api_secret = $voice_api_secret else . end)' \
|
||
"$config_path" > "$temp_config"
|
||
|
||
if [ $? -eq 0 ]; then
|
||
mv "$temp_config" "$config_path"
|
||
info "Development secrets configured"
|
||
|
||
sync_meilisearch_key_file "$has_search" "$seeded_meilisearch_api_key"
|
||
else
|
||
error "Failed to update config.json with development secrets"
|
||
rm -f "$temp_config"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
validate_vapid_keys() {
|
||
public_key="$1"
|
||
private_key="$2"
|
||
|
||
node - "$public_key" "$private_key" >/dev/null 2>&1 <<'NODE'
|
||
const [publicKey, privateKey] = process.argv.slice(2);
|
||
|
||
try {
|
||
if (!publicKey || !privateKey) {
|
||
process.exit(1);
|
||
}
|
||
|
||
const publicRaw = Buffer.from(publicKey, 'base64url');
|
||
const privateRaw = Buffer.from(privateKey, 'base64url');
|
||
if (publicRaw.length !== 65 || publicRaw[0] !== 0x04 || privateRaw.length !== 32) {
|
||
process.exit(1);
|
||
}
|
||
|
||
process.exit(0);
|
||
} catch (_error) {
|
||
process.exit(1);
|
||
}
|
||
NODE
|
||
}
|
||
|
||
generate_vapid_keypair() {
|
||
node - <<'NODE'
|
||
const {generateKeyPairSync} = require('node:crypto');
|
||
|
||
const {privateKey, publicKey} = generateKeyPairSync('ec', {namedCurve: 'prime256v1'});
|
||
const publicJwk = publicKey.export({format: 'jwk'});
|
||
const privateJwk = privateKey.export({format: 'jwk'});
|
||
const publicRaw = Buffer.concat([
|
||
Buffer.from([0x04]),
|
||
Buffer.from(publicJwk.x, 'base64url'),
|
||
Buffer.from(publicJwk.y, 'base64url'),
|
||
]);
|
||
|
||
process.stdout.write(
|
||
JSON.stringify({
|
||
public_key: publicRaw.toString('base64url'),
|
||
private_key: privateJwk.d,
|
||
})
|
||
);
|
||
NODE
|
||
}
|
||
|
||
ensure_vapid_keys() {
|
||
config_path="${FLUXER_CONFIG:-$REPO_ROOT/config/config.json}"
|
||
|
||
info "Checking VAPID configuration..."
|
||
|
||
if [ ! -f "$config_path" ]; then
|
||
warn "Config file not found, skipping VAPID key generation"
|
||
return 0
|
||
fi
|
||
|
||
vapid_public_key=$(jq -r '.auth.vapid.public_key // empty' "$config_path" 2>/dev/null || true)
|
||
vapid_private_key=$(jq -r '.auth.vapid.private_key // empty' "$config_path" 2>/dev/null || true)
|
||
|
||
if validate_vapid_keys "$vapid_public_key" "$vapid_private_key"; then
|
||
info "VAPID keys already configured"
|
||
return 0
|
||
fi
|
||
|
||
# Development VAPID keys are generated locally by bootstrap, not issued by
|
||
# an external provider. There is no external renewal process – if keys are
|
||
# missing or invalid we generate a fresh pair here.
|
||
info "Generating development-only VAPID keypair..."
|
||
vapid_keys_json=$(generate_vapid_keypair)
|
||
generated_public_key=$(printf '%s' "$vapid_keys_json" | jq -r '.public_key // empty')
|
||
generated_private_key=$(printf '%s' "$vapid_keys_json" | jq -r '.private_key // empty')
|
||
|
||
if ! validate_vapid_keys "$generated_public_key" "$generated_private_key"; then
|
||
error "Failed to generate valid VAPID keys"
|
||
return 1
|
||
fi
|
||
|
||
temp_config="$config_path.tmp"
|
||
jq --arg vapid_public_key "$generated_public_key" \
|
||
--arg vapid_private_key "$generated_private_key" \
|
||
'.auth.vapid.public_key = $vapid_public_key |
|
||
.auth.vapid.private_key = $vapid_private_key' "$config_path" > "$temp_config"
|
||
|
||
if [ $? -eq 0 ]; then
|
||
mv "$temp_config" "$config_path"
|
||
info "VAPID keys configured for development"
|
||
else
|
||
error "Failed to update config.json with VAPID keys"
|
||
rm -f "$temp_config"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
generate_bluesky_oauth_keys() {
|
||
config_path="${FLUXER_CONFIG:-$REPO_ROOT/config/config.json}"
|
||
key_path="$REPO_ROOT/dev/bluesky_oauth_key.pem"
|
||
|
||
info "Checking Bluesky OAuth configuration..."
|
||
|
||
if [ ! -f "$config_path" ]; then
|
||
warn "Config file not found, skipping Bluesky OAuth key generation"
|
||
return 0
|
||
fi
|
||
|
||
keys_length=$(jq -r '.auth.bluesky.keys | length' "$config_path" 2>/dev/null || echo "0")
|
||
|
||
if [ "$keys_length" != "0" ]; then
|
||
all_keys_exist=true
|
||
for key_file in $(jq -r '.auth.bluesky.keys[].private_key_path // empty' "$config_path" 2>/dev/null); do
|
||
if [ ! -f "$key_file" ]; then
|
||
warn "Configured key file missing: $key_file"
|
||
all_keys_exist=false
|
||
continue
|
||
fi
|
||
if ! openssl pkey -in "$key_file" -text -noout 2>/dev/null | grep -Eq "prime256v1|secp256r1"; then
|
||
warn "Configured key file is not an ES256 (P-256) key: $key_file"
|
||
all_keys_exist=false
|
||
elif ! openssl pkcs8 -topk8 -nocrypt -in "$key_file" -out /dev/null >/dev/null 2>&1; then
|
||
warn "Configured key file is not PKCS#8 encoded: $key_file"
|
||
all_keys_exist=false
|
||
fi
|
||
done
|
||
if [ "$all_keys_exist" = true ]; then
|
||
info "Bluesky OAuth keys already configured"
|
||
return 0
|
||
fi
|
||
info "Regenerating Bluesky OAuth key files..."
|
||
fi
|
||
|
||
info "Generating Bluesky OAuth ES256 (P-256) keypair..."
|
||
|
||
mkdir -p "$REPO_ROOT/dev"
|
||
|
||
if ! openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out "$key_path" >/dev/null 2>&1; then
|
||
error "Failed to generate ES256 key for Bluesky OAuth"
|
||
return 1
|
||
fi
|
||
|
||
info "Generated ES256 key at: $key_path"
|
||
|
||
info "Updating config.json with Bluesky OAuth key..."
|
||
|
||
temp_config="$config_path.tmp"
|
||
|
||
jq --arg kid "dev-key-1" \
|
||
--arg key_path "$key_path" \
|
||
'.auth.bluesky.enabled = true |
|
||
.auth.bluesky.logo_uri = "https://fluxerstatic.com/web/apple-touch-icon.png" |
|
||
.auth.bluesky.tos_uri = "https://fluxer.app/terms" |
|
||
.auth.bluesky.policy_uri = "https://fluxer.app/privacy" |
|
||
.auth.bluesky.token_endpoint_auth_signing_alg = "ES256" |
|
||
.auth.bluesky.keys = [{
|
||
"kid": $kid,
|
||
"private_key_path": $key_path
|
||
}]' "$config_path" > "$temp_config"
|
||
|
||
if [ $? -eq 0 ]; then
|
||
mv "$temp_config" "$config_path"
|
||
info "Bluesky OAuth configured with dev key (enabled: true)"
|
||
else
|
||
error "Failed to update config.json with Bluesky OAuth key"
|
||
rm -f "$temp_config"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
generate_livekit_config() {
|
||
config_path="${FLUXER_CONFIG:-$REPO_ROOT/config/config.json}"
|
||
livekit_config="$REPO_ROOT/dev/livekit.yaml"
|
||
template="$REPO_ROOT/dev/livekit.template.yaml"
|
||
|
||
info "Generating LiveKit configuration..."
|
||
|
||
api_key=
|
||
api_secret=
|
||
webhook_url=
|
||
base_domain=
|
||
|
||
api_key=$(jq -r '.integrations.voice.api_key // empty' "$config_path" 2>/dev/null || true)
|
||
api_secret=$(jq -r '.integrations.voice.api_secret // empty' "$config_path" 2>/dev/null || true)
|
||
webhook_url=$(jq -r '.integrations.voice.webhook_url // empty' "$config_path" 2>/dev/null || true)
|
||
base_domain=$(jq -r '.domain.base_domain // empty' "$config_path" 2>/dev/null || true)
|
||
|
||
api_key="${api_key:-devkey}"
|
||
api_secret="${api_secret:-devsecret}"
|
||
webhook_url="${webhook_url:-http://localhost:49319/api/webhooks/livekit}"
|
||
base_domain="${base_domain:-localhost}"
|
||
|
||
if [ "$base_domain" = "localhost" ] || [ "$base_domain" = "127.0.0.1" ]; then
|
||
node_ip="127.0.0.1"
|
||
turn_domain="localhost"
|
||
else
|
||
turn_domain="$base_domain"
|
||
node_ip=$(curl -4 -sf --max-time 5 https://ifconfig.me 2>/dev/null || true)
|
||
if [ -z "$node_ip" ]; then
|
||
node_ip=$(curl -4 -sf --max-time 5 https://api.ipify.org 2>/dev/null || true)
|
||
fi
|
||
if [ -z "$node_ip" ]; then
|
||
warn "Could not resolve public IP for LiveKit. Voice may not work for remote clients."
|
||
warn "Set rtc.node_ip manually in dev/livekit.yaml to your server's public IP."
|
||
node_ip="127.0.0.1"
|
||
else
|
||
info "Resolved public IP for LiveKit: $node_ip"
|
||
fi
|
||
fi
|
||
|
||
sed -e "s|{{API_KEY}}|$api_key|g" \
|
||
-e "s|{{API_SECRET}}|$api_secret|g" \
|
||
-e "s|{{WEBHOOK_URL}}|$webhook_url|g" \
|
||
-e "s|{{NODE_IP}}|$node_ip|g" \
|
||
-e "s|{{TURN_DOMAIN}}|$turn_domain|g" \
|
||
"$template" > "$livekit_config"
|
||
|
||
info "LiveKit config generated at: $livekit_config (domain: $base_domain, node_ip: $node_ip)"
|
||
}
|
||
|
||
setup_model_symlink() {
|
||
source="$REPO_ROOT/fluxer_media_proxy/data/model.onnx"
|
||
target_dir="$REPO_ROOT/fluxer_server/data"
|
||
target="$target_dir/model.onnx"
|
||
|
||
info "Setting up ONNX model symlink..."
|
||
|
||
if [ ! -f "$source" ]; then
|
||
warn "Source model not found: $source"
|
||
warn "NSFW detection will not work until model.onnx is provided"
|
||
return 0
|
||
fi
|
||
|
||
mkdir -p "$target_dir"
|
||
|
||
if ls -ld "$target" 2>/dev/null | grep -q '^l'; then
|
||
info "Model symlink already exists"
|
||
elif [ -f "$target" ]; then
|
||
source_size=$(stat -f%z "$source" 2>/dev/null || stat -c%s "$source" 2>/dev/null)
|
||
target_size=$(stat -f%z "$target" 2>/dev/null || stat -c%s "$target" 2>/dev/null)
|
||
if [ "$target_size" -lt 1000 ]; then
|
||
info "Replacing empty/corrupt model file with symlink"
|
||
rm -f "$target"
|
||
ln -s "$source" "$target"
|
||
else
|
||
info "Model file already exists (not a symlink)"
|
||
fi
|
||
else
|
||
ln -s "$source" "$target"
|
||
info "Created model symlink: $target -> $source"
|
||
fi
|
||
}
|
||
|
||
main() {
|
||
echo ""
|
||
info "Fluxer Development Bootstrap"
|
||
echo ""
|
||
|
||
prepare_log_dir
|
||
check_config
|
||
ensure_core_secrets
|
||
ensure_vapid_keys
|
||
generate_bluesky_oauth_keys
|
||
generate_livekit_config
|
||
setup_model_symlink
|
||
|
||
echo ""
|
||
info "Bootstrap complete"
|
||
echo ""
|
||
}
|
||
|
||
main "$@"
|