Skip to content

Instantly share code, notes, and snippets.

@Cellane
Created September 22, 2026 02:49
Show Gist options
  • Select an option

  • Save Cellane/ab2c422ce981afa2101ba68fa3cd11b0 to your computer and use it in GitHub Desktop.

Select an option

Save Cellane/ab2c422ce981afa2101ba68fa3cd11b0 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -Eeuo pipefail
# Make Node.js applications (Bitwarden CLI) trust Arch's system CA bundle,
# including our private CA.
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# ---------------------------------------------------------------------------
# Account configuration
# ---------------------------------------------------------------------------
ACCOUNT="${1:-DEFAULT_SQUEENIX_ACCOUNT_HERE}"
XIV_ACCOUNT_ID="${XIV_ACCOUNT_ID:-${ACCOUNT,,}-True-False}"
BW_ITEM="${BW_ITEM:-secure.square-enix.com/${ACCOUNT}}"
# ---------------------------------------------------------------------------
# Application / KWallet configuration
# ---------------------------------------------------------------------------
XIVLAUNCHER="${XIVLAUNCHER:-xivlauncher-rb}"
KWALLET="${KWALLET:-kdewallet}"
KWALLET_FOLDER="${KWALLET_FOLDER:-Passwords}"
KWALLET_ENTRY="${KWALLET_ENTRY:-xiv-autologin-bitwarden-master}"
OTP_SERVER="http://127.0.0.1:4646"
MAX_WAIT="${MAX_WAIT:-90}"
POLL_INTERVAL="${POLL_INTERVAL:-1}"
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/xiv-autologin"
mkdir -p "$LOG_DIR"
exec >>"$LOG_DIR/${ACCOUNT}.log" 2>&1
printf '\n=== XIV autologin started: %s ===\n' "$(date --iso-8601=seconds)"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
need_command() {
command -v "$1" >/dev/null 2>&1 ||
die "Required command not found: $1"
}
need_command "$XIVLAUNCHER"
need_command bw
need_command jq
need_command kwallet-query
need_command curl
need_command setxkbmap
# Session acquired by this script, if any.
BW_SESSION_LOCAL=""
bw_cmd() {
if [[ -n "$BW_SESSION_LOCAL" ]]; then
BW_SESSION="$BW_SESSION_LOCAL" bw "$@"
else
bw "$@"
fi
}
unlock_bitwarden() {
printf '🔐 Unlocking Bitwarden using KWallet...\n'
local bw_password
bw_password="$(
kwallet-query \
-f "$KWALLET_FOLDER" \
-r "$KWALLET_ENTRY" \
"$KWALLET"
)" || die "Could not read Bitwarden password from KWallet."
[[ -n "$bw_password" ]] ||
die "KWallet entry '$KWALLET_ENTRY' is empty."
BW_SESSION_LOCAL="$(
BW_PASSWORD="$bw_password" \
bw unlock \
--passwordenv BW_PASSWORD \
--raw \
--nointeraction
)" || {
unset bw_password
die "Could not unlock Bitwarden."
}
unset bw_password
[[ -n "$BW_SESSION_LOCAL" ]] ||
die "Bitwarden returned an empty session key."
printf '✓ Bitwarden unlocked.\n'
}
get_totp() {
local otp
otp="$(
bw_cmd get totp "$BW_ITEM" \
--raw \
--nointeraction \
2>/dev/null
)" || return 1
[[ "$otp" =~ ^[0-9]{6}$ ]] || return 1
printf '%s' "$otp"
}
configure_xwayland_keyboard() {
printf '⌨️ Configuring XWayland keyboard layout...\n'
local attempt
for attempt in {1..20}; do
if setxkbmap \
-layout custom \
-variant apple_qwerty \
>/dev/null 2>&1; then
printf '✓ XWayland keyboard layout configured (attempt %d).\n' "$attempt"
return 0
fi
sleep 0.25
done
printf '⚠ Could not configure XWayland keyboard layout.\n' >&2
printf ' Continuing anyway; Czech input may be QWERTZ.\n' >&2
return 0
}
# ---------------------------------------------------------------------------
# Inspect Bitwarden state
# ---------------------------------------------------------------------------
printf '🔎 Checking Bitwarden status...\n'
BW_STATUS_JSON="$(bw status --nointeraction 2>/dev/null)" ||
die "Could not query Bitwarden CLI status."
BW_STATUS="$(
jq -r '.status // empty' <<<"$BW_STATUS_JSON"
)"
case "$BW_STATUS" in
unlocked)
printf '✓ Bitwarden vault is already unlocked.\n'
;;
locked)
printf '• Bitwarden vault is locked.\n'
unlock_bitwarden
;;
unauthenticated)
die "Bitwarden CLI is not logged in. Run 'bw login' once first."
;;
*)
die "Unexpected Bitwarden status: '${BW_STATUS:-<empty>}'"
;;
esac
unset BW_STATUS_JSON
# ---------------------------------------------------------------------------
# Verify vault access
#
# This catches an important edge case:
#
# `bw status` says "unlocked", but this invocation doesn't actually have a
# usable session for `bw get`.
#
# If that happens, just unlock normally through KWallet.
# ---------------------------------------------------------------------------
if ! get_totp >/dev/null; then
if [[ "$BW_STATUS" == "unlocked" ]]; then
printf '⚠ Bitwarden reports unlocked, but vault access failed.\n'
printf ' Falling back to KWallet unlock.\n'
unlock_bitwarden
get_totp >/dev/null ||
die "Could not access TOTP item '$BW_ITEM' after unlocking."
else
die "Could not access TOTP item '$BW_ITEM'."
fi
fi
printf '✓ Bitwarden TOTP entry is accessible.\n'
unset BW_STATUS
# ---------------------------------------------------------------------------
# Launch XIVLauncher-RB
# ---------------------------------------------------------------------------
printf '🚀 Launching XIVLauncher-RB\n'
printf ' Account: %s\n' "$XIV_ACCOUNT_ID"
"$XIVLAUNCHER" \
"--CurrentAccountId=$XIV_ACCOUNT_ID" \
"--IsAutologin=true" \
"--IsOtpServer=true" \
>/dev/null 2>&1 &
XIV_PID=$!
configure_xwayland_keyboard
# ---------------------------------------------------------------------------
# Wait for XIVLauncher to request an OTP
# ---------------------------------------------------------------------------
printf '⏳ Waiting for XIVLauncher OTP server...\n'
DEADLINE=$((SECONDS + MAX_WAIT))
while ((SECONDS < DEADLINE)); do
# Don't wait forever if XIVLauncher itself crashed.
if ! kill -0 "$XIV_PID" 2>/dev/null; then
unset BW_SESSION_LOCAL
die "XIVLauncher-RB exited before reaching the OTP screen."
fi
# Probe the HTTP listener. It only appears once XIVLauncher reaches
# the OTP-entry phase.
if curl \
--silent \
--max-time 1 \
"$OTP_SERVER/" \
>/dev/null 2>&1; then
printf '✓ OTP server is ready.\n'
# Generate a FRESH TOTP at the moment we're ready to send it.
OTP="$(get_totp)" ||
die "Could not obtain TOTP from Bitwarden item '$BW_ITEM'."
printf '🔑 Supplying TOTP to XIVLauncher...\n'
if curl \
--silent \
--fail \
--max-time 2 \
"$OTP_SERVER/ffxivlauncher/$OTP" \
>/dev/null 2>&1; then
unset OTP BW_SESSION_LOCAL
printf '✓ TOTP supplied. Login continuing.\n'
exit 0
fi
# Listener may have changed state between probe and submission.
# Don't reuse an aging OTP.
unset OTP
fi
sleep "$POLL_INTERVAL"
done
unset BW_SESSION_LOCAL
die "XIVLauncher did not request an OTP within ${MAX_WAIT}s."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment