Migrate Authelia addon; fix real config-clobbering bug in save_config; bump to v2.6.0

Second Addon migrated: menus/addon_authelia.sh (encrypted SSO
credentials, AES-256-CBC with a key derived from /etc/machine-id via
scrypt - same algorithm main.js decrypts with - plus the full
Dockerized server-side setup instructions, now viewable again later
without reconfiguring).

Investigating how to wire its three config.json fields (autheliaURL/
autheliaUsername/autheliaEncryptedPassword) into lib/config.sh surfaced
a real, currently-shipping bug that has nothing to do with Authelia
specifically: save_config() did a full `jq -n` rebuild of config.json
from a fixed list of known fields - identical to what the legacy
script's own save_config still does. The legacy configure_authelia()
writes its three fields via a careful `. + {...}` merge that preserves
everything else already in the file, but neither save_config knew those
fields existed - so the next time a user visited Sites, Touch Controls,
Navigation, or Password Protection (all of which call save_config),
their Authelia credentials were silently deleted. This bug already
existed in the shipped single-file installer; it was ported faithfully
into lib/config.sh's first version because no test happened to set an
untracked field before calling save_config.

Fixed in lib/config.sh: save_config now merges its known fields onto
whatever's already in config.json (jq `. + {...}`) instead of rebuilding
the file from nothing, with a `jq empty` validity check falling back to
`{}` if the existing file is missing or corrupt. Any field this tool
doesn't track - Authelia's three today, anything else a future addon
adds tomorrow - now survives automatically. autheliaURL/
autheliaUsername/autheliaEncryptedPassword are also tracked fields in
their own right now (load_existing_config/save_config), consistent with
every other config.json field this tool manages, giving Authelia both a
direct fix and the general safety net.

The equivalent bug still exists, unfixed, in ubuntu-based-kiosk.sh's own
save_config - noted in both that script's changelog and the Readme's
"Modular Management" section as an open question: whether to backport
just that one fix into the legacy script now, independent of the wider
migration, given it's a real credential-loss bug affecting the
currently-shipping installer today.

Verified:
- New dedicated test (test_save_merge.sh) proving the save_config fix
  itself: seeded config.json with a simulated untracked field via the
  same `. + {...}` merge Authelia's own code uses, called save_config
  from an unrelated context (Sites deleting a tab), and confirmed the
  untracked field survived while the tab deletion still correctly took
  effect (not undone by the merge) - plus corrupt-JSON and
  missing-file edge cases both handled without crashing.
- New scratch-config test for addon_authelia.sh using REAL encryption
  (this sandbox has both Node and /etc/machine-id): configured with a
  real password, then decrypted the stored ciphertext using main.js's
  exact algorithm (independently reproduced in the test) and confirmed
  it recovers the original password exactly - true interoperability,
  not just "some ciphertext was produced." Also covered cancel paths,
  clearing the configuration, the encryption-unavailable failure path,
  and confirmed Authelia's config survives an unrelated Sites save.
- Full regression: re-ran all 9 prior scratch/stub test suites after
  both the lib/config.sh changes - all still clean.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user with a seeded minimal config.json, through Addons ->
  Authelia -> Configure with a real URL/username/password -> confirmed
  the resulting config.json on disk, and independently decrypted the
  stored password for real using main.js's algorithm to confirm it
  matches exactly. Clean exit code 0 throughout.
This commit is contained in:
Claude
2026-08-18 21:56:21 +00:00
parent 1b16bcf3ee
commit 6c68897935
5 changed files with 328 additions and 11 deletions
+20 -4
View File
@@ -1,6 +1,6 @@
# Ubuntu Based Kiosk
**Current Version:** 2.5.0 (check script header for latest version)
**Current Version:** 2.6.0 (check script header for latest version)
**Built with Claude Sonnet 4.6 AI assistance**
**License:** GPL v3 - Keep derivatives open source
**Repository:** https://github.com/outis1one/ubuntu-based-kiosk/
@@ -1209,6 +1209,9 @@ terminal menu and the web UI, so they can't drift apart).
reconfigure for network access, complete uninstall (purge). The first
Addon migrated — genuinely mutates real system state (apt packages,
`/etc/cups`, ufw) rather than this project's own files.
- `menus/addon_authelia.sh`**Authelia Auto-Login** (Addons):
encrypted SSO credentials plus the server-side setup instructions.
Prompted the `save_config` merge fix above.
- `install.sh` — entry point for the modular tool, now grouped **Core
Settings / Addons / Advanced** like the legacy menu. Run it against an
*already-installed* kiosk:
@@ -1221,7 +1224,7 @@ terminal menu and the web UI, so they can't drift apart).
**Honest status:** this does not yet replace first-time installation, or
most of the old installer. `ubuntu-based-kiosk.sh` is still ~12,000
lines and still contains its own unremoved, unmodified copies of every
menu above (plus Upgrade, Reinstall, Uninstall, 4 more Addons, and the
menu above (plus Upgrade, Reinstall, Uninstall, 3 more Addons, and the
other 8 Advanced items — none of that has moved yet). Both copies
coexist deliberately: the old ones stay until enough
of Core Settings/Addons/Advanced is migrated to
@@ -1230,13 +1233,26 @@ Migration continues one `menus/*.sh` file at a time; first-time
installation itself is the last and largest piece to move, if it moves
at all.
**Open question:** the config-clobbering bug fixed in `lib/config.sh`
(v2.6.0 — `save_config` silently deleting fields it doesn't know about,
like Authelia's credentials, on the next unrelated save) has the exact
same shape in `ubuntu-based-kiosk.sh`'s own `save_config`, unfixed. It's
a real bug in the currently-shipping single-file installer, independent
of whether the rest of that menu ever gets migrated. Worth deciding
separately whether to backport just that fix into the legacy script now
rather than waiting for a full migration pass.
---
## Project Status & Future Plans
**Current Version:** 2.5.0
**Current Version:** 2.6.0
**Recent Updates (v2.5.0):**
**Recent Updates (v2.6.0):**
- **Authelia Auto-Login migrated** — encrypted SSO credentials (same AES-256-CBC/scrypt algorithm `main.js` decrypts with, verified by a real encrypt→decrypt round trip in testing) plus the full server-side Docker setup instructions, viewable again later without reconfiguring.
- **Important bug found and fixed, not specific to Authelia:** `save_config()` did a full rebuild of `config.json` from known fields — exactly like the legacy script's `save_config` still does. Authelia's own write is a careful merge that preserves everything else, but the *next* save from Sites, Touch Controls, Navigation, or Password Protection would silently delete the Authelia credentials, since none of those knew the three Authelia fields existed. **This is a real bug in the currently-shipping single-file installer**, not introduced by this migration. Fixed in `lib/config.sh` by changing `save_config` to merge its known fields onto whatever's already on disk instead of rebuilding from nothing, so any untracked field — Authelia's three today, anything else tomorrow — survives automatically. The equivalent bug still exists, unfixed, in `ubuntu-based-kiosk.sh`'s own `save_config` — see "Modular Management" below.
**Previous (v2.5.0):**
- **First Addon migrated:** CUPS Printing — install/reconfigure/complete uninstall, in `./install.sh`. Genuinely mutates real system state (`apt install`/`remove --purge`, `/etc/cups`, `ufw`) at fixed paths CUPS itself doesn't let us relocate, so every test uses full command-level `sudo` stubbing rather than the scratch-directory approach used for this project's own files.
- **Menu restructured:** `install.sh`'s top level is now grouped Core Settings / Addons / Advanced, matching the legacy tool, instead of one flat list — done now while it's cheap, ahead of the list getting unwieldy.
- **Bug fix:** a "wait for service to start" retry loop used a bare `cmd1 && cmd2 && break` as its body — that's not made safe by being inside a loop; a bare `&&`/`||` list used as a standalone statement is fully subject to `set -e`, and the first command failing on an early iteration (near-certain right after a fresh install) would have killed the whole session. Restored the `if cmd1 && cmd2; then break; fi` form.
+6 -3
View File
@@ -16,7 +16,8 @@
# Core Settings: Sites & Page Timing, Display & Interaction, Timezone,
# Hidden Site PIN, Password Protection & Lockout, WiFi,
# Power/Display/Quiet Hours.
# Addons: CUPS Printing (menus/addon_cups.sh).
# Addons: CUPS Printing (menus/addon_cups.sh), Authelia Auto-Login
# (menus/addon_authelia.sh).
# Advanced: Diagnostics (menus/diagnostics.sh - system status/logs/
# audio/network).
#
@@ -52,6 +53,8 @@ source "$SCRIPT_DIR/menus/power_schedule.sh"
source "$SCRIPT_DIR/menus/diagnostics.sh"
# shellcheck source=menus/addon_cups.sh
source "$SCRIPT_DIR/menus/addon_cups.sh"
# shellcheck source=menus/addon_authelia.sh
source "$SCRIPT_DIR/menus/addon_authelia.sh"
################################################################################
# Preflight
@@ -115,8 +118,8 @@ core_settings_menu() {
}
addons_menu_builder() {
MENU_LABELS=("CUPS Printing")
MENU_HANDLERS=(addon_cups_menu)
MENU_LABELS=("CUPS Printing" "Authelia Auto-Login")
MENU_HANDLERS=(addon_cups_menu addon_authelia_menu)
}
addons_menu() {
+33 -2
View File
@@ -60,6 +60,9 @@ LOCKOUT_AT_TIME=""
LOCKOUT_ACTIVE_START=""
LOCKOUT_ACTIVE_END=""
REQUIRE_PASSWORD_ON_BOOT="false"
AUTHELIA_URL=""
AUTHELIA_USERNAME=""
AUTHELIA_ENCRYPTED_PASSWORD=""
kiosk_user_exists() {
id "$KIOSK_USER" &>/dev/null
@@ -125,6 +128,10 @@ load_existing_config() {
boot_password=$(sudo -u "$KIOSK_USER" jq -r '.requirePasswordOnBoot // false' "$CONFIG_PATH" 2>/dev/null)
[[ "$boot_password" == "true" ]] && REQUIRE_PASSWORD_ON_BOOT="true" || REQUIRE_PASSWORD_ON_BOOT="false"
AUTHELIA_URL=$(sudo -u "$KIOSK_USER" jq -r '.autheliaURL // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
AUTHELIA_USERNAME=$(sudo -u "$KIOSK_USER" jq -r '.autheliaUsername // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
AUTHELIA_ENCRYPTED_PASSWORD=$(sudo -u "$KIOSK_USER" jq -r '.autheliaEncryptedPassword // ""' "$CONFIG_PATH" 2>/dev/null || echo "")
}
# Write every bash global back out to config.json, then offer to reload the
@@ -159,7 +166,28 @@ save_config() {
local boot_password_json="false"
[[ "$REQUIRE_PASSWORD_ON_BOOT" == "true" ]] && boot_password_json="true"
jq -n \
# Merge onto whatever's already in config.json rather than rebuilding
# the file from nothing. The legacy save_config did a full `jq -n`
# rebuild listing every known field - any field it doesn't know about
# (e.g. Authelia's autheliaURL/autheliaUsername/
# autheliaEncryptedPassword, written by its own careful `. + {...}`
# merge) gets silently DELETED the next time any other menu that
# calls save_config runs. Real, currently-shipping bug in the legacy
# script, not unique to this migration - ported faithfully into this
# file's first version because no test happened to set an untracked
# field first. `. + {known fields...}` below preserves anything this
# tool doesn't track while still fully replacing every field it does
# (including tabs, via the same array-rebuild loop as before) - jq's
# `+` on objects takes the right-hand value for any key present on
# both sides, so a fully-specified `tabs` here still discards a
# deleted tab rather than merging old and new.
local existing="{}"
if sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then
existing=$(sudo -u "$KIOSK_USER" cat "$CONFIG_PATH" 2>/dev/null)
echo "$existing" | jq empty 2>/dev/null || existing="{}"
fi
echo "$existing" | jq \
--argjson autoswitch true \
--argjson enableTouch true \
--argjson dualSwipe "$dual_json" \
@@ -177,7 +205,10 @@ save_config() {
--arg lockoutActiveStart "${LOCKOUT_ACTIVE_START:-}" \
--arg lockoutActiveEnd "${LOCKOUT_ACTIVE_END:-}" \
--argjson requirePasswordOnBoot "$boot_password_json" \
'{autoswitch:$autoswitch,enableTouch:$enableTouch,dualSwipe:$dualSwipe,swipeMode:$swipeMode,allowNavigation:$allowNavigation,homeTabIndex:$homeTabIndex,inactivityTimeout:$inactivityTimeout,enablePauseButton:$enablePauseButton,enableKeyboardButton:$enableKeyboardButton,enableNavButton:$enableNavButton,enablePasswordProtection:$enablePasswordProtection,lockoutPassword:$lockoutPassword,lockoutTimeout:$lockoutTimeout,lockoutAtTime:$lockoutAtTime,lockoutActiveStart:$lockoutActiveStart,lockoutActiveEnd:$lockoutActiveEnd,requirePasswordOnBoot:$requirePasswordOnBoot,tabs:[]}' > "$tmp"
--arg autheliaURL "${AUTHELIA_URL:-}" \
--arg autheliaUsername "${AUTHELIA_USERNAME:-}" \
--arg autheliaEncryptedPassword "${AUTHELIA_ENCRYPTED_PASSWORD:-}" \
'. + {autoswitch:$autoswitch,enableTouch:$enableTouch,dualSwipe:$dualSwipe,swipeMode:$swipeMode,allowNavigation:$allowNavigation,homeTabIndex:$homeTabIndex,inactivityTimeout:$inactivityTimeout,enablePauseButton:$enablePauseButton,enableKeyboardButton:$enableKeyboardButton,enableNavButton:$enableNavButton,enablePasswordProtection:$enablePasswordProtection,lockoutPassword:$lockoutPassword,lockoutTimeout:$lockoutTimeout,lockoutAtTime:$lockoutAtTime,lockoutActiveStart:$lockoutActiveStart,lockoutActiveEnd:$lockoutActiveEnd,requirePasswordOnBoot:$requirePasswordOnBoot,autheliaURL:$autheliaURL,autheliaUsername:$autheliaUsername,autheliaEncryptedPassword:$autheliaEncryptedPassword,tabs:[]}' > "$tmp"
if [[ ${#URLS[@]} -gt 0 ]]; then
for idx in "${!URLS[@]}"; do
+232
View File
@@ -0,0 +1,232 @@
#!/bin/bash
################################################################################
# menus/addon_authelia.sh - "Authelia Auto-Login" addon.
#
# Stores encrypted Authelia SSO credentials so the kiosk authenticates
# automatically on every startup. The password is AES-256-CBC encrypted
# with a key derived from this machine's /etc/machine-id via scrypt -
# the encrypted blob is useless on any other machine - and is NEVER
# stored in plain text, matching the legacy addon exactly (same
# algorithm, same salt, same node crypto calls).
#
# autheliaURL/autheliaUsername/autheliaEncryptedPassword are tracked
# fields in lib/config.sh now (load_existing_config/save_config), same
# as every other config.json field this tool manages - this is also
# what motivated fixing save_config to merge onto the existing file
# instead of rebuilding it from scratch (see lib/config.sh): the legacy
# save_config had no idea these three fields existed, so configuring
# Authelia and then visiting Sites/Touch/Navigation/Password Protection
# in the legacy menu would silently wipe the credentials on the next
# save. Real bug in the shipped script, not unique to this migration.
#
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
################################################################################
addon_authelia_status() {
if [[ -n "$AUTHELIA_URL" ]]; then
echo "Authelia: configured"
echo " URL: $AUTHELIA_URL"
echo " Username: $AUTHELIA_USERNAME"
else
echo "Authelia: not configured"
fi
}
addon_authelia_menu_builder() {
if [[ -n "$AUTHELIA_URL" ]]; then
MENU_LABELS=("Reconfigure (overwrite)" "Show server-side setup instructions again" "Clear Authelia configuration")
MENU_HANDLERS=(action_configure_authelia action_show_authelia_server_setup action_clear_authelia)
else
MENU_LABELS=("Configure Authelia auto-login")
MENU_HANDLERS=(action_configure_authelia)
fi
}
addon_authelia_menu() {
load_existing_config
if ! sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then
log_error "config.json not found at $CONFIG_PATH - run a full install first"
pause
return
fi
run_menu "AUTHELIA AUTO-LOGIN" addon_authelia_menu_builder addon_authelia_status
}
################################################################################
# Encryption
################################################################################
# Same algorithm as the legacy addon: AES-256-CBC, key derived from
# /etc/machine-id via scrypt with a fixed salt, random IV prepended to
# the ciphertext, everything base64-encoded. main.js decrypts with the
# same derivation - do not change this without updating main.js too.
encrypt_authelia_password() {
local password="$1"
command -v node &>/dev/null || return 1
node -e "
const crypto=require('crypto'),fs=require('fs');
const id=fs.readFileSync('/etc/machine-id','utf8').trim();
const key=crypto.scryptSync(id,'kiosk-authelia-v1',32);
const iv=crypto.randomBytes(16);
const c=crypto.createCipheriv('aes-256-cbc',key,iv);
const enc=Buffer.concat([c.update(process.argv[1],'utf8'),c.final()]);
process.stdout.write(Buffer.concat([iv,enc]).toString('base64'));
" "$password" 2>/dev/null
}
################################################################################
# Actions
################################################################################
action_configure_authelia() {
echo
echo "Stores encrypted Authelia credentials so the kiosk"
echo "authenticates automatically on every startup."
echo "Password is encrypted with this machine's unique ID -"
echo "the encrypted blob is useless on any other machine."
echo
if [[ -n "$AUTHELIA_URL" ]]; then
echo "Current config:"
echo " URL: $AUTHELIA_URL"
echo " Username: $AUTHELIA_USERNAME"
echo
ask_yes_no "Overwrite existing Authelia config?" "n" || { echo "Cancelled"; return; }
echo
fi
local url user pass
read -r -p "Authelia URL (e.g. https://auth.yourdomain.com): " url
[[ -z "$url" ]] && { echo "Cancelled"; return; }
read -r -p "Authelia username: " user
[[ -z "$user" ]] && { echo "Cancelled"; return; }
read -r -s -p "Authelia password: " pass
echo
[[ -z "$pass" ]] && { echo "Cancelled"; return; }
echo "Encrypting with machine ID..."
local encrypted
encrypted=$(encrypt_authelia_password "$pass")
if [[ -z "$encrypted" ]]; then
log_error "Encryption failed - is Node.js installed?"
return 1
fi
AUTHELIA_URL="$url"
AUTHELIA_USERNAME="$user"
AUTHELIA_ENCRYPTED_PASSWORD="$encrypted"
save_config
log_success "Authelia config saved (password encrypted, NOT stored in plain text)"
action_show_authelia_server_setup
echo
if is_service_active lightdm && ask_yes_no "Restart kiosk display now?" "n"; then
sudo systemctl restart lightdm
fi
}
action_clear_authelia() {
echo
ask_yes_no "Clear Authelia configuration?" "n" || { echo "Cancelled"; return; }
AUTHELIA_URL=""
AUTHELIA_USERNAME=""
AUTHELIA_ENCRYPTED_PASSWORD=""
save_config
log_success "Authelia configuration cleared"
}
action_show_authelia_server_setup() {
echo
echo "════════════════════════════════════════════════════════════"
echo " AUTHELIA SERVER-SIDE SETUP (Dockerized)"
echo "════════════════════════════════════════════════════════════"
echo
echo "1. Generate the argon2 password hash on your Docker host:"
echo
echo " docker run --rm authelia/authelia:latest \\"
echo " authelia crypto hash generate argon2 \\"
echo " --password 'yourpassword'"
echo
echo " Copy the \$argon2id\$... output — that is your hash."
echo
echo "2. ADD a kiosk user to ~/docker/authelia/config/users.yml"
echo " (append — do not replace existing users):"
echo
echo " kiosk:"
echo " displayname: \"Kiosk Display\""
echo " password: '\$argon2id\$v=19\$m=65536,t=3,p=4\$<paste hash here>'"
echo " email: kiosk@local.com"
echo " groups:"
echo " - kiosk"
echo
echo "3. MERGE into ~/docker/authelia/config/configuration.yml:"
echo
echo " ── access_control ─────────────────────────────────────"
echo " Find your EXISTING access_control block and add the"
echo " kiosk rule as the FIRST rule inside it."
echo
echo " !! DO NOT create a second access_control: block !!"
echo " YAML silently ignores duplicate keys — the kiosk rule"
echo " will be invisible to Authelia and you will get a white"
echo " screen on the kiosk."
echo
echo " Authelia reads rules top-down, first match wins."
echo " The kiosk rule MUST be above any two_factor rule or"
echo " the two_factor wildcard will match first."
echo
echo " ── EXAMPLE — before (your existing config): ──────────"
echo " access_control:"
echo " default_policy: deny"
echo " rules:"
echo " - domain: '*.yourdomain.com'"
echo " policy: two_factor"
echo
echo " ── EXAMPLE — after (add kiosk rule above two_factor): ─"
echo " access_control:"
echo " default_policy: deny"
echo " rules:"
echo " - domain: '*.yourdomain.com' # <-- kiosk first"
echo " subject: 'group:kiosk'"
echo " policy: one_factor"
echo " - domain: '*.yourdomain.com' # <-- existing"
echo " policy: two_factor"
echo
echo " Why one_factor? The kiosk authenticates via the API"
echo " (/api/firstfactor — password only). TOTP and WebAuthn"
echo " require a second interactive step that is impossible"
echo " from a script, so the kiosk group must use one_factor."
echo
echo " ── session ─────────────────────────────────────────────"
echo " Keep your existing session block — no changes needed."
echo " The kiosk re-authenticates via API on every startup so"
echo " session expiry barely matters for it."
echo
echo " If you do NOT yet have a session block, add:"
echo
echo " session:"
echo " expiration: 8h"
echo " inactivity: 1h"
echo " remember_me: 7d"
echo " cookies:"
echo " - domain: yourdomain.com"
echo " authelia_url: https://auth.yourdomain.com"
echo
echo "4. Restart Authelia on your Docker host:"
echo " docker compose restart authelia"
echo
echo "────────────────────────────────────────────────────────────"
echo " NOTE: HTTP Basic Auth (per-site username/password) still"
echo " works alongside Authelia for sites that use browser-popup"
echo " authentication rather than Authelia SSO."
echo "────────────────────────────────────────────────────────────"
echo
echo " To clear Authelia config later, use this menu's"
echo " 'Clear Authelia configuration' option."
echo
pause
}
+37 -2
View File
@@ -1,8 +1,43 @@
#!/bin/bash
################################################################################
### Ubuntu Based Kiosk v2.5.0 ###
### Ubuntu Based Kiosk v2.6.0 ###
################################################################################
#
# RELEASE v2.6.0 - Authelia Migrated; Real Config-Clobbering Bug Fixed
# - New in ./install.sh: Authelia Auto-Login (menus/addon_authelia.sh) -
# encrypted SSO credentials (AES-256-CBC, key derived from this
# machine's /etc/machine-id via scrypt - same algorithm main.js
# decrypts with, verified by test with a real round-trip encrypt/
# decrypt, not just "some string came out"), plus the full Dockerized
# server-side setup instructions, viewable again later without
# reconfiguring.
# - IMPORTANT bug found and fixed in lib/config.sh, NOT specific to
# Authelia or to this migration: save_config() did a full `jq -n`
# rebuild of config.json from known fields, exactly like the legacy
# script's save_config still does. Authelia's own write is a careful
# `. + {...}` merge that preserves everything - but the legacy
# configure_authelia() writes autheliaURL/autheliaUsername/
# autheliaEncryptedPassword into config.json via that merge, and
# *neither* the legacy save_config nor this project's own (before this
# fix) had any idea those three fields existed. The next time a user
# visited Sites, Touch Controls, Navigation, or Password Protection -
# all of which call save_config - their Authelia credentials were
# silently deleted. This is a real bug in the currently-shipping
# single-file installer, not introduced by this migration; ported
# faithfully into lib/config.sh's first version because no test
# happened to set an untracked field before calling save_config.
# Fixed here by changing save_config to merge its known fields onto
# whatever's already in config.json (jq `. + {...}`) instead of
# rebuilding the file from nothing, so any field this tool doesn't
# track - Authelia's three today, anything else tomorrow - survives
# automatically. autheliaURL/autheliaUsername/autheliaEncryptedPassword
# are also now tracked fields in their own right, same as every other
# config.json field this tool manages. NOTE: the equivalent bug still
# exists in this script's own save_config below, unfixed - see
# Readme.md ("Modular Management") for the open question of whether to
# backport this specific fix here independent of the wider migration,
# given it's a real, currently-shipping credential-loss bug.
#
# RELEASE v2.5.0 - First Addon Migrated (CUPS), Menu Restructured
# - New in ./install.sh: CUPS Printing (menus/addon_cups.sh) - the first
# Addon migrated. Install/reconfigure/complete uninstall (purge),
@@ -226,7 +261,7 @@ set -euo pipefail
### SECTION 1: CONSTANTS & GLOBALS
################################################################################
SCRIPT_VERSION="2.5.0"
SCRIPT_VERSION="2.6.0"
# Resolve the real path to this script file.
# When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,