#!/usr/bin/env bash # ============================================================================ # vendor.sh — put the on-device voice runtime (Moonshine WASM) on the app's # own site. # # third_party/moonshine-wasm/vendor.sh [cache-dir] # # Writes /vendor/moonshine-wasm--/ (and removes # any other vendor/moonshine-wasm-* there): # moonshine.mjs, moonshine.wasm from @moonshine-ai/moonshine-wasm on npm # voice-worker.js this directory's worker (the app's own) # LICENSE.txt, NOTICE.txt this directory's licence and notice # THIRD_PARTY/ licences of the code linked into the wasm # # The app never imports these URLs directly. When someone downloads a neural # voice, src/neural_voice.rs stores the three runtime files in the model # store (SHA-256 checked, kept across app updates) and neural_tts.js runs # them from there, in a worker, offline. The TypeScript layer of the npm # package is not vendored: it downloads from Moonshine's CDN, which the app # must never do. # # The version and revision come from MOONSHINE_VERSION and # MOONSHINE_VENDOR_REV in crates/aac-web/src/neural_voice.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; 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}" NV="$AAC/crates/aac-web/src/neural_voice.rs" MS_VERSION="$(sed -n 's/^pub const MOONSHINE_VERSION: &str = "\([^"]*\)";$/\1/p' "$NV")" MS_REV="$(sed -n 's/^pub const MOONSHINE_VENDOR_REV: &str = "\([^"]*\)";$/\1/p' "$NV")" [ -n "$MS_VERSION" ] || { echo "✗ MOONSHINE_VERSION not found in $NV" >&2; exit 1; } [ -n "$MS_REV" ] || { echo "✗ MOONSHINE_VENDOR_REV not found in $NV" >&2; exit 1; } DIR="moonshine-wasm-$MS_VERSION-$MS_REV" # ─── Pins ──────────────────────────────────────────────────────────────────── # The npm release, by its registry integrity (npm view # @moonshine-ai/moonshine-wasm@0.1.5 dist.integrity, read 2026-09-29). PINNED_VERSION="0.1.5" NPM_INTEGRITY="sha512-b90GVFkbuQQthW8wZFU/dvJpnnsISHSSxs/p6Xv/NMfaHotLhRp0e9Risy5/np6QSF3SFust3jGE3Uj2LdecmQ==" # 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 MOONSHINE_VENDOR_REV. VENDOR_PINNED="moonshine-wasm-0.1.5-aac2" VENDOR_DIGEST="a1102edb2e06645107aa8cb38c2774820f4fad3e9f76c0945ac8600ad3801537" # Licences of the code linked into moonshine.wasm, from Moonshine's # core/third-party/ at the commit below and ONNX Runtime's own notices at the # version Moonshine builds (scripts/ort-build-common.sh: ORT_VERSION 1.23.2). MS_COMMIT="234f60faa0eb388b01cdf7e60aca232af37aefda" MS_RAW="https://raw.githubusercontent.com/moonshine-ai/moonshine/$MS_COMMIT/core/third-party" THIRD_PARTY=( "onnxruntime-LICENSE.txt|$MS_RAW/onnxruntime/LICENSE.txt|2f07c72751aed99790b8a4869cf2311df85a860b22ded05fa22803587a48922c" "onnxruntime-ThirdPartyNotices.txt|https://raw.githubusercontent.com/microsoft/onnxruntime/v1.23.2/ThirdPartyNotices.txt|e9e90971a8e75a9a8ac0c6412e29c1202d079998389915aa485f46c816c3b4cc" "Eigen-COPYING.MPL2|$MS_RAW/Eigen/COPYING.MPL2|fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85" "kaldi-native-fbank-LICENSE|$MS_RAW/kaldi-native-fbank/LICENSE|cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" "kissfft-COPYING|$MS_RAW/kissfft/COPYING|a2840585f8411be8e6826a31ef15ae65c950bd74a2437a73b013398a934ad0c6" "nlohmann-LICENSE.MIT|$MS_RAW/nlohmann/LICENSE.MIT|86b998c792894ccb911a1cb7994f7a9652894e7a094c0b5e45be2f553f45cf14" "utf-8-LICENSE.txt|$MS_RAW/utf-8/LICENSE.txt|c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566" "utf8proc-LICENSE.md|$MS_RAW/utf8proc/LICENSE.md|3b510150d34f248a221bb88e1d811238d6c6c18b51231822c42974c39bb07256" ) [ "$MS_VERSION" = "$PINNED_VERSION" ] || { echo "✗ neural_voice.rs asks for Moonshine WASM $MS_VERSION, but vendor.sh is pinned to $PINNED_VERSION —" >&2 echo " check the new release (licence, no GPL, no network use), then re-pin PINNED_VERSION and NPM_INTEGRITY" >&2 exit 1 } [ "$DIR" = "$VENDOR_PINNED" ] || { echo "✗ neural_voice.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 Moonshine WASM $MS_VERSION ($DIR)" mkdir -p "$CACHE" # Tell (but do not fail) when npm has a newer release to review. LATEST="$(npm view @moonshine-ai/moonshine-wasm version 2>/dev/null || true)" if [ -n "$LATEST" ] && [ "$LATEST" != "$MS_VERSION" ]; then echo " note: npm's latest @moonshine-ai/moonshine-wasm is $LATEST (pinned: $MS_VERSION)" fi # npm package: fetched and checked against the pinned integrity beside the # cache entry, then moved in whole, so an interrupted run never leaves half a # package that looks complete. MS_PKG="$CACHE/moonshine-wasm-$MS_VERSION/package" if [ ! -f "$MS_PKG/package.json" ]; then TMP="$(mktemp -d "$CACHE/moonshine-wasm.XXXXXX")" (cd "$TMP" && npm pack --silent "@moonshine-ai/moonshine-wasm@$MS_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 "✗ @moonshine-ai/moonshine-wasm@$MS_VERSION from npm is not the pinned tarball (got $GOT)" >&2 exit 1 } (cd "$TMP" && tar xzf "$TGZ") rm -rf "$CACHE/moonshine-wasm-$MS_VERSION" mv "$TMP" "$CACHE/moonshine-wasm-$MS_VERSION" fi grep -q "\"version\": \"$MS_VERSION\"" "$MS_PKG/package.json" || { echo "✗ cached moonshine-wasm is not $MS_VERSION" >&2; exit 1; } grep -q '"license": "MIT"' "$MS_PKG/package.json" || { echo "✗ moonshine-wasm $MS_VERSION no longer declares MIT — review it" >&2; exit 1; } # Third-party licence texts: fetched to a temporary name and moved in only # once their hash matches. TP_CACHE="$CACHE/moonshine-wasm-third-party" mkdir -p "$TP_CACHE" for entry in "${THIRD_PARTY[@]}"; do IFS='|' read -r name url want <<< "$entry" f="$TP_CACHE/$name" if [ ! -f "$f" ] || [ "$(sha256 "$f")" != "$want" ]; then rm -f "$f" curl -fsSL "$url" -o "$f.tmp" [ "$(sha256 "$f.tmp")" = "$want" ] || { rm -f "$f.tmp"; echo "✗ $name ($url) does not match its pinned sha256" >&2; exit 1; } mv "$f.tmp" "$f" fi done rm -rf "$SITE"/vendor/moonshine-wasm-* MS="$SITE/vendor/$DIR" mkdir -p "$MS/THIRD_PARTY" cp "$MS_PKG/dist/moonshine.mjs" "$MS_PKG/dist/moonshine.wasm" "$MS/" cp "$HERE/voice-worker.js" "$HERE/LICENSE.txt" "$HERE/NOTICE.txt" "$MS/" for entry in "${THIRD_PARTY[@]}"; do IFS='|' read -r name _ _ <<< "$entry" cp "$TP_CACHE/$name" "$MS/THIRD_PARTY/$name" done # The runtime must never reach another site. (moonshine.mjs only fetches its # own wasm, same origin; the app hands it the bytes anyway.) if grep -nE 'https?://' "$MS/moonshine.mjs" "$MS/voice-worker.js"; then echo "✗ the voice runtime names another site — review it" >&2; exit 1 fi # Each runtime file must be exactly what neural_voice.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' "$NV" | tr -d '_')" [ "$(printf '%s\n' "$PINS" | grep -c .)" = 3 ] || { echo "✗ RUNTIME_FILES in $NV should pin 3 files" >&2; exit 1; } while read -r name bytes want; do f="$MS/$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 " neural_voice.rs RUNTIME_FILES pins $bytes bytes, sha256 $want." >&2 echo " Changed bytes need a new MOONSHINE_VENDOR_REV (a new URL), then new pins there and here." >&2 exit 1 fi done <<< "$PINS" DIGEST="$(cd "$MS" && 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 MOONSHINE_VENDOR_REV in neural_voice.rs," >&2 echo " then set VENDOR_PINNED and VENDOR_DIGEST in $HERE/vendor.sh." >&2 exit 1 } echo "✓ vendor/$DIR ($(du -sh "$MS" | cut -f1))"