feat(webui/registry): catalog scan generator + hotfix-only Improvements stream
webuiRegistryCatalogScan (run by updater check, same atomic keep-prior pattern as webuiArtifactScan) writes apps/generated/registry_catalog.json: the type:"app"/kind:"bundle" rows of the signed index annotated with defined/installed, browse metadata from the envelope meta, and icons mirrored into core/icons/apps/registry/ ONLY when their bytes match the sha256 pin in the signed index — the browser stays same-origin; a tampered or oversized icon is skipped, never served. webuiArtifactScan now selects type=="hotfix" so app rows never render as pseudo-hotfixes in the Improvements tab, and counts+logs artifacts of unrecognized type instead of surfacing them (the §8.1 forward-compat firewall on the scan path). Harness vs a locally served registry: 14/14 (catalog row + meta + flags, icon pin verify + tamper skip, hotfix-only stream, unknown-type skip+log, unreachable-registry keeps prior files). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
d1958f6033
commit
87edd09994
@ -52,6 +52,12 @@ cliHandleUpdaterCommands()
|
||||
source "$install_scripts_dir/webui/data/generators/updater/webui_artifact_scan.sh" 2>/dev/null
|
||||
fi
|
||||
declare -F webuiArtifactScan >/dev/null 2>&1 && webuiArtifactScan
|
||||
# Registry catalog: refresh the App Center's marketplace data
|
||||
# (the type:"app" rows of the same signed index).
|
||||
if ! declare -F webuiRegistryCatalogScan >/dev/null 2>&1; then
|
||||
source "$install_scripts_dir/webui/data/generators/apps/webui_registry_scan.sh" 2>/dev/null
|
||||
fi
|
||||
declare -F webuiRegistryCatalogScan >/dev/null 2>&1 && webuiRegistryCatalogScan
|
||||
if ! declare -F artifactApplyAuto >/dev/null 2>&1; then
|
||||
source "$install_scripts_dir/cli/commands/artifact/cli_artifact_apply.sh" 2>/dev/null
|
||||
fi
|
||||
|
||||
@ -8,6 +8,7 @@ webui_scripts=(
|
||||
"webui/data/generators/apps/webui_app_status.sh"
|
||||
"webui/data/generators/apps/webui_config_patch.sh"
|
||||
"webui/data/generators/apps/webui_config.sh"
|
||||
"webui/data/generators/apps/webui_registry_scan.sh"
|
||||
"webui/data/generators/apps/webui_services.sh"
|
||||
"webui/data/generators/apps/webui_tools.sh"
|
||||
"webui/data/generators/backup/webui_backup_app_status.sh"
|
||||
|
||||
@ -943,6 +943,7 @@ declare -gA LP_FN_MAP=(
|
||||
[webuiPrintInstallCard]="webui/webui_display_logins.sh"
|
||||
[webuiPrintLoginBlock]="webui/webui_display_logins.sh"
|
||||
[_webuiReadServiceTags]="webui/data/generators/apps/webui_config.sh"
|
||||
[webuiRegistryCatalogScan]="webui/data/generators/apps/webui_registry_scan.sh"
|
||||
[webuiRemoveSetupLock]="webui/data/lock/webui_remove_setup_lock.sh"
|
||||
[webuiRemoveUpdateLock]="webui/data/lock/webui_remove_update_lock.sh"
|
||||
[webuiRunUpdate]="update/check_update.sh"
|
||||
@ -1908,6 +1909,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[webuiPrintInstallCard]="scripts"
|
||||
[webuiPrintLoginBlock]="scripts"
|
||||
[_webuiReadServiceTags]="scripts"
|
||||
[webuiRegistryCatalogScan]="scripts"
|
||||
[webuiRemoveSetupLock]="scripts"
|
||||
[webuiRemoveUpdateLock]="scripts"
|
||||
[webuiRunUpdate]="scripts"
|
||||
@ -2894,6 +2896,7 @@ webuiPatchAppConfigJson() { source "${install_scripts_dir}webui/data/generators/
|
||||
webuiPrintInstallCard() { source "${install_scripts_dir}webui/webui_display_logins.sh"; webuiPrintInstallCard "$@"; }
|
||||
webuiPrintLoginBlock() { source "${install_scripts_dir}webui/webui_display_logins.sh"; webuiPrintLoginBlock "$@"; }
|
||||
_webuiReadServiceTags() { source "${install_scripts_dir}webui/data/generators/apps/webui_config.sh"; _webuiReadServiceTags "$@"; }
|
||||
webuiRegistryCatalogScan() { source "${install_scripts_dir}webui/data/generators/apps/webui_registry_scan.sh"; webuiRegistryCatalogScan "$@"; }
|
||||
webuiRemoveSetupLock() { source "${install_scripts_dir}webui/data/lock/webui_remove_setup_lock.sh"; webuiRemoveSetupLock "$@"; }
|
||||
webuiRemoveUpdateLock() { source "${install_scripts_dir}webui/data/lock/webui_remove_update_lock.sh"; webuiRemoveUpdateLock "$@"; }
|
||||
webuiRunUpdate() { source "${install_scripts_dir}update/check_update.sh"; webuiRunUpdate "$@"; }
|
||||
|
||||
124
scripts/webui/data/generators/apps/webui_registry_scan.sh
Normal file
124
scripts/webui/data/generators/apps/webui_registry_scan.sh
Normal file
@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WebUI registry-catalog data generator (the marketplace browse data)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Writes the read-only JSON the App Center merges into its grid as
|
||||
# "Available — Add" cards:
|
||||
# frontend/data/apps/generated/registry_catalog.json
|
||||
#
|
||||
# It fetches + verifies the signed artifact index (lpFetchIndexInto), selects
|
||||
# the type:"app" / payload.kind:"bundle" envelopes, annotates each with
|
||||
# `defined` (a definition exists in the install tree) and `installed` (a live
|
||||
# dir exists), and mirrors each catalog icon into
|
||||
# frontend/core/icons/apps/registry/<slug>.<ext>
|
||||
# only when it matches the sha256 pin in the signed index — the browser is
|
||||
# never pointed at a remote host (same-origin only). Written ATOMICALLY
|
||||
# (temp -> validate -> one runFileWrite); on ANY fetch/verify failure the
|
||||
# prior file is KEPT. Run by `libreportal updater check`.
|
||||
|
||||
webuiRegistryCatalogScan() {
|
||||
local out_dir="${containers_dir%/}/libreportal/frontend/data/apps/generated"
|
||||
local out="$out_dir/registry_catalog.json"
|
||||
local icon_dir="${containers_dir%/}/libreportal/frontend/core/icons/apps/registry"
|
||||
local icon_web="/core/icons/apps/registry"
|
||||
local max_icon=262144
|
||||
runFileOp mkdir -p "$out_dir" 2>/dev/null || true
|
||||
|
||||
# Lazy-loader gap: ensure the read primitives are present.
|
||||
if ! declare -F lpFetchIndex >/dev/null 2>&1; then
|
||||
source "$install_scripts_dir/source/fetch.sh" 2>/dev/null
|
||||
source "$install_scripts_dir/source/artifacts.sh" 2>/dev/null
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
isNotice "webuiRegistryCatalogScan: jq not available — keeping the prior registry_catalog.json."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local index
|
||||
if ! lpFetchIndexInto index; then
|
||||
isNotice "webuiRegistryCatalogScan: no verified index available — keeping the prior file."
|
||||
return 0
|
||||
fi
|
||||
local signed="false"; [[ "$LP_INDEX_SIGSTATE" == "verified" ]] && signed="true"
|
||||
local serial now
|
||||
serial="$(_lpJsonNum "$index" index_serial)"
|
||||
now="$(date -Iseconds 2>/dev/null || date)"
|
||||
|
||||
# Defined apps = definition dirs in the install tree; installed = live dirs.
|
||||
local defined="[]" installed="[]" d
|
||||
if [[ -d "$install_containers_dir" ]]; then
|
||||
defined="$(for d in "$install_containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)"
|
||||
fi
|
||||
if [[ -d "$containers_dir" ]]; then
|
||||
installed="$(for d in "$containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)"
|
||||
fi
|
||||
|
||||
# Mirror the pinned catalog icons locally (slug -> local web path). An icon
|
||||
# is used only if its bytes match the sha256 the signed index pins — a
|
||||
# tampered or oversized icon is simply skipped, never served.
|
||||
local icons_map="{}" base rows
|
||||
base="$(lpReleaseBaseUrl)"
|
||||
rows="$(printf '%s' "$index" | jq -r '
|
||||
.artifacts[]? | select(.type=="app" and .payload.kind=="bundle")
|
||||
| select((.meta.icon // "") != "" and (.meta.icon_sha256 // "") != "")
|
||||
| [(.applies_when.app // ""), .meta.icon, .meta.icon_sha256] | @tsv' 2>/dev/null)"
|
||||
if [[ -n "$rows" ]]; then
|
||||
runFileOp mkdir -p "$icon_dir" 2>/dev/null || true
|
||||
local slug icon pin ext url tmpf dest got size
|
||||
while IFS=$'\t' read -r slug icon pin; do
|
||||
[[ "$slug" =~ ^[a-z0-9][a-z0-9_]{0,31}$ ]] || continue
|
||||
ext="${icon##*.}"
|
||||
case "$ext" in svg|png) : ;; *) continue ;; esac
|
||||
dest="$icon_dir/$slug.$ext"
|
||||
if [[ -f "$dest" && "$(_lpSha256 "$dest")" == "$pin" ]]; then
|
||||
icons_map="$(jq -c --arg s "$slug" --arg p "$icon_web/$slug.$ext" '.[$s]=$p' <<<"$icons_map")"
|
||||
continue
|
||||
fi
|
||||
url="$icon"; case "$url" in http*://*) : ;; *) url="$base/$url" ;; esac
|
||||
tmpf="$(mktemp)"
|
||||
if _lpDownload "$url" "$tmpf" 2>/dev/null; then
|
||||
got="$(_lpSha256 "$tmpf")"
|
||||
size="$(stat -c %s "$tmpf" 2>/dev/null || echo 0)"
|
||||
if [[ "$got" == "$pin" ]] && (( size > 0 && size <= max_icon )); then
|
||||
runFileWrite "$dest" < "$tmpf"
|
||||
runFileOp chown "$docker_install_user":"$docker_install_user" "$dest" 2>/dev/null || true
|
||||
icons_map="$(jq -c --arg s "$slug" --arg p "$icon_web/$slug.$ext" '.[$s]=$p' <<<"$icons_map")"
|
||||
fi
|
||||
fi
|
||||
rm -f "$tmpf"
|
||||
done <<<"$rows"
|
||||
fi
|
||||
|
||||
local tmp; tmp="$(mktemp)"
|
||||
printf '%s' "$index" | jq \
|
||||
--arg now "$now" --arg signed "$signed" --arg serial "${serial:-0}" \
|
||||
--argjson defined "$defined" --argjson installed "$installed" --argjson icons "$icons_map" '
|
||||
{ generated_at: $now,
|
||||
signed: ($signed=="true"),
|
||||
serial: ($serial|tonumber? // 0),
|
||||
apps: [ .artifacts[]? | select(.type=="app" and .payload.kind=="bundle")
|
||||
| (.applies_when.app // "") as $slug | select($slug != "")
|
||||
| { id,
|
||||
app: $slug,
|
||||
version: (.version // 1),
|
||||
title: (.title // $slug),
|
||||
why: (.why // ""),
|
||||
publisher: (.publisher // ""),
|
||||
trust: (.trust // "official"),
|
||||
category: (.meta.category // ""),
|
||||
description: (.meta.description // .why // ""),
|
||||
long_description: (.meta.long_description // ""),
|
||||
icon: ($icons[$slug] // null),
|
||||
defined: (($defined | index($slug)) != null),
|
||||
installed: (($installed | index($slug)) != null) } ] }' > "$tmp" 2>/dev/null
|
||||
|
||||
if ! jq empty "$tmp" 2>/dev/null; then
|
||||
isNotice "webuiRegistryCatalogScan: generated JSON was invalid — keeping the prior file."
|
||||
rm -f "$tmp"; return 1
|
||||
fi
|
||||
runFileWrite "$out" < "$tmp"
|
||||
rm -f "$tmp"
|
||||
runFileOp chown "$docker_install_user":"$docker_install_user" "$out" 2>/dev/null || true
|
||||
local n; n="$(jq '.apps | length' "$out" 2>/dev/null || echo '?')"
|
||||
isSuccessful "Registry catalog refreshed ($n app(s), serial=${serial:-?}, signed=$signed)."
|
||||
}
|
||||
@ -49,6 +49,13 @@ webuiArtifactScan() {
|
||||
installed="$(for d in "$containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)"
|
||||
fi
|
||||
|
||||
# The Improvements stream is HOTFIX-only. type:"app" rows belong to the
|
||||
# registry catalog (webuiRegistryCatalogScan); anything else is a newer
|
||||
# format this build doesn't know — skip + log, never error (§8.1 firewall).
|
||||
local unknown
|
||||
unknown="$(printf '%s' "$index" | jq '[.artifacts[]? | (.type // "") | select(. != "hotfix" and . != "app")] | length' 2>/dev/null || echo 0)"
|
||||
[[ "$unknown" =~ ^[0-9]+$ ]] && (( unknown > 0 )) && isNotice "webuiArtifactScan: skipped $unknown artifact(s) of unrecognized type (newer format?)."
|
||||
|
||||
local tmp; tmp="$(mktemp)"
|
||||
printf '%s' "$index" | jq \
|
||||
--arg now "$now" --arg signed "$signed" --arg serial "${serial:-0}" \
|
||||
@ -56,7 +63,7 @@ webuiArtifactScan() {
|
||||
{ generated_at: $now,
|
||||
signed: ($signed=="true"),
|
||||
serial: ($serial|tonumber? // 0),
|
||||
artifacts: [ .artifacts[]? | {
|
||||
artifacts: [ .artifacts[]? | select(.type=="hotfix") | {
|
||||
id, type,
|
||||
severity: (.severity // "tweak"),
|
||||
auto: (.auto // false),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user