#!/usr/bin/env bash # # Refresh local copies of external dependencies that we cache in the repo # (allowlists, public key bundles, etc.) so the mobile/browser/server apps # do not need to fetch them at runtime. # # Run as part of the manual release pipeline. # # Usage: # ./refresh-external-dependencies.sh # run every task # ./refresh-external-dependencies.sh ... # run only the named task(s) # ./refresh-external-dependencies.sh --list # show available tasks # # Adding a new task: # 1. Define a function named `task_` that fetches the upstream payload # and writes the result into the relevant source file. Use the # `replace_generated_block` helper to swap content between # `// BEGIN_GENERATED: ` and `// END_GENERATED: ` markers (any # comment syntax works — the helper matches both line- and block-style). # 2. Register the task in TASK_ORDER + TASK_DESCRIPTIONS below. set -euo pipefail if [ -z "${BASH_VERSION:-}" ]; then echo "Error: this script must be run with bash" >&2 exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" TODAY="$(date +%Y-%m-%d)" # Colors BLUE='\033[0;34m' GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' RESET='\033[0m' WORK_DIR="$(mktemp -d)" trap 'rm -rf "$WORK_DIR"' EXIT # ---------------------------------------------------------------------- # Helpers shared by all tasks # ---------------------------------------------------------------------- # Replace everything between `BEGIN_GENERATED: ` and `END_GENERATED: ` # markers in $1 with the contents of the file at $3. The marker lines # themselves are preserved; only the lines between them change. # # Arguments: # $1 target file # $2 block id (must match the marker) # $3 path to file whose contents become the new block body replace_generated_block() { local target_file="$1" local block_id="$2" local payload_file="$3" python3 - "$target_file" "$block_id" "$payload_file" <<'PY' import re import sys target, block_id, payload_path = sys.argv[1:4] with open(target, "r") as f: src = f.read() with open(payload_path, "r") as f: payload = f.read().rstrip("\n") pattern = re.compile( r"(BEGIN_GENERATED:\s*" + re.escape(block_id) + r".*?\n)" r"(.*?)" r"(^[ \t]*\S+\s*END_GENERATED:\s*" + re.escape(block_id) + r")", re.DOTALL | re.MULTILINE, ) match = pattern.search(src) if not match: sys.stderr.write( f"ERROR: BEGIN_GENERATED/END_GENERATED markers for '{block_id}' " f"not found in {target}\n" ) sys.exit(1) new_src = src[:match.start()] + match.group(1) + payload + "\n" + match.group(3) + src[match.end():] with open(target, "w") as f: f.write(new_src) PY } # Download a URL into a file, failing loudly on HTTP errors. download() { local url="$1" local out="$2" if ! curl --fail --silent --show-error --location "$url" -o "$out"; then echo -e " ${RED}✗${RESET} download failed: $url" >&2 return 1 fi } # Validate that a file contains parseable JSON. ensure_valid_json() { local file="$1" if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$file" 2>/dev/null; then echo -e " ${RED}✗${RESET} downloaded content is not valid JSON" >&2 return 1 fi } # ---------------------------------------------------------------------- # Tasks — one function per external dependency # ---------------------------------------------------------------------- task_passkeys_allowlist() { local url="https://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json" local target="$REPO_ROOT/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/OriginVerifier.kt" local block_id="passkeys-allowlist" echo -e " ${BLUE}↓${RESET} fetching $url" local raw="$WORK_DIR/passkeys-allowlist.json" download "$url" "$raw" ensure_valid_json "$raw" # Build the replacement block. The JSON lines are embedded inside a # Kotlin triple-quoted string with `.trimIndent()`, so we prefix every # line with 8 spaces of indent — trimIndent strips that back out at # runtime, leaving the upstream bytes intact. local block="$WORK_DIR/passkeys-allowlist.block" { echo " // Source: $url" echo " // Last refreshed: $TODAY" echo ' private val PRIVILEGED_ALLOWLIST_JSON = """' awk '{ if (length($0) > 0) print " " $0; else print "" }' "$raw" echo ' """.trimIndent()' } > "$block" replace_generated_block "$target" "$block_id" "$block" echo -e " ${GREEN}✓${RESET} updated ${target#$REPO_ROOT/}" } # ---------------------------------------------------------------------- # Registry # ---------------------------------------------------------------------- # Each entry is "task-name:human description". The order here is the order # tasks run when no specific task argument is given. TASKS=( "passkeys-allowlist:Android privileged-apps allowlist for WebAuthn (gstatic.com/gpm-passkeys-privileged-apps/apps.json)" ) # ---------------------------------------------------------------------- # Dispatch # ---------------------------------------------------------------------- task_description() { local needle="$1" local entry for entry in "${TASKS[@]}"; do if [ "${entry%%:*}" = "$needle" ]; then echo "${entry#*:}" return 0 fi done return 1 } list_tasks() { echo "Available tasks:" local entry name desc for entry in "${TASKS[@]}"; do name="${entry%%:*}" desc="${entry#*:}" printf " %-24s %s\n" "$name" "$desc" done } run_task() { local name="$1" local fn="task_${name//-/_}" if ! declare -F "$fn" > /dev/null; then echo -e "${RED}Unknown task: $name${RESET}" >&2 echo >&2 list_tasks >&2 return 1 fi echo -e "${YELLOW}→${RESET} $name" "$fn" } usage() { cat <