#!/bin/sh
# ---------------------------------------------------------------------------
# tilde - manage user-submitted scripts and apps
# Copyright 2018, Ben Harris <ben@tilde.team>
#
# GPL-3.0-or-later. See LICENSE for the full licence text.
# ---------------------------------------------------------------------------

set -u
umask 077
# Preserve users' PATH, especially for "tilde run". Sanitize only root commands.
if [ "$(/usr/bin/id -u)" -eq 0 ]; then
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    export PATH
fi

PROGNAME=${0##*/}
VERSION=0.2.1
ROOT=/tilde
BIN=$ROOT/bin
DESCRIPTIONS=$ROOT/descriptions
PENDING=$ROOT/pending-submissions
APPROVED=$ROOT/approved
META=$ROOT/metadata
REVIEW=$ROOT/review
REJECTED=$ROOT/rejected
LOCK=$ROOT/.launcher.lock
MAX_SCRIPT=52428800
MAX_DESCRIPTION=16384
WORK=

fail() {
    printf '%s: %s\n' "$PROGNAME" "$1" >&2
    exit 1
}

warn() {
    printf '%s: %s\n' "$PROGNAME" "$1" >&2
}

cleanup() {
    if [ -n "$WORK" ]; then
        rm -rf -- "$WORK"
        WORK=
    fi
}
trap 'cleanup' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP

require_root() {
    [ "$(id -u)" -eq 0 ] || fail 'this command requires root'
}

valid_name() {
    case ${1-} in
        ''|.*|*[!a-zA-Z0-9_.+-]*) return 1 ;;
        *) return 0 ;;
    esac
}

check_name() {
    valid_name "${1-}" || fail 'invalid script name (use letters, numbers, _, -, + or .; not starting with .)'
    case $1 in
        tilde|about|description|list|ls|submit|update|approve|revoke|run|help|apropos|pending|status|migrate|init|version)
            fail "$1 is a reserved name" ;;
    esac
}

valid_user() {
    case ${1-} in
        ''|.*|*[!a-zA-Z0-9_.-]*) return 1 ;;
        *) return 0 ;;
    esac
}

# Never trust pre-existing filesystem entries when operating as root.
trusted_dir() {
    [ -d "$1" ] && [ ! -L "$1" ] || fail "not a directory: $1"
    [ "$(stat -c %u -- "$1")" = 0 ] || fail "directory must be owned by root: $1"
    mode=$(stat -c %a -- "$1") || fail "cannot stat $1"
    [ $((0$mode & 0022)) -eq 0 ] || fail "directory is writable by group/others: $1"
}

storage_ready() {
    trusted_dir "$ROOT"
    trusted_dir "$BIN"
    trusted_dir "$DESCRIPTIONS"
    trusted_dir "$APPROVED"
    trusted_dir "$META"
    trusted_dir "$REJECTED"
    trusted_dir "$REVIEW"
    [ $((0$(stat -c %a -- "$REVIEW") & 0077)) -eq 0 ] || fail 'review directory must be private (0700)'
    [ -d "$PENDING" ] && [ ! -L "$PENDING" ] || fail "invalid pending directory"
    [ "$(stat -c %u -- "$PENDING")" = 0 ] || fail "pending directory must be root-owned"
    # The pending parent must be sticky when it is publicly writable.
    mode=$(stat -c %a -- "$PENDING") || fail 'cannot stat pending directory'
    [ $((0$mode & 01000)) -ne 0 ] || fail 'pending directory needs the sticky bit'
}

init_storage() {
    require_root
    for dir in "$ROOT" "$BIN" "$DESCRIPTIONS" "$APPROVED" "$META" "$REJECTED" "$REVIEW" "$PENDING"; do
        [ ! -L "$dir" ] || fail "refusing symlink: $dir"
        if [ -e "$dir" ]; then
            [ -d "$dir" ] || fail "not a directory: $dir"
            if [ "$dir" = "$PENDING" ]; then
                [ "$(stat -c %u -- "$dir")" = 0 ] || fail "directory must be root-owned: $dir"
            else
                trusted_dir "$dir"
            fi
        else
            mkdir -m 0755 -- "$dir" || fail "cannot create $dir"
        fi
    done
    chmod 0700 "$REVIEW" || fail 'cannot secure review directory'
    chmod 1733 "$PENDING" || fail 'cannot secure pending directory'
    storage_ready
    printf 'Launcher directories are ready. Existing scripts were not changed.\n'
}

admin_lock() {
    require_root
    storage_ready
    [ ! -L "$LOCK" ] || fail 'lock file must not be a symlink'
    (umask 077; : >> "$LOCK") || fail 'cannot open lock file'
    [ "$(stat -c %u -- "$LOCK")" = 0 ] || fail 'lock file must be root-owned'
    exec 9>"$LOCK" || fail 'cannot lock launcher'
    flock -n 9 || fail 'another launcher administration is in progress'
}

prompt() {
    while :; do
        printf '%s [y/n]: ' "$1"
        IFS= read -r answer || return 1
        case $answer in
            y|Y) return 0 ;;
            n|N) return 1 ;;
            *) printf 'Please answer y or n.\n' ;;
        esac
    done
}

ask_line() {
    printf '%s' "$1"
    IFS= read -r INPUT || fail 'input cancelled'
}

help_message() {
    cat <<EOF
Usage: $PROGNAME [option] [command]

Options:
  -h, --help        Show this help
  -v, --version     Show version

Commands:
  list              List approved scripts
  about <script>    Show description and author (alias: description)
  submit            Submit a new script from ~/bin
  update <script>   Submit an update to one of your approved scripts
  pending           List your submitted scripts awaiting review
  status <script>   Show the approval status of a script
  run <script>      Run an approved script (passes along arguments)
  approve           Review and approve pending submissions (root)
  revoke <script>   Revoke an approved script (root)
  migrate [script]  Review and migrate legacy symlinks (root)
  init              Prepare secured /tilde directories (root)

Add /tilde/bin to PATH to run approved scripts directly.
EOF
}

metadata_value() {
    key=$1
    file=$2
    [ -f "$file" ] && [ ! -L "$file" ] || return 1
    sed -n "s/^${key}=//p" "$file" | head -n 1
}

approved_author() {
    metadata_value author "$META/$1"
}

approved_digest() {
    metadata_value sha256 "$META/$1"
}

# The user's own writable directory is never trusted as an approval record.
user_pending_dir() {
    owner=$1
    valid_user "$owner" || fail "invalid username: $owner"
    dir=$PENDING/$owner
    [ -d "$dir" ] && [ ! -L "$dir" ] || fail "missing user submissions directory: $dir"
    [ "$(stat -c %U -- "$dir")" = "$owner" ] || fail "submission directory ownership mismatch: $dir"
    printf '%s\n' "$dir"
}

# Older launcher versions sometimes left root-owned submission directories.
# Admins may review those directories if root alone can change their entries.
# Ordinary users still need user-owned directories for new submissions.
review_user_dir() {
    review_owner=$1
    valid_user "$review_owner" || { warn 'invalid submission account'; return 1; }
    review_dir=$PENDING/$review_owner
    if [ ! -d "$review_dir" ] || [ -L "$review_dir" ]; then
        warn "missing or unsafe submission directory: $review_dir"
        return 1
    fi
    review_dir_owner=$(stat -c %U -- "$review_dir") || return 1
    if [ "$review_dir_owner" != "$review_owner" ]; then
        if [ "$review_dir_owner" != root ]; then
            warn "unexpected owner of $review_dir: $review_dir_owner"
            return 1
        fi
        review_mode=$(stat -c %a -- "$review_dir") || return 1
        if [ $((0$review_mode & 0022)) -ne 0 ]; then
            warn "root-owned legacy directory is writable by other accounts: $review_dir"
            return 1
        fi
    fi
    printf '%s\n' "$review_dir"
}

ensure_user_dir() {
    owner=$1
    valid_user "$owner" || fail 'unsupported account name'
    [ -d "$PENDING" ] && [ ! -L "$PENDING" ] || fail 'launcher has not been initialized by an admin'
    dir=$PENDING/$owner
    if [ ! -e "$dir" ] && [ ! -L "$dir" ]; then
        mkdir -m 0700 -- "$dir" || fail 'cannot create your submissions directory (ask an admin if your name is taken)'
    fi
    user_pending_dir "$owner" >/dev/null
}

send_notice() {
    recipient=$1
    subject=$2
    body=$3
    valid_user "$recipient" || { warn 'skipping mail to invalid account'; return 1; }
    if ! {
        printf 'To: %s\nSubject: %s\n\n%s\n' "$recipient" "$subject" "$body"
    } | /usr/sbin/sendmail -t; then
        warn "email to $recipient failed; the filesystem changes were retained"
        return 1
    fi
}

submit_script() {
    operation=$1
    [ "$(id -u)" -ne 0 ] || fail 'submit as your own user, not root'
    storage_ready
    current_user=$(id -un) || fail 'cannot identify user'
    valid_user "$current_user" || fail 'unsupported account name'
    if [ "$operation" = update ]; then
        name=${2-}
        check_name "$name"
        [ "$(approved_author "$name" 2>/dev/null || :)" = "$current_user" ] || fail 'only the approved author can submit an update'
        [ -L "$BIN/$name" ] || fail 'script is not currently approved'
    else
        printf 'Hello, %s! Ready to submit your script?\n' "$current_user"
        printf 'Place an executable in ~/bin that supports --help.\n'
        prompt 'Continue?' || return 0
        ask_line 'Script name: '
        name=$INPUT
        check_name "$name"
        [ ! -e "$BIN/$name" ] && [ ! -L "$BIN/$name" ] || fail 'name already approved; use update if it is yours'
        [ ! -f "$META/$name" ] || fail 'name already has an approval record'
    fi
    source=$HOME/bin/$name
    if [ "$operation" = submit ]; then
        located=$(command -v "$name" 2>/dev/null || :)
        if [ -n "$located" ] && [ "$(readlink -f -- "$located" 2>/dev/null || :)" != "$(readlink -f -- "$source" 2>/dev/null || :)" ]; then
            fail "$name is already an existing command; use another name"
        fi
    fi
    [ -f "$source" ] && [ -x "$source" ] || fail "executable not found: $source"
    if [ "$(wc -c < "$source")" -gt "$MAX_SCRIPT" ]; then
        fail 'script is too large (limit: 50 MiB)'
    fi
    if [ "$operation" = update ]; then
        printf 'Updating %s (the currently approved version keeps working).\n' "$name"
    fi
    ask_line 'Description: '
    description=$INPUT
    [ -n "$description" ] || fail 'description is required'
    [ "${#description}" -le "$MAX_DESCRIPTION" ] || fail 'description is too long'
    prompt 'Send this version to the admins for review?' || return 0
    ensure_user_dir "$current_user"
    parent=$PENDING/$current_user
    target=$parent/$name
    if [ -e "$target" ] || [ -L "$target" ]; then
        [ -d "$target" ] && [ ! -L "$target" ] || fail 'unsafe existing submission directory'
        prompt 'Replace your previous pending submission?' || return 0
    fi
    stage=$(mktemp -d "$parent/.submit.XXXXXXXX") || fail 'cannot stage submission'
    if ! cp -- "$source" "$stage/$name" ||
       ! chmod 0700 "$stage/$name" ||
       ! printf '%s\n' "$description" > "$stage/description.txt"; then
        rm -rf -- "$stage"
        fail 'cannot stage submission'
    fi
    if [ -e "$target" ]; then
        rm -rf -- "$target" || fail 'cannot replace your pending submission'
    fi
    if ! mv -- "$stage" "$target"; then
        rm -rf -- "$stage"
        fail 'cannot save submission'
    fi
    send_notice root 'tilde script submission' "Submission from $current_user: $name. Review with sudo tilde approve." || :
    printf 'Submitted %s for review.\n' "$name"
}

# Capture potentially hostile user-controlled files *as that user*, never as root.
# Review and publication use only the resulting root-owned immutable snapshot.
capture() {
    author=$1
    path=$2
    dest=$3
    maximum=$4
    [ -f "$path" ] || return 1
    timeout 60s runuser -u "$author" -- head -c "$((maximum + 1))" -- "$path" > "$dest" || return 1
    [ "$(wc -c < "$dest")" -le "$maximum" ] || return 1
}

capture_submission() {
    author=$1
    name=$2
    if ! getent passwd "$author" >/dev/null; then
        warn "submission account no longer exists: $author"
        return 1
    fi
    folder=$(review_user_dir "$author") || return 1
    submission=$folder/$name
    if [ ! -d "$submission" ] || [ -L "$submission" ]; then
        warn "missing or unsafe submission folder: $submission"
        return 1
    fi
    if [ -L "$submission/$name" ] && [ ! -e "$submission/$name" ]; then
        warn "broken submitted-script symlink: $author/$name; ask the author to restore or resubmit"
        return 1
    fi
    if [ ! -f "$submission/$name" ]; then
        warn "submitted executable is missing or not a regular file: $author/$name"
        return 1
    fi
    if [ ! -f "$submission/description.txt" ]; then
        warn "missing description: $author/$name"
        return 1
    fi
    if ! runuser -u "$author" -- test -r "$submission/$name" ||
       ! runuser -u "$author" -- test -x "$submission/$name"; then
        warn "author cannot read or execute submitted script: $author/$name"
        return 1
    fi
    if ! runuser -u "$author" -- test -r "$submission/description.txt"; then
        warn "author cannot read submission description: $author/$name"
        return 1
    fi
    WORK=$(mktemp -d "$REVIEW/.review.XXXXXXXX") || fail 'cannot stage review'
    if ! capture "$author" "$submission/$name" "$WORK/script" "$MAX_SCRIPT"; then
        warn "cannot capture script (unreadable, changed or larger than 50 MiB): $author/$name"
        return 1
    fi
    if ! capture "$author" "$submission/description.txt" "$WORK/description" "$MAX_DESCRIPTION"; then
        warn "cannot capture description (unreadable or larger than 16 KiB): $author/$name"
        return 1
    fi
    if [ ! -s "$WORK/script" ] || [ ! -s "$WORK/description" ]; then
        warn "submitted script or description is empty: $author/$name"
        return 1
    fi
}

log_event() {
    logger -t tilde-launcher -- "$1" 2>/dev/null || :
}

write_metadata() {
    name=$1
    author=$2
    digest=$3
    temp=$(mktemp "$META/.record.XXXXXXXX") || return 1
    if ! printf 'author=%s\nsha256=%s\napproved_at=%s\nstate=approved\n' \
        "$author" "$digest" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$temp" ||
       ! chmod 0644 "$temp" || ! mv -f -- "$temp" "$META/$name"; then
        rm -f -- "$temp"
        return 1
    fi
}

publish() {
    author=$1
    name=$2
    digest=$(sha256sum "$WORK/script" | cut -d ' ' -f 1) || fail 'cannot hash script'
    folder=$APPROVED/$name
    [ ! -L "$folder" ] || fail 'approved directory must not be a symlink'
    if [ ! -e "$folder" ]; then
        mkdir -m 0755 -- "$folder" || fail 'cannot create approved directory'
    fi
    trusted_dir "$folder"
    version=$folder/$digest
    [ ! -L "$version" ] || fail 'approved version must not be a symlink'
    if [ ! -e "$version" ]; then
        temp=$(mktemp "$folder/.version.XXXXXXXX") || fail 'cannot stage approved version'
        if ! cp -- "$WORK/script" "$temp" || ! chmod 0755 "$temp" || ! mv -- "$temp" "$version"; then
            rm -f -- "$temp"
            fail 'cannot publish approved version'
        fi
    else
        [ -f "$version" ] &&
            [ "$(stat -c %u -- "$version")" = 0 ] &&
            [ $((0$(stat -c %a -- "$version") & 0022)) -eq 0 ] &&
            [ "$(sha256sum "$version" | cut -d ' ' -f 1)" = "$digest" ] || fail 'existing approved version has changed'
    fi
    dtmp=$(mktemp "$DESCRIPTIONS/.description.XXXXXXXX") || fail 'cannot stage description'
    if ! cp -- "$WORK/description" "$dtmp" || ! chmod 0644 "$dtmp" || ! mv -f -- "$dtmp" "$DESCRIPTIONS/$name"; then
        rm -f -- "$dtmp"
        fail 'cannot publish description'
    fi
    old_target=$(readlink -f -- "$BIN/$name" 2>/dev/null || :)
    chmod 0755 -- "$version" || fail 'cannot activate approved version'
    write_metadata "$name" "$author" "$digest" || fail 'cannot save approval metadata'
    linktmp=$BIN/.link.$name.$$
    [ ! -e "$linktmp" ] && [ ! -L "$linktmp" ] || fail 'temporary link collision'
    ln -s -- "$version" "$linktmp" || fail 'cannot create approved link'
    if ! mv -Tf -- "$linktmp" "$BIN/$name"; then
        rm -f -- "$linktmp"
        fail 'cannot activate approved version'
    fi
    # Previously approved revisions are retained but no longer user-executable.
    case $old_target in
        "$APPROVED/$name/"*)
            if [ "$old_target" != "$version" ]; then
                chmod 0700 -- "$old_target" || fail 'could not restrict retired version'
            fi ;;
    esac
    log_event "approved $name by $author sha256=$digest"
    send_notice "$author" 'tilde script approved' "$name is approved and available at $BIN/$name" || :
    printf 'Approved %s by %s.\n' "$name" "$author"
}

review_snapshot() {
    author=$1
    name=$2
    printf '\nScript: %s\nAuthor: %s\n' "$name" "$author"
    printf 'SHA-256: %s\n' "$(sha256sum "$WORK/script" | cut -d ' ' -f 1)"
    printf 'Description:\n'
    LC_ALL=C cat -v "$WORK/description"
    printf '\nSource preview (up to 80 lines / 8 KiB, control characters escaped):\n'
    head -c 8192 "$WORK/script" | head -n 80 | LC_ALL=C cat -v
    while :; do
        printf '\n[a]pprove, [r]eject, [v]iew full source, [s]kip? '
        IFS= read -r answer || return 1
        case $answer in
            a|A) return 0 ;;
            r|R) return 2 ;;
            v|V) LC_ALL=C cat -v "$WORK/script" ;;
            s|S) return 1 ;;
            *) printf 'Choose a, r, v or s.\n' ;;
        esac
    done
}

submission_fingerprint() {
    sha256sum "$1" "$2" | cut -d ' ' -f 1 | sha256sum | cut -d ' ' -f 1
}

reject_snapshot() {
    author=$1
    name=$2
    ask_line 'Reason for rejection: '
    reason=$INPUT
    [ -n "$reason" ] || { warn 'rejection cancelled: reason required'; return 0; }
    dir=$REJECTED/$author
    if [ ! -e "$dir" ]; then
        mkdir -m 0755 -- "$dir" || fail 'cannot create rejection records'
    fi
    trusted_dir "$dir"
    fingerprint=$(submission_fingerprint "$WORK/script" "$WORK/description")
    temp=$(mktemp "$dir/.rejection.XXXXXXXX") || fail 'cannot stage rejection record'
    if ! printf 'fingerprint=%s\nrejected_at=%s\n' \
        "$fingerprint" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$temp" ||
       ! chmod 0644 "$temp" || ! mv -f -- "$temp" "$dir/$name"; then
        rm -f -- "$temp"
        fail 'cannot save rejection record'
    fi
    log_event "rejected $name by $author"
    send_notice "$author" 'tilde script rejected' "$name was not approved. Reason: $reason. You may submit a revised version." || :
    printf 'Rejected %s by %s.\n' "$name" "$author"
}

approve_one() {
    author=$1
    name=$2
    check_name "$name"
    if ! capture_submission "$author" "$name"; then
        warn "could not safely read $author/$name; skipping"
        cleanup
        return 0
    fi
    digest=$(sha256sum "$WORK/script" | cut -d ' ' -f 1)
    existing_author=$(approved_author "$name" 2>/dev/null || :)
    existing_digest=$(approved_digest "$name" 2>/dev/null || :)
    if [ -n "$existing_author" ] && [ "$existing_author" != "$author" ]; then
        warn "name $name belongs to $existing_author; skipping $author"
        cleanup
        return 0
    fi
    if [ "$digest" = "$existing_digest" ] && [ -L "$BIN/$name" ]; then
        cleanup
        return 0
    fi
    rejected=$(metadata_value fingerprint "$REJECTED/$author/$name" 2>/dev/null || :)
    fingerprint=$(submission_fingerprint "$WORK/script" "$WORK/description")
    if [ -n "$rejected" ] && [ "$rejected" = "$fingerprint" ]; then
        cleanup
        return 0
    fi
    if [ -e "$BIN/$name" ] || [ -L "$BIN/$name" ]; then
        [ -n "$existing_author" ] || {
            warn "$name is an untracked legacy script; use tilde migrate first"
            cleanup
            return 0
        }
        [ -L "$BIN/$name" ] || fail "refusing to replace a non-symlink: $BIN/$name"
    fi
    if review_snapshot "$author" "$name"; then
        publish "$author" "$name"
    else
        action=$?
        [ "$action" -ne 2 ] || reject_snapshot "$author" "$name"
    fi
    cleanup
}

approve_queue() {
    admin_lock
    printf 'Pending submissions:\n'
    for ownerdir in "$PENDING"/*; do
        [ -d "$ownerdir" ] && [ ! -L "$ownerdir" ] || continue
        author=${ownerdir##*/}
        if ! review_user_dir "$author" >/dev/null; then
            continue
        fi
        for item in "$ownerdir"/*; do
            [ -d "$item" ] && [ ! -L "$item" ] || continue
            name=${item##*/}
            valid_name "$name" || { warn "skipping invalid name $name"; continue; }
            case $name in
                tilde|about|description|list|ls|submit|update|approve|revoke|run|help|apropos|pending|status|migrate|init|version)
                    continue ;;
            esac
            approve_one "$author" "$name"
        done
    done
    printf 'Approval queue complete.\n'
}

migrate_one() {
    name=$1
    check_name "$name"
    link=$BIN/$name
    [ -L "$link" ] || { warn "$name is not a legacy symlink; skipping"; return 0; }
    if [ -n "$(approved_author "$name" 2>/dev/null || :)" ]; then
        warn "$name has already been migrated"
        return 0
    fi
    if ! source=$(readlink -e -- "$link" 2>/dev/null); then
        warn "broken legacy symlink: $name; leaving it unchanged and skipping"
        return 1
    fi
    if [ ! -f "$source" ]; then
        warn "legacy target is not a regular file: $name; skipping"
        return 1
    fi
    author=$(stat -c %U -- "$source") || fail 'cannot inspect author'
    valid_user "$author" || fail 'cannot determine legacy author'
    home=$(getent passwd "$author" | cut -d: -f6)
    case $source in
        "$home"/bin/*) : ;;
        *) warn "legacy target is outside $author's ~/bin: $source"; return 0 ;;
    esac
    [ -f "$DESCRIPTIONS/$name" ] && [ ! -L "$DESCRIPTIONS/$name" ] || {
        warn "missing or unsafe legacy description for $name"
        return 0
    }
    [ "$(stat -c %u -- "$DESCRIPTIONS/$name")" = 0 ] || {
        warn "legacy description $name is not root-owned"
        return 0
    }
    WORK=$(mktemp -d "$REVIEW/.review.XXXXXXXX") || fail 'cannot stage migration'
    if ! capture "$author" "$source" "$WORK/script" "$MAX_SCRIPT" ||
       ! cp -- "$DESCRIPTIONS/$name" "$WORK/description"; then
        warn "could not capture legacy $name"
        cleanup
        return 0
    fi
    [ "$(wc -c < "$WORK/description")" -le "$MAX_DESCRIPTION" ] || fail 'legacy description is too large'
    printf '\nLegacy link: %s -> %s\n' "$name" "$source"
    printf 'The reviewed copy will be immutable. Scripts requiring adjacent files may need changes.\n'
    if review_snapshot "$author" "$name"; then
        publish "$author" "$name"
    fi
    cleanup
}

migrate_legacy() {
    admin_lock
    if [ -n "${1-}" ]; then
        migrate_one "$1"
    else
        skipped=0
        for item in "$BIN"/*; do
            [ -L "$item" ] || continue
            migrate_one "${item##*/}" || skipped=$((skipped + 1))
        done
        if [ "$skipped" -gt 0 ]; then
            warn "migration finished with $skipped unreadable or broken link(s); no affected links were removed"
            return 1
        fi
    fi
}

revoke_script() {
    admin_lock
    name=${1-}
    check_name "$name"
    link=$BIN/$name
    [ -L "$link" ] || fail "$name is not an approved symlink"
    author=$(approved_author "$name" 2>/dev/null || :)
    if [ -z "$author" ]; then
        source=$(readlink -e -- "$link" 2>/dev/null || :)
        if [ -n "$source" ]; then
            author=$(stat -c %U -- "$source" 2>/dev/null || :)
        fi
    fi
    if ! valid_user "$author"; then
        author=
    fi
    if [ -z "$author" ]; then
        printf 'No author could be identified for %s. This may be a broken legacy link.\n' "$name"
        prompt "Revoke $name (author unknown)?" || { printf 'Cancelled.\n'; return 0; }
    else
        prompt "Revoke $name by $author?" || { printf 'Cancelled.\n'; return 0; }
    fi
    ask_line 'Reason: '
    reason=$INPUT
    [ -n "$reason" ] || fail 'a reason is required'
    old_target=$(readlink -f -- "$link" 2>/dev/null || :)
    # Retire the previously published file before removing its public link.
    case $old_target in
        "$APPROVED/$name/"*)
            chmod 0700 -- "$old_target" || fail 'could not restrict revoked executable' ;;
    esac
    # Only unlink exact, validated entries. Do not recurse into user submissions.
    rm -- "$link" || fail 'could not revoke executable'
    if [ -e "$DESCRIPTIONS/$name" ] || [ -L "$DESCRIPTIONS/$name" ]; then
        rm -- "$DESCRIPTIONS/$name" || warn 'could not remove description'
    fi
    if [ -e "$META/$name" ] || [ -L "$META/$name" ]; then
        rm -- "$META/$name" || warn 'could not remove metadata'
    fi
    log_event "revoked $name by ${author:-unknown}"
    if [ -n "$author" ]; then
        send_notice "$author" 'tilde script revoked' "$name was revoked. Reason: $reason. You may submit a revised version." || :
    else
        warn 'author unknown; no notification sent'
    fi
    printf '%s revoked. Older reviewed copies were retained in %s.\n' "$name" "$APPROVED/$name"
}

list_scripts() {
    printf 'Available scripts:\n\n'
    for item in "$BIN"/*; do
        [ -x "$item" ] && [ -f "$item" ] || continue
        name=${item##*/}
        author=$(approved_author "$name" 2>/dev/null || :)
        if [ -z "$author" ]; then
            author=$(stat -c %U -- "$item" 2>/dev/null || printf unknown)
        fi
        printf '%s by %s\n' "$name" "$author"
        if [ -f "$DESCRIPTIONS/$name" ]; then
            cat -- "$DESCRIPTIONS/$name"
        fi
        printf '\n'
    done
}

about_script() {
    name=${1-}
    check_name "$name"
    [ -f "$DESCRIPTIONS/$name" ] || fail "$name not found; use '$PROGNAME list'"
    cat -- "$DESCRIPTIONS/$name"
    author=$(approved_author "$name" 2>/dev/null || :)
    [ -z "$author" ] || printf '\nAuthor: %s\n' "$author"
}

pending_scripts() {
    current_user=$(id -un)
    valid_user "$current_user" || fail 'unsupported account name'
    [ -d "$PENDING/$current_user" ] || { printf 'No submissions.\n'; return 0; }
    folder=$(user_pending_dir "$current_user") || return 1
    found=0
    for item in "$folder"/*; do
        [ -d "$item" ] && [ ! -L "$item" ] || continue
        name=${item##*/}
        valid_name "$name" || continue
        current=$(approved_digest "$name" 2>/dev/null || :)
        submitted=$(sha256sum "$item/$name" 2>/dev/null | cut -d ' ' -f 1)
        if [ -n "$current" ] && [ "$current" = "$submitted" ] && [ -L "$BIN/$name" ]; then
            continue
        fi
        rejected=$(metadata_value fingerprint "$REJECTED/$current_user/$name" 2>/dev/null || :)
        fingerprint=$(submission_fingerprint "$item/$name" "$item/description.txt" 2>/dev/null || :)
        if [ -n "$fingerprint" ] && [ "$fingerprint" = "$rejected" ]; then
            printf '%s (rejected; revise with submit)\n' "$name"
        else
            printf '%s (awaiting review)\n' "$name"
        fi
        found=1
    done
    [ "$found" -eq 1 ] || printf 'No pending submissions.\n'
}

status_script() {
    name=${1-}
    check_name "$name"
    author=$(approved_author "$name" 2>/dev/null || :)
    if [ -n "$author" ] && [ -L "$BIN/$name" ]; then
        printf '%s: approved (author: %s)\n' "$name" "$author"
    elif [ -L "$BIN/$name" ]; then
        if [ -f "$BIN/$name" ] && [ -x "$BIN/$name" ]; then
            printf '%s: approved (legacy symlink; needs migration)\n' "$name"
        else
            printf '%s: broken or inaccessible approved symlink; requires admin review\n' "$name"
        fi
    else
        printf '%s: not approved\n' "$name"
    fi
    current_user=$(id -un)
    if [ -d "$PENDING/$current_user/$name" ]; then
        submission=$PENDING/$current_user/$name
        fingerprint=$(submission_fingerprint "$submission/$name" "$submission/description.txt" 2>/dev/null || :)
        rejected=$(metadata_value fingerprint "$REJECTED/$current_user/$name" 2>/dev/null || :)
        if [ -n "$fingerprint" ] && [ "$fingerprint" = "$rejected" ]; then
            printf 'Latest submission: rejected (submit a revision).\n'
        elif [ -n "$author" ] && [ "$author" = "$current_user" ] &&
             [ "$(sha256sum "$submission/$name" 2>/dev/null | cut -d ' ' -f 1)" = "$(approved_digest "$name" 2>/dev/null || :)" ]; then
            printf 'Submitted version matches the current approval.\n'
        else
            printf 'A submission is awaiting review for %s.\n' "$current_user"
        fi
    fi
}

case ${1-} in
    -h|--help|'') help_message ;;
    -v|--version) printf '%s\n' "$VERSION" ;;
    init) init_storage ;;
    list|ls) list_scripts ;;
    about|description) about_script "${2-}" ;;
    submit) submit_script submit ;;
    update) submit_script update "${2-}" ;;
    pending) pending_scripts ;;
    status) status_script "${2-}" ;;
    approve) approve_queue ;;
    migrate) migrate_legacy "${2-}" ;;
    revoke) revoke_script "${2-}" ;;
    run)
        name=${2-}
        check_name "$name"
        [ -f "$BIN/$name" ] && [ -x "$BIN/$name" ] || fail "$name not found or not approved"
        shift 2
        exec "$BIN/$name" "$@"
        ;;
    *) fail "invalid command: $1 (use --help)" ;;
esac
