#!/bin/sh set -eu DOG_RESET="" DOG_BOLD="" DOG_CYAN="" DOG_MAGENTA="" DOG_AMBER="" DOG_GREEN="" DOG_RED="" DOG_GRAY="" if [ -t 1 ]; then DOG_RESET="$(printf '\033[0m')" DOG_BOLD="$(printf '\033[1m')" if [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then DOG_CYAN="$(printf '\033[38;2;0;255;255m')" DOG_MAGENTA="$(printf '\033[38;2;255;0;255m')" DOG_AMBER="$(printf '\033[38;2;255;176;0m')" DOG_GREEN="$(printf '\033[38;2;51;255;51m')" DOG_RED="$(printf '\033[38;2;255;51;51m')" DOG_GRAY="$(printf '\033[38;2;106;106;122m')" else DOG_CYAN="$(printf '\033[0;96m')" DOG_MAGENTA="$(printf '\033[0;95m')" DOG_AMBER="$(printf '\033[0;33m')" DOG_GREEN="$(printf '\033[0;32m')" DOG_RED="$(printf '\033[0;31m')" DOG_GRAY="$(printf '\033[0;90m')" fi fi header() { printf "%s%sDOG_ EDGE INSTALLER%s\n" "$DOG_CYAN" "$DOG_BOLD" "$DOG_RESET" printf "%sNative Installer%s\n" "$DOG_BOLD" "$DOG_RESET" printf "%sLocal vision. Cloud-linked.%s\n" "$DOG_MAGENTA" "$DOG_RESET" } step() { printf "%s==>%s %s\n" "$DOG_AMBER" "$DOG_RESET" "$*" } note() { printf "%s•%s %s\n" "$DOG_CYAN" "$DOG_RESET" "$*" } subtle() { printf "%s%s%s\n" "$DOG_GRAY" "$*" "$DOG_RESET" } lore_quote() { case $(( $(date +%s) % 6 )) in 0) printf "The feed continues without you." ;; 1) printf "Every frame is evidence." ;; 2) printf "The eye does not blink." ;; 3) printf "The watch continues." ;; 4) printf "The calm is only the space between events." ;; 5) printf "Archive first. Forget later." ;; esac } flair() { subtle "\"$(lore_quote)\"" } ok() { printf "%s✓%s %s\n" "$DOG_GREEN" "$DOG_RESET" "$*" } die() { printf "%sERROR:%s %s\n" "$DOG_RED" "$DOG_RESET" "$*" >&2 exit 1 } sha256_file() { target="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "$target" | awk '{print $1}' return fi if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$target" | awk '{print $1}' return fi die "Missing sha256 tool (sha256sum or shasum)." } release_public_key_pem() { cat <<'DOG_RELEASE_PUBLIC_KEY_EOF' -----BEGIN PUBLIC KEY----- MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApkngeFoPfdFGZH5Iw2XR WtyGi9Dk+f99facfp/iPvUHPj2bICHpDYYXc06M2QikUuiyRA2wkRkLnY5Lvseub BqceEnYpSAvSfAL1tXRC+Ne022B6rNYUOjwpy1sHDh3nYy7QWtv7XCyNxtOy6Qhi dzb9zHaZkLIaV1l5VUThvKk3N+NNKjaiJM9ca6u1rYChg1MJDMG/vWacP4OuAzmE 9fQFANkNkleTENrB+urBzcuOMLbOFahmDvzm6b94OJ4N3KZojwmbYyBO7YZDXFk0 Lze2stHVGnIGAavB8b/iJNBL5f53gwBVKFX+Pp00Htlal1dzkOHYl8m6wrVamUJE UrLPlH/bKkeW6AGirR8dIZZyOivUbhcbCrJdT9x/f/K4tGbr8Vl/xXuN8QX1n8CP 86DIo9GCpQ4ZXzEHc7P8DzKaJjw3kvGn0MjLQZUnH6nltFcycIrstKHlAGlR7+BO 612NN0+yjrgL2jvRxQXMshFe7c9Wn4zs5H5GSEAnLJ4pAgMBAAE= -----END PUBLIC KEY----- DOG_RELEASE_PUBLIC_KEY_EOF } require_openssl() { command -v openssl >/dev/null 2>&1 || die "Missing openssl. DOG_ Edge release manifests require OpenSSL signature verification." } verify_release_manifest() { manifest="$1" signature="$2" require_openssl pubkey="$(mktemp)" release_public_key_pem > "$pubkey" if ! openssl dgst -sha256 -verify "$pubkey" -signature "$signature" "$manifest" >/dev/null 2>&1; then rm -f "$pubkey" die "Release manifest signature verification failed." fi rm -f "$pubkey" } manifest_get() { manifest="$1" key="$2" awk -F= -v wanted="$key" ' $1 == wanted { print substr($0, length($1) + 2) found = 1 } END { exit found ? 0 : 1 } ' "$manifest" } validate_release_manifest() { manifest="$1" expected_channel="$2" awk -F= -v expected_channel="$expected_channel" ' function fail(message) { print message > "/dev/stderr" failed = 1 exit 1 } function require_key(key) { if (!(key in seen)) { fail("Release manifest missing required key: " key) } } function require_sha(key) { if (length(value[key]) != 64 || value[key] !~ /^[0-9A-Fa-f]+$/) { fail("Release manifest has invalid SHA256 for " key) } } function require_asset(key, expected) { if (value[key] != expected) { fail("Release manifest asset mismatch for " key ": expected " expected ", got " value[key]) } } BEGIN { required["format"] = 1 required["channel"] = 1 required["version"] = 1 required["asset_base_url"] = 1 required["linux_amd64_asset"] = 1 required["linux_amd64_sha256"] = 1 required["linux_arm64_asset"] = 1 required["linux_arm64_sha256"] = 1 required["darwin_arm64_asset"] = 1 required["darwin_arm64_sha256"] = 1 } { if ($0 == "") { fail("Release manifest contains a blank line") } if ($0 !~ /^[a-z0-9_]+=[A-Za-z0-9._:\/?=&%+-]+$/) { fail("Release manifest contains invalid syntax: " $0) } key = $1 val = substr($0, length(key) + 2) if (!(key in required) && !(key in optional)) { fail("Release manifest contains unknown key: " key) } if (key in seen) { fail("Release manifest contains duplicate key: " key) } seen[key] = 1 value[key] = val } END { if (failed) { exit 1 } require_key("format") require_key("channel") require_key("version") require_key("asset_base_url") require_key("linux_amd64_asset") require_key("linux_amd64_sha256") require_key("linux_arm64_asset") require_key("linux_arm64_sha256") require_key("darwin_arm64_asset") require_key("darwin_arm64_sha256") if (value["format"] != "dog-edge-release-manifest-v1") { fail("Release manifest format is unsupported: " value["format"]) } if (value["channel"] != expected_channel) { fail("Release manifest channel mismatch: expected " expected_channel ", got " value["channel"]) } if (value["version"] !~ /^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$/) { fail("Release manifest version is invalid: " value["version"]) } version = value["version"] if (value["asset_base_url"] != "https://install.dognet.cam/releases/" version "/download") { fail("Release manifest asset_base_url must use the version-specific release authority") } require_asset("linux_amd64_asset", "dog-edge-linux-amd64-" version ".tar.gz") require_asset("linux_arm64_asset", "dog-edge-linux-arm64-" version ".tar.gz") require_asset("darwin_arm64_asset", "dog-edge-darwin-arm64-" version ".tar.gz") require_sha("linux_amd64_sha256") require_sha("linux_arm64_sha256") require_sha("darwin_arm64_sha256") } ' "$manifest" || die "Release manifest validation failed." } resolve_release_asset() { manifest="$1" VERSION="$(manifest_get "$manifest" version)" ASSET_BASE_URL="$(manifest_get "$manifest" asset_base_url)" case "$PLATFORM_KEY" in LINUX_AMD64) ASSET="$(manifest_get "$manifest" linux_amd64_asset)" EXPECTED_SHA="$(manifest_get "$manifest" linux_amd64_sha256)" ;; LINUX_ARM64) ASSET="$(manifest_get "$manifest" linux_arm64_asset)" EXPECTED_SHA="$(manifest_get "$manifest" linux_arm64_sha256)" ;; DARWIN_ARM64) ASSET="$(manifest_get "$manifest" darwin_arm64_asset)" EXPECTED_SHA="$(manifest_get "$manifest" darwin_arm64_sha256)" ;; *) die "Unsupported platform: ${PLATFORM_KEY}" ;; esac } archive_paths_are_safe() { awk ' { name = $0 sub(/\/$/, "", name) count = split(name, component, "/") if (count < 1 || component[1] !~ /^dog-edge-[A-Za-z0-9._-]+$/) { exit 1 } if (wrapper == "") { wrapper = component[1] } else if (component[1] != wrapper) { exit 1 } for (part = 2; part <= count; part++) { if (component[part] == "" || component[part] == "." || component[part] == ".." || component[part] !~ /^[A-Za-z0-9._-]+$/) { exit 1 } } } END { if (wrapper == "") exit 1 } ' } archive_has_safe_entries() { archive="$1" tar -tzf "$archive" >/dev/null 2>&1 || return 1 tar -tzf "$archive" | archive_paths_are_safe || return 1 tar -tvzf "$archive" | awk ' { type = substr($1, 1, 1) group_write = substr($1, 6, 1) other_write = substr($1, 9, 1) if ((type != "d" && type != "-") || group_write == "w" || other_write == "w") { exit 1 } } ' } bundle_mode() { mode_path="$1" case "$(uname -s)" in Linux) stat -c '%a' "$mode_path" ;; *) stat -f '%Lp' "$mode_path" ;; esac } require_bundle_mode() { mode_path="$1" expected_mode="$2" actual_mode="$(bundle_mode "$mode_path")" || return 1 [ "$actual_mode" = "$expected_mode" ] || { printf 'Release bundle mode mismatch: %s is 0%s, expected 0%s.\n' \ "$mode_path" "$actual_mode" "$expected_mode" >&2 return 1 } } normalize_bundle_permissions() { bundle_root="$1" [ -d "$bundle_root" ] || return 1 for required_dir in bin lib scripts templates; do [ -d "$bundle_root/$required_dir" ] || return 1 done [ -f "$bundle_root/VERSION" ] || return 1 unsafe_entry="$(find "$bundle_root" -type l -print -quit)" [ -z "$unsafe_entry" ] || return 1 unsafe_entry="$(find "$bundle_root" ! -type d ! -type f -print -quit)" [ -z "$unsafe_entry" ] || return 1 if [ "$(uname -s)" = "Darwin" ]; then command -v xattr >/dev/null 2>&1 || return 1 chmod -RN "$bundle_root" || return 1 xattr -cr "$bundle_root" || return 1 fi find "$bundle_root" -type d | while IFS= read -r entry; do relative="${entry#"$bundle_root"}" case "$relative" in ""|/bin|/lib|/scripts|/templates) ;; *) exit 1 ;; esac chmod 0755 "$entry" require_bundle_mode "$entry" 755 || exit 1 done || return 1 find "$bundle_root" -type f | while IFS= read -r entry; do relative="${entry#"$bundle_root"/}" case "$relative" in VERSION|lib/*|templates/*) leaf="${relative#*/}" case "$leaf" in */*) exit 1 ;; esac expected_mode=644 ;; bin/*) leaf="${relative#bin/}" case "$leaf" in */*) exit 1 ;; esac expected_mode=755 ;; scripts/*.sh) leaf="${relative#scripts/}" case "$leaf" in */*) exit 1 ;; esac expected_mode=755 ;; *) exit 1 ;; esac chmod "0${expected_mode}" "$entry" require_bundle_mode "$entry" "$expected_mode" || exit 1 done } find_bundle_root() { destination="$1" matches="$(find "$destination" -mindepth 1 -maxdepth 1 -type d -name 'dog-edge-*')" count="$(printf "%s\n" "$matches" | awk 'NF { count++ } END { print count + 0 }')" [ "$count" = "1" ] || return 1 printf "%s\n" "$matches" } extract_verified_bundle() { archive="$1" destination="$2" archive_has_safe_entries "$archive" || die "Release archive contains unsafe entries or permissions." COPYFILE_DISABLE=1 tar \ --no-same-owner --no-same-permissions \ -xzf "$archive" -C "$destination" extracted_root="$(find_bundle_root "$destination")" || die "Expected exactly one extracted DOG_ Edge bundle." normalize_bundle_permissions "$extracted_root" || \ die "Extracted DOG_ Edge bundle violates the release layout or permission contract." } locate_bundle_root() { destination="$1" matches="$(find_bundle_root "$destination")" || die "Expected exactly one extracted DOG_ Edge bundle." [ -f "$matches/VERSION" ] \ && [ -x "$matches/bin/dog-edge" ] \ && [ -x "$matches/bin/dog-watchdog" ] \ && [ -x "$matches/bin/dognet" ] \ && [ -x "$matches/scripts/install-local.sh" ] \ || die "Extracted DOG_ Edge bundle has an invalid layout." printf "%s\n" "$matches" } spinner_frame() { case $(( $1 % 10 )) in 0) printf "⠋" ;; 1) printf "⠙" ;; 2) printf "⠹" ;; 3) printf "⠸" ;; 4) printf "⠼" ;; 5) printf "⠴" ;; 6) printf "⠦" ;; 7) printf "⠧" ;; 8) printf "⠇" ;; 9) printf "⠏" ;; esac } format_bytes() { b="${1:-0}" if [ "$b" -lt 1024 ]; then printf "%d B" "$b" elif [ "$b" -lt 1048576 ]; then printf "%d KiB" "$((b / 1024))" elif [ "$b" -lt 1073741824 ]; then printf "%d.%d MiB" "$((b / 1048576))" "$(((b % 1048576) * 10 / 1048576))" else printf "%d.%d GiB" "$((b / 1073741824))" "$(((b % 1073741824) * 10 / 1073741824))" fi } # Downloads $1 to $2 with a Braille spinner + byte counter. Falls back to a # silent curl when stdout is not a TTY (logs, CI, piped installs). download_with_feedback() { dl_url="$1" dl_dest="$2" if [ ! -t 1 ]; then curl -fsSL "$dl_url" -o "$dl_dest" return fi dl_total_raw="$(curl -fsSLI "$dl_url" 2>/dev/null \ | awk 'BEGIN{IGNORECASE=1} /^content-length:/ {gsub("\r",""); v=$2} END{print v+0}')" case "$dl_total_raw" in ''|*[!0-9]*) dl_total=0 ;; *) dl_total="$dl_total_raw" ;; esac curl -fsSL "$dl_url" -o "$dl_dest" & dl_pid=$! dl_spin=0 dl_start="$(date +%s)" while kill -0 "$dl_pid" 2>/dev/null; do dl_bytes=0 if [ -f "$dl_dest" ]; then dl_bytes_raw="$(wc -c < "$dl_dest" 2>/dev/null | tr -d ' ')" case "$dl_bytes_raw" in ''|*[!0-9]*) dl_bytes=0 ;; *) dl_bytes="$dl_bytes_raw" ;; esac fi dl_elapsed=$(( $(date +%s) - dl_start )) dl_detail="$(format_bytes "$dl_bytes")" if [ "$dl_total" -gt 0 ]; then dl_pct=$((dl_bytes * 100 / dl_total)) dl_detail="${dl_detail} / $(format_bytes "$dl_total") (${dl_pct}%)" fi printf "\r\033[K%s%s%s %s %s· %ss elapsed%s" \ "$DOG_CYAN" "$(spinner_frame "$dl_spin")" "$DOG_RESET" \ "$dl_detail" "$DOG_GRAY" "$dl_elapsed" "$DOG_RESET" dl_spin=$((dl_spin + 1)) sleep 1 done set +e wait "$dl_pid" dl_rc=$? set -e printf "\r\033[K" return "$dl_rc" } detect_platform() { os="$(uname -s | tr '[:upper:]' '[:lower:]')" arch="$(uname -m)" case "$os/$arch" in linux/x86_64) PLATFORM_KEY="LINUX_AMD64" ;; linux/aarch64|linux/arm64) PLATFORM_KEY="LINUX_ARM64" ;; darwin/arm64) PLATFORM_KEY="DARWIN_ARM64" ;; *) die "Unsupported platform: ${os}/${arch}" ;; esac } MODE="install" CHANNEL="${DOG_EDGE_CHANNEL:-stable}" RELEASE_BASE_URL="${DOG_EDGE_RELEASE_BASE_URL:-https://install.dognet.cam/releases/latest/download}" BOOTSTRAP_URL="${DOG_EDGE_BOOTSTRAP_URL:-https://install.dognet.cam/edge}" INSTALL_ROOT="" CONFIG_DIR="" DATA_DIR="" LOG_DIR="" # Empty means "whatever the bundle's installer defaults to". This bootstrap is # fetched standalone (curl | sh) so it cannot source the bundle's common.sh; not # defaulting here is what keeps the default in exactly one place per side # (dog_default_webui_port) instead of drifting in a script nobody rebuilds. WEBUI_PORT="${DOG_EDGE_WEBUI_PORT:-}" CLOUD_ENDPOINT="${DOG_EDGE_CLOUD_ENDPOINT:-wss://dognet.cam/ws/edge}" EXPECTED_VERSION="" while [ "$#" -gt 0 ]; do case "$1" in --update) MODE="update" shift ;; --channel) CHANNEL="$2" shift 2 ;; --release-base-url) RELEASE_BASE_URL="$2" shift 2 ;; --bootstrap-url) BOOTSTRAP_URL="$2" shift 2 ;; --install-root) INSTALL_ROOT="$2" shift 2 ;; --config-dir) CONFIG_DIR="$2" shift 2 ;; --data-dir) DATA_DIR="$2" shift 2 ;; --log-dir) LOG_DIR="$2" shift 2 ;; --webui-port) WEBUI_PORT="$2" shift 2 ;; --cloud-endpoint) CLOUD_ENDPOINT="$2" shift 2 ;; --expected-version) EXPECTED_VERSION="$2" shift 2 ;; *) die "Unknown argument: $1" ;; esac done detect_platform tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT INT TERM manifest_path="${tmpdir}/stable.manifest" manifest_sig_path="${tmpdir}/stable.manifest.sig" step "Fetching signed release manifest" curl -fsSL "${RELEASE_BASE_URL}/${CHANNEL}.manifest" -o "$manifest_path" curl -fsSL "${RELEASE_BASE_URL}/${CHANNEL}.manifest.sig" -o "$manifest_sig_path" verify_release_manifest "$manifest_path" "$manifest_sig_path" validate_release_manifest "$manifest_path" "$CHANNEL" resolve_release_asset "$manifest_path" if [ -n "${EXPECTED_VERSION}" ] && [ "${VERSION}" != "${EXPECTED_VERSION}" ]; then die "Release manifest changed during update: expected ${EXPECTED_VERSION}, got ${VERSION}." fi # The signed version-specific origin is the only archive authority. header flair step "Preview" note "Mode: ${MODE}" note "Channel: ${CHANNEL}" note "Release: ${VERSION}" note "Platform: ${PLATFORM_KEY}" note "Model: CERBERUS J-micro H264" # Only claim a port we were actually told to use. An update keeps the port from # the preserved config, so a number printed here would be a guess either way. if [ -n "$WEBUI_PORT" ]; then note "Web UI: :${WEBUI_PORT}" fi note "Release origin: ${ASSET_BASE_URL}" subtle "This installer will:" note "download a verified DOG_ Edge bundle" note "verify or install system ffmpeg 4.4+ with required media capabilities" note "install the runtime and managed model support" note "register the system service" note "start the Web UI and print its address when it is up" note "keep the device ready for pairing" bundle_path="${tmpdir}/${ASSET}" step "Downloading ${ASSET}" download_with_feedback "${ASSET_BASE_URL}/${ASSET}" "$bundle_path" || die "Failed to download ${ASSET}." step "Verifying checksum" ACTUAL_SHA="$(sha256_file "$bundle_path")" [ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] || die "Checksum mismatch for ${ASSET}." ok "Checksum verified" step "Extracting bundle" extract_verified_bundle "$bundle_path" "$tmpdir" bundle_root="$(locate_bundle_root "$tmpdir")" expected_bundle_root="${tmpdir}/${ASSET%.tar.gz}" [ "$bundle_root" = "$expected_bundle_root" ] || \ die "Release archive wrapper does not match the signed asset name." bundle_version="$(tr -d '\n' < "${bundle_root}/VERSION")" [ "$bundle_version" = "$VERSION" ] || \ die "Release archive VERSION does not match the signed manifest." set -- \ --mode "$MODE" \ --release-base-url "$RELEASE_BASE_URL" \ --bootstrap-url "$BOOTSTRAP_URL" \ --channel "$CHANNEL" \ --cloud-endpoint "$CLOUD_ENDPOINT" if [ -n "$WEBUI_PORT" ]; then set -- "$@" --webui-port "$WEBUI_PORT" fi if [ -n "$INSTALL_ROOT" ]; then set -- "$@" --install-root "$INSTALL_ROOT" fi if [ -n "$CONFIG_DIR" ]; then set -- "$@" --config-dir "$CONFIG_DIR" fi if [ -n "$DATA_DIR" ]; then set -- "$@" --data-dir "$DATA_DIR" fi if [ -n "$LOG_DIR" ]; then set -- "$@" --log-dir "$LOG_DIR" fi if "${bundle_root}/scripts/install-local.sh" "$@"; then install_status=0 else install_status=$? fi exit "$install_status"