#!/usr/bin/env bash # ============================================================================ # vendor.sh — put the listener's runtime (Microsoft's onnxruntime-web) on the # app's own site. # # third_party/onnxruntime-web/vendor.sh [cache-dir] # # Writes /vendor/onnxruntime-web--/ (and removes # any other vendor/onnxruntime-web-* there): # asr-worker.js this directory's worker.js, unchanged # ort.jspi.bundle.min.mjs and ort-wasm-simd-threaded.jspi.wasm # the WebGPU runtime for browsers with JavaScript # Promise Integration (WebAssembly.Suspending), from # the npm package, unchanged # LICENSE.txt, NOTICE.txt this directory's licence and notice # ThirdPartyNotices.txt ONNX Runtime's notices for the code it includes, # at the same release, fetched and pinned # # Only this build: the listener is offered only where JSPI is (it reads the # model one tensor at a time; without JSPI the runtime would hold the whole # model in memory first). The package's other builds (asyncify, the # processor-only WebAssembly one, WebGL, JSEP) are not shipped. # # The app never imports these URLs directly. When someone downloads the # listener, src/listen.rs stores the worker and the one build their browser # runs in the model store (SHA-256 checked, kept across app updates), and # src/bindings/listen.js runs them from there, in a worker, offline. # # The version and revision come from ORT_WEB_VERSION and ORT_WEB_VENDOR_REV # in crates/aac-web/src/listen.rs, and each runtime file's size and SHA-256 # from RUNTIME_FILES there. This script fails loudly on any drift: another # version asked for than pinned here, an npm tarball that is not the pinned # one, a vendored file that differs from the Rust pin, or a directory whose # bytes differ from VENDOR_DIGEST. Changed bytes need a new revision, because # browsers and the model store treat the directory's URLs as immutable. # # Idempotent; the download is cached in cache-dir (default ~/.cache/aac-rs). # Needs node (npm). # ============================================================================ 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}" LS="$AAC/crates/aac-web/src/listen.rs" ORT_VERSION="$(sed -n 's/^pub const ORT_WEB_VERSION: &str = "\([^"]*\)";$/\1/p' "$LS")" ORT_REV="$(sed -n 's/^pub const ORT_WEB_VENDOR_REV: &str = "\([^"]*\)";$/\1/p' "$LS")" [ -n "$ORT_VERSION" ] || { echo "✗ ORT_WEB_VERSION not found in $LS" >&2; exit 1; } [ -n "$ORT_REV" ] || { echo "✗ ORT_WEB_VENDOR_REV not found in $LS" >&2; exit 1; } DIR="onnxruntime-web-$ORT_VERSION-$ORT_REV" # ─── Pins ──────────────────────────────────────────────────────────────────── # The npm release, by its registry integrity (npm view onnxruntime-web@1.30.0 # dist.integrity, read 2026-10-01; 1.30.0 is the latest stable, only # 1.31.0-dev builds are newer). PINNED_VERSION="1.30.0" NPM_INTEGRITY="sha512-q0y+JrrtukXSzsBWEMccVfqX25LRmosXHF+CaRJmg8pZClzcV7svNc4rKY3jL02Vb7QmRMDs1SigqR4CXAfKYQ==" # ONNX Runtime's LICENSE at tag v1.30.0 (commit f2c39fe2f838cf35ce7da92824f5a5e3ee6e88a7). LICENSE_SHA256="2f07c72751aed99790b8a4869cf2311df85a860b22ded05fa22803587a48922c" # ONNX Runtime's ThirdPartyNotices.txt at the same commit. NOTICES_URL="https://raw.githubusercontent.com/microsoft/onnxruntime/f2c39fe2f838cf35ce7da92824f5a5e3ee6e88a7/ThirdPartyNotices.txt" NOTICES_SHA256="143764b952fdb1a7c69ce653bfba74a7744d6a8a573bfb73e235fba356c83de3" # 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 ORT_WEB_VENDOR_REV. VENDOR_PINNED="onnxruntime-web-1.30.0-aac4" VENDOR_DIGEST="29e86994b4eb7224033963e085a7cb15673142db9db8ad7ac8bfae66df6bcfca" # The files shipped from the package's dist/. DIST_FILES=(ort.jspi.bundle.min.mjs ort-wasm-simd-threaded.jspi.wasm) # The one link in the bundle: a page cited in an error message about # cross-origin isolation, never fetched. DOC_LINK="https://web.dev/cross-origin-isolation-guide/" [ "$ORT_VERSION" = "$PINNED_VERSION" ] || { echo "✗ listen.rs asks for onnxruntime-web $ORT_VERSION, but vendor.sh is pinned to $PINNED_VERSION —" >&2 echo " check the new release (licence, no network use, the graphs still run on WebGPU only), then re-pin PINNED_VERSION and NPM_INTEGRITY" >&2 exit 1 } [ "$DIR" = "$VENDOR_PINNED" ] || { echo "✗ listen.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; } fsize() { wc -c < "$1" | tr -d ' '; } echo "▸ vendoring onnxruntime-web $ORT_VERSION ($DIR)" mkdir -p "$CACHE" # Tell (but do not fail) when npm has a newer stable release to review. LATEST="$(npm view onnxruntime-web dist-tags.latest 2>/dev/null || true)" if [ -n "$LATEST" ] && [ "$LATEST" != "$ORT_VERSION" ]; then echo " note: npm's latest onnxruntime-web is $LATEST (pinned: $ORT_VERSION)" fi # The package, fetched and checked against its pinned integrity beside the # cache entry, then moved in whole, so an interrupted run never leaves half a # package that looks complete. ORT_CACHE="$CACHE/onnxruntime-web-$ORT_VERSION" if [ ! -f "$ORT_CACHE/package/package.json" ]; then tmp="$(mktemp -d "$CACHE/npm.XXXXXX")" (cd "$tmp" && npm pack --silent "onnxruntime-web@$ORT_VERSION" >/dev/null) tgz="$(ls "$tmp"/*.tgz)" got="sha512-$(openssl dgst -sha512 -binary "$tgz" | base64 | tr -d '\n')" [ "$got" = "$NPM_INTEGRITY" ] || { rm -rf "$tmp" echo "✗ onnxruntime-web@$ORT_VERSION from npm is not the pinned tarball (got $got)" >&2 exit 1 } (cd "$tmp" && tar xzf "$tgz") rm -f "$tgz" rm -rf "$ORT_CACHE" mv "$tmp" "$ORT_CACHE" fi grep -q "\"version\": \"$ORT_VERSION\"" "$ORT_CACHE/package/package.json" || { echo "✗ cached onnxruntime-web is not $ORT_VERSION" >&2; exit 1; } grep -q '"license": "MIT"' "$ORT_CACHE/package/package.json" || { echo "✗ onnxruntime-web $ORT_VERSION no longer declares MIT — review it" >&2; exit 1; } [ "$(sha256 "$HERE/LICENSE.txt")" = "$LICENSE_SHA256" ] || { echo "✗ $HERE/LICENSE.txt is not ONNX Runtime's LICENSE at v1.30.0" >&2; exit 1; } # ONNX Runtime's third-party notices: fetched to a temporary name and moved in # only once their hash matches. NOTICES="$CACHE/onnxruntime-$ORT_VERSION-ThirdPartyNotices.txt" if [ ! -f "$NOTICES" ] || [ "$(sha256 "$NOTICES")" != "$NOTICES_SHA256" ]; then rm -f "$NOTICES" curl -fsSL "$NOTICES_URL" -o "$NOTICES.tmp" [ "$(sha256 "$NOTICES.tmp")" = "$NOTICES_SHA256" ] || { rm -f "$NOTICES.tmp"; echo "✗ ThirdPartyNotices.txt ($NOTICES_URL) does not match its pinned sha256" >&2; exit 1; } mv "$NOTICES.tmp" "$NOTICES" fi rm -rf "$SITE"/vendor/onnxruntime-web-* OUT="$SITE/vendor/$DIR" mkdir -p "$OUT" cp "$HERE/worker.js" "$OUT/asr-worker.js" cp "$HERE/LICENSE.txt" "$HERE/NOTICE.txt" "$OUT/" cp "$NOTICES" "$OUT/ThirdPartyNotices.txt" for f in "${DIST_FILES[@]}"; do cp "$ORT_CACHE/package/dist/$f" "$OUT/"; done chmod 644 "$OUT"/* # The runtime must never reach another site: no URL in the worker at all, and # in the bundle none but the documentation link in one error message. if grep -nE 'https?://' "$OUT/asr-worker.js"; then echo "✗ the listener's worker names another site — review it" >&2; exit 1 fi other="$(grep -oE "https?://[^\"'\` )]*" "$OUT/ort.jspi.bundle.min.mjs" | grep -vxF "$DOC_LINK" || true)" [ -z "$other" ] || { echo "✗ ort.jspi.bundle.min.mjs names another site: $other — review it" >&2; exit 1; } # Each runtime file must be exactly what listen.rs pins (RUNTIME_FILES): the # app downloads it by that size and SHA-256. PINS="$(sed -n 's/^ *RuntimeFile { name: "\([^"]*\)", bytes: \([0-9_]*\), sha256: "\([0-9a-f]*\)" },$/\1 \2 \3/p' "$LS" | awk '{ gsub(/_/, "", $2); print }')" [ "$(printf '%s\n' "$PINS" | grep -c .)" = 3 ] || { echo "✗ RUNTIME_FILES in $LS should pin 3 files" >&2; exit 1; } bad=0 while read -r name bytes want; do f="$OUT/$name" [ -s "$f" ] || { echo "✗ vendored runtime is missing $name" >&2; exit 1; } got_bytes="$(fsize "$f")"; got="$(sha256 "$f")" if [ "$got_bytes" != "$bytes" ] || [ "$got" != "$want" ]; then echo "✗ vendor/$DIR/$name is $got_bytes bytes, sha256 $got;" >&2 echo " listen.rs RUNTIME_FILES pins $bytes bytes, sha256 $want." >&2 bad=1 fi done <<< "$PINS" [ "$bad" = 0 ] || { echo " Changed bytes need a new ORT_WEB_VENDOR_REV (a new URL), then new pins there and here." >&2; exit 1; } DIGEST="$(cd "$OUT" && 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 " Its URLs are cached as immutable: bump ORT_WEB_VENDOR_REV in listen.rs," >&2 echo " then set VENDOR_PINNED and VENDOR_DIGEST in $HERE/vendor.sh." >&2 exit 1 } echo "✓ vendor/$DIR ($(du -sh "$OUT" | cut -f1))"