#!/usr/bin/env bash # ============================================================================ # vendor.sh — put MediaPipe (eye gaze) on the app's own site. # # third_party/mediapipe/vendor.sh [cache-dir] # # Writes /vendor/mediapipe--/ (and removes any # other vendor/mediapipe-* there): @mediapipe/tasks-vision from npm, the face # landmarker model, and this directory's LICENSE.txt and NOTICE.txt. The app # loads it from there (src/bindings/mediapipe_init.js), so turning on eye gaze # makes no third-party request, keeps the app cross-origin isolated, and works # offline once used. # # The directory name comes from MEDIAPIPE_VERSION and MEDIAPIPE_VENDOR_REV in # crates/aac-web/src/eye_tracking.rs. Servers and the service workers cache it # as immutable, so its bytes are pinned below (VENDOR_DIGEST): any change to # what is served there fails the build until the revision is bumped, which # gives the changed files a new URL. # # Used by prerender-static.sh (aac.transaction.science) and by the aurasense # host's scripts/build-app.sh. Idempotent; downloads are cached in cache-dir # (default ~/.cache/aac-rs). # ============================================================================ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AAC="$(cd "$HERE/../.." && pwd)" SITE="${1:?usage: vendor.sh [cache-dir]}" CACHE="${2:-${XDG_CACHE_HOME:-$HOME/.cache}/aac-rs}" ET="$AAC/crates/aac-web/src/eye_tracking.rs" MP_VERSION="$(sed -n 's/^pub const MEDIAPIPE_VERSION: &str = "\([^"]*\)";$/\1/p' "$ET")" MP_REV="$(sed -n 's/^pub const MEDIAPIPE_VENDOR_REV: &str = "\([^"]*\)";$/\1/p' "$ET")" [ -n "$MP_VERSION" ] || { echo "✗ MEDIAPIPE_VERSION not found in $ET" >&2; exit 1; } [ -n "$MP_REV" ] || { echo "✗ MEDIAPIPE_VENDOR_REV not found in $ET" >&2; exit 1; } DIR="mediapipe-$MP_VERSION-$MP_REV" # What vendor/$VENDOR_PINNED holds, as a sha256 over its sorted file list and # each file's sha256. Re-pin only together with a new MEDIAPIPE_VENDOR_REV. VENDOR_PINNED="mediapipe-1.0.1-aac1" VENDOR_DIGEST="17805cc3670feb85dc0a555a21a04097f0663bcca5ad2a24454dfccce0b08910" MP_MODEL_URL="https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task" MP_MODEL_SHA256="64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff" [ "$DIR" = "$VENDOR_PINNED" ] || { echo "✗ eye_tracking.rs asks for $DIR, but vendor.sh is pinned to $VENDOR_PINNED — vendor it, check it, and re-pin VENDOR_PINNED and VENDOR_DIGEST" >&2 exit 1 } sha256() { shasum -a 256 "$1" | cut -c1-64; } echo "▸ vendoring MediaPipe tasks-vision $MP_VERSION ($DIR)" mkdir -p "$CACHE" # npm package: unpacked beside the cache entry, then moved in whole, so an # interrupted run never leaves half a package that looks complete. MP_PKG="$CACHE/tasks-vision-$MP_VERSION/package" if [ ! -f "$MP_PKG/package.json" ]; then TMP="$(mktemp -d "$CACHE/tasks-vision.XXXXXX")" (cd "$TMP" && npm pack --silent "@mediapipe/tasks-vision@$MP_VERSION" >/dev/null && tar xzf ./*.tgz) rm -rf "$CACHE/tasks-vision-$MP_VERSION" mv "$TMP" "$CACHE/tasks-vision-$MP_VERSION" fi grep -q "\"version\": \"$MP_VERSION\"" "$MP_PKG/package.json" || { echo "✗ cached tasks-vision is not $MP_VERSION" >&2; exit 1; } # Model: downloaded to a temporary name and moved in only once its hash # matches; a cached copy that does not match is fetched again. MODEL="$CACHE/face_landmarker.task" if [ ! -f "$MODEL" ] || [ "$(sha256 "$MODEL")" != "$MP_MODEL_SHA256" ]; then rm -f "$MODEL" curl -fsSL "$MP_MODEL_URL" -o "$MODEL.tmp" [ "$(sha256 "$MODEL.tmp")" = "$MP_MODEL_SHA256" ] \ || { rm -f "$MODEL.tmp"; echo "✗ face_landmarker.task does not match its pinned sha256" >&2; exit 1; } mv "$MODEL.tmp" "$MODEL" fi rm -rf "$SITE"/vendor/mediapipe-* MP="$SITE/vendor/$DIR" mkdir -p "$MP" cp "$MP_PKG/vision_bundle.mjs" "$MP/" cp -R "$MP_PKG/wasm" "$MP/wasm" cp "$MODEL" "$MP/" # The npm package ships no licence text; Apache-2.0 §4 asks for one, and a # notice of what we changed. cp "$HERE/LICENSE.txt" "$HERE/NOTICE.txt" "$MP/" # The library reports usage metrics to Google (odml.pa.googleapis.com) every # minute, with no option to turn it off (https://goo.gle/mediapipe-privacy, # read 2026-09-29). Make its one sender drop the queue instead of posting it, # and say so at the top of the file (Apache-2.0 §4(b)). MP_SEND='flush(t,e){if(this.error)e?.("net-send-failed")' MP_DROP='flush(t,e){this.h=[];return void t?.();if(this.error)e?.("net-send-failed")' [ "$(grep -oF "$MP_SEND" "$MP/vision_bundle.mjs" | wc -l | tr -d ' ')" = 1 ] \ || { echo "✗ MediaPipe's metrics sender has changed — re-check the telemetry patch for $MP_VERSION" >&2; exit 1; } { echo "// Changed by the AAC build: usage-metrics upload disabled (Logger.flush drops its queue). See NOTICE.txt." sed 's/flush(t,e){if(this.error)e?.("net-send-failed")/flush(t,e){this.h=[];return void t?.();if(this.error)e?.("net-send-failed")/' "$MP/vision_bundle.mjs" } > "$MP/vision_bundle.mjs.tmp" && mv "$MP/vision_bundle.mjs.tmp" "$MP/vision_bundle.mjs" grep -qF "$MP_DROP" "$MP/vision_bundle.mjs" || { echo "✗ telemetry patch did not apply" >&2; exit 1; } if grep -qF "$MP_SEND" "$MP/vision_bundle.mjs"; then echo "✗ unpatched metrics sender still present" >&2; exit 1; fi for f in vision_bundle.mjs wasm/vision_wasm_internal.js wasm/vision_wasm_internal.wasm face_landmarker.task LICENSE.txt NOTICE.txt; do [ -s "$MP/$f" ] || { echo "✗ vendored MediaPipe is missing $f" >&2; exit 1; } done DIGEST="$(cd "$MP" && find . -type f | LC_ALL=C sort | while IFS= read -r f; do printf '%s %s\n' "$f" "$(sha256 "$f")"; done | shasum -a 256 | cut -c1-64)" [ "$DIGEST" = "$VENDOR_DIGEST" ] || { echo "✗ vendor/$DIR differs from what was pinned (got $DIGEST)." >&2 echo " Browsers cache that URL as immutable: bump MEDIAPIPE_VENDOR_REV in eye_tracking.rs," >&2 echo " then set VENDOR_PINNED and VENDOR_DIGEST in $HERE/vendor.sh." >&2 exit 1 } echo "✓ vendor/$DIR ($(du -sh "$MP" | cut -f1))"