#!/bin/sh # # aura — macOS installer # # curl -fsSL https://unreleased.world/install.sh | sh # # ── Why this exists ───────────────────────────────────────────────────────── # The app is code-signed, but not *notarized* by Apple — notarization needs a # paid Apple Developer account. Anything downloaded through a browser gets # tagged with the `com.apple.quarantine` extended attribute, and Gatekeeper # refuses to open a non-notarized app carrying that tag without the user first # digging through System Settings. # # curl does not set that attribute. So installing this way avoids the browser # tagging step that makes macOS demand a manual override for an app you # deliberately asked for. # # ── What is actually verified, and what is not ────────────────────────────── # Being straight about this, because the previous version of this comment # overstated it. # # • THE DOWNLOAD IS PINNED. This script names an exact release tag and the # exact SHA-256 of the archive for your architecture, and refuses to go on # if the bytes do not match. That is the real protection here: the digest # lives in this file, served from unreleased.world, while the archive comes # from GitHub. Substituting the app therefore requires compromising BOTH, # rather than either one alone. It also means this script pins a VERSION — # see the RELEASE CHECKLIST below. # # • `codesign --verify` PROVES INTEGRITY, NOT IDENTITY. It checks a bundle is # internally consistent with whatever signature it carries — including an # ad-hoc one anyone can generate. It cannot, on its own, tell you who signed # the app. These builds are ad-hoc signed, so that is exactly the situation. # The checksum above is what actually establishes authenticity today. # # • `spctl` IS RUN, BUT ONLY REPORTS. Gatekeeper assessment fails for any # app that is not notarized, so it cannot be a gate until the app is. It is # run and its answer printed rather than claimed. # # When the app gains a Developer ID and notarization, the codesign check should # pin the Team ID and `spctl` should become a hard failure. Until then, do not # read the signature line as proof of origin — read the checksum line. # # Everything here is readable, does exactly what it says, and touches nothing # outside /Applications. Read it before running it — that is the whole point of # shipping it in plain sight. # # ── RELEASE CHECKLIST ─────────────────────────────────────────────────────── # After publishing a new release, update VERSION and both SHA256_* values below, # then deploy. Until you do, this installer keeps serving the pinned version. # That is the safe direction to fail: users get a slightly older build rather # than an unverified one. GitHub publishes each asset's digest — the API field # is `digest`, and the same checksums are listed at /discord. # # The whole body is wrapped in main() and only invoked on the last line, so a # download truncated mid-transfer can never execute a partial script. set -eu main() { REPO="0k0k113/aura" APP_NAME="aura" APP_BUNDLE="${APP_NAME}.app" # Pinned release + expected archive digests. Both must be bumped together. VERSION="v1.0.3" SHA256_arm64="1634d91c1a3154c322506741b2f14f8a6a0517b957b52d369d92313463fa8a7d" SHA256_x64="b941f31cdf4dda58ed3f8de352f93227bf97fce48735a3be4bfda2adbc615874" # Previous bundle names. The install step removes any of these so a rename # cannot leave two copies of the same app in /Applications, both launching and # both competing for Discord. LEGACY_APP_NAMES="Aura Unreleased Presence" # Colours, but only when attached to a terminal. if [ -t 1 ]; then B="$(printf '\033[1m')"; DIM="$(printf '\033[2m')"; R="$(printf '\033[0m')" GREEN="$(printf '\033[32m')"; RED="$(printf '\033[31m')"; YELLOW="$(printf '\033[33m')" else B=""; DIM=""; R=""; GREEN=""; RED=""; YELLOW="" fi say() { printf '%s\n' "$*"; } step() { printf '%s→%s %s\n' "$B" "$R" "$*"; } ok() { printf '%s✓%s %s\n' "$GREEN" "$R" "$*"; } die() { printf '%s✗%s %s\n' "$RED" "$R" "$*" >&2; exit 1; } warn() { printf '%s!%s %s\n' "$YELLOW" "$R" "$*" >&2; } say "" say "${B}aura${R} ${DIM}— Discord Rich Presence for unreleased.world${R}" say "" # ── Preflight ─────────────────────────────────────────────────────────────── [ "$(uname -s)" = "Darwin" ] || die "This installer is for macOS. On Windows or Linux, download from https://unreleased.world/discord" MACOS_MAJOR="$(sw_vers -productVersion | cut -d. -f1)" if [ "$MACOS_MAJOR" -lt 11 ] 2>/dev/null; then die "macOS 11 (Big Sur) or newer is required. You are on $(sw_vers -productVersion)." fi case "$(uname -m)" in arm64) ARCH="arm64"; CHIP="Apple Silicon"; EXPECTED_SHA="$SHA256_arm64" ;; x86_64) ARCH="x64"; CHIP="Intel"; EXPECTED_SHA="$SHA256_x64" ;; *) die "Unsupported architecture: $(uname -m)" ;; esac # A pinned tag, not /releases/latest/download. `latest` is a mutable pointer: # whoever can publish a release controls what it resolves to, and a digest # cannot be pinned against a moving target. URL="https://github.com/${REPO}/releases/download/${VERSION}/Aura-mac-${ARCH}.zip" step "Detected ${B}${CHIP}${R} on macOS $(sw_vers -productVersion)" # ── Choose a destination we can actually write to ──────────────────────────── DEST="/Applications" if [ ! -w "$DEST" ]; then DEST="${HOME}/Applications" mkdir -p "$DEST" warn "/Applications is not writable — installing to ~/Applications instead." fi TMP="$(mktemp -d)" # Clean up on any exit path, including failure. trap 'rm -rf "$TMP"' EXIT INT TERM # ── Download ──────────────────────────────────────────────────────────────── step "Downloading ${VERSION}…" if ! curl -fL# --proto '=https' --tlsv1.2 -o "${TMP}/app.zip" "$URL"; then die "Download failed. Check your connection, or grab ${VERSION} manually from https://github.com/${REPO}/releases/tag/${VERSION}" fi # ── Verify the bytes before touching them ─────────────────────────────────── # # Before unpacking, not after: an archive that is not what we expect should # never be handed to ditto, let alone installed. step "Verifying checksum…" ACTUAL_SHA="$(shasum -a 256 "${TMP}/app.zip" | cut -d' ' -f1)" if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then say "" say " expected ${EXPECTED_SHA}" say " received ${ACTUAL_SHA}" say "" die "CHECKSUM MISMATCH. Not installing. The archive is not the build this installer was pinned to. That is either a stale pin after a new release, or something is wrong. Do not work around it — please report it at https://github.com/${REPO}/issues" fi ok "Checksum matches ${DIM}(sha256 ${EXPECTED_SHA})${R}" # ── Unpack ────────────────────────────────────────────────────────────────── step "Unpacking…" # ditto, not unzip: it preserves the symlinks and extended attributes inside an # .app bundle. A bundle unpacked with plain `unzip` can end up with a broken # signature, which macOS then reports as "damaged". ditto -x -k "${TMP}/app.zip" "${TMP}/unpacked" || die "Could not unpack the archive." SRC="${TMP}/unpacked/${APP_BUNDLE}" [ -d "$SRC" ] || SRC="$(find "${TMP}/unpacked" -maxdepth 2 -name '*.app' -print -quit)" [ -n "$SRC" ] && [ -d "$SRC" ] || die "No .app bundle found inside the archive." # ── Inspect the signature (integrity, and Gatekeeper's opinion) ────────────── step "Checking the code signature…" if codesign --verify --deep --strict "$SRC" >/dev/null 2>&1; then if codesign --display --verbose=2 "$SRC" 2>&1 | grep -q "TeamIdentifier=not set"; then ok "Signature intact ${DIM}(ad-hoc — identity not proven by this check; the checksum above is what vouches for it)${R}" else TEAM="$(codesign --display --verbose=2 "$SRC" 2>&1 | sed -n 's/^TeamIdentifier=//p')" ok "Signature intact ${DIM}(Developer ID, team ${TEAM})${R}" fi else # Refuse rather than install something that will misbehave. An invalid # signature is exactly what produced the old "damaged" error. die "Signature is broken. Not installing. Please report this at https://github.com/${REPO}/issues" fi # Reported, not enforced: assessment fails for anything not notarized, so it # cannot gate an ad-hoc build without blocking every legitimate install. if spctl --assess --type execute "$SRC" >/dev/null 2>&1; then ok "Gatekeeper accepts it ${DIM}(notarized)${R}" else say " ${DIM}Gatekeeper would not accept this build on its own (not notarized).${R}" say " ${DIM}Expected for now — the pinned checksum is what verifies it.${R}" fi # ── Install ───────────────────────────────────────────────────────────────── # Name the installed bundle after what the archive really holds, so an older # archive installs as itself rather than being renamed into a mismatch between # the bundle name and its Info.plist. TARGET="${DEST}/$(basename "$SRC")" # A rename would otherwise leave the previous install behind, running, still # connected to Discord, and competing with the new one. printf '%s\n' "$LEGACY_APP_NAMES" | while IFS= read -r legacy; do [ -n "$legacy" ] || continue LEGACY_TARGET="${DEST}/${legacy}.app" [ "$LEGACY_TARGET" != "$TARGET" ] || continue [ -d "$LEGACY_TARGET" ] || continue step "Removing the previous ${legacy} install…" osascript -e "tell application \"${legacy}\" to quit" >/dev/null 2>&1 || true sleep 1 rm -rf "$LEGACY_TARGET" || warn "Could not remove ${LEGACY_TARGET} — delete it by hand." done if [ -d "$TARGET" ]; then step "Replacing the existing installation…" # Quit it first, or the replace races a running process. osascript -e "tell application \"$(basename "$TARGET" .app)\" to quit" >/dev/null 2>&1 || true sleep 1 rm -rf "$TARGET" || die "Could not remove the old version at ${TARGET}" fi step "Installing to ${DEST}…" ditto "$SRC" "$TARGET" || die "Could not copy the app to ${DEST}" # Belt and braces: curl does not quarantine, but if this script was itself # fetched and run in some other way, clear the flag rather than leave the user # staring at a Gatekeeper dialog. Safe only because the bytes were checksummed # against a pin above — never do this to something unverified. xattr -dr com.apple.quarantine "$TARGET" 2>/dev/null || true ok "Installed ${B}${TARGET}${R}" # ── Launch ────────────────────────────────────────────────────────────────── say "" step "Launching…" open "$TARGET" || die "Installed, but could not launch. Open it from ${DEST}." say "" ok "${B}Done.${R}" say "" say " ${DIM}Make sure the Discord desktop app is running, then play something${R}" say " ${DIM}on unreleased.world — your profile will show what you're listening to.${R}" say "" say " ${DIM}Not showing up? In Discord: Settings → Activity Privacy →${R}" say " ${DIM}\"Display current activity as a status message\".${R}" say "" } # Nothing above ran. A truncated download stops here and does nothing. main "$@"