#!/usr/bin/env bash # Boundary tripwire: flag domain-taxonomy leaks in the Svelte frontend. # # Implements DR-094 (see docs/requirements.md). # # The project rule (CLAUDE.md, docs/architecture/02-svelte-frontend.md) is that # the frontend is presentation-only and the Rust backend owns domain logic โ€” # including Jellyfin's item-type *taxonomy* (what the category "Music" means as a # set of item types). See docs/specs/scoped-search-boundary.md for the incident # that motivated this check. # # โš ๏ธ This is a TRIPWIRE, NOT A PROOF. A grep cannot distinguish taxonomy-as-policy # (a leak) from taxonomy-as-display (legitimate: "is this a music card?"). It # targets the machine-detectable signature of the leak class and defers # everything subtler to the human spec-review checklist # (docs/specs/SPEC-REVIEW-CHECKLIST.md). A clean run here does not mean the # boundary is respected; it means the crudest violation isn't present. # # What it flags: an array literal naming two or more Jellyfin item types, # ANYWHERE in src/ โ€” i.e. the frontend deciding that a *category* maps to a *set* # of Jellyfin types, which is domain knowledge the backend should own. # Single-type arrays (`includeItemTypes: ["Movie"]`) are a page saying "I show # movies" and are allowed. Type *inspection* (`item.type === "Audio"`) is display # logic and is not matched. # # ๐Ÿ”ด What it still CANNOT see (do not read a green run as proof): # - a type set built at run time: [...musicTypes, "Playlist"] # - types split across variables: const A = "Audio"; [A, B] # - taxonomy as control flow: switch (t) { case "Audio": โ€ฆ } # t === "Audio" || t === "MusicAlbum" # - an item type absent from ITEM_TYPES below (false negative by design) # # This check was hardened in July 2026 after the audit found it passing on the # very leak it was written for: the original pattern was anchored to # `includeItemTypes:` at the query site, so assigning the same array to a named # const evaded it entirely. See docs/specs/boundary-tripwire-hardening.md (DR-094). # # Escaping a genuine exception: add the file+reason to the ALLOWLIST below. set -euo pipefail cd "$(dirname "$0")/.." # Files permitted to contain a multi-type item-type array, with the reason. # Keep this SHORT. A growing allowlist means the boundary is eroding โ€” that is a # signal to push taxonomy into Rust, not to keep appending here. ALLOWLIST=( # "Things a person appeared in" is arguably taxonomy, but it is a fixed # two-type filmography query with no category-configuration behind it. Tracked # as acceptable pending any person-scope work; revisit if it grows. "src/lib/components/library/PersonDetailView.svelte" # Grid styling predicate over `config.itemType`, a value the page already # declares about itself. Selects a *look*, issues no query, and would only # change if the UI were redesigned โ€” presentation, not taxonomy-as-policy. "src/lib/components/library/GenericMediaListPage.svelte" # "Is this item a container?" predicate for downloads browsing. # BORDERLINE โ€” leans domain: the container set grows when Jellyfin adds a # container type. TODO: replace with a backend-supplied `MediaItem.isContainer` # flag and remove this entry. Tracked in # docs/specs/boundary-tripwire-hardening.md ยงOut of scope. "src/lib/components/downloads/DownloadedBrowse.svelte" ) # Hard cap so erosion is caught mechanically rather than by whoever notices. # Deliberately just above the current count: the next exception forces a # conversation instead of a one-line append. MAX_ALLOWLIST=4 if [[ "${#ALLOWLIST[@]}" -gt "$MAX_ALLOWLIST" ]]; then echo "โŒ Allowlist has ${#ALLOWLIST[@]} entries (max $MAX_ALLOWLIST)." echo " Push taxonomy into Rust instead of appending here." exit 1 fi is_allowed() { local file="$1" for allowed in "${ALLOWLIST[@]}"; do [[ "$file" == "$allowed" ]] && return 0 done return 1 } # Two or more adjacent Jellyfin item-type string literals inside a bracket. # # NOT anchored to `includeItemTypes:` โ€” that was the original rule, and it missed # the real leak: `searchScope.ts` assigned the same array to a named const and # dereferenced it one indirection away from the query, so the grep never saw it # while CI stayed green. Matching the array literal itself catches a const, a # Record value, a function return, and an inline query alike. # # Deliberate limits: # - requires TWO adjacent types, so single-type presentation # (`itemType: "Movie"`) stays legal โ€” the rule targets *category* taxonomy; # - requires string literals, so `item.type === "Audio"` (display inspection) # does not match; # - uses an explicit type list rather than a generic capitalised-word pattern, # so unrelated string arrays (`["High","Low"]`) produce no noise. # # An item type missing from this list is a false *negative*, never a false # positive โ€” the check degrades safely as Jellyfin adds types. ITEM_TYPES='Movie|Series|Episode|Audio|MusicAlbum|MusicArtist|MusicVideo|Season|BoxSet|Playlist|Book|AudioBook|Video|Person|Folder|CollectionFolder|TvChannel|LiveTvChannel' PATTERN="\[[[:space:]]*\"($ITEM_TYPES)\"[[:space:]]*,[[:space:]]*\"($ITEM_TYPES)\"" echo "๐Ÿ”Ž Checking frontend for domain-taxonomy leaks (item-type array literals)โ€ฆ" # Collect hits, excluding tests and the allowlist. violations="" while IFS= read -r line; do [[ -z "$line" ]] && continue file="${line%%:*}" case "$file" in *.test.*) continue ;; esac if is_allowed "$file"; then echo " โญ๏ธ allowlisted: $line" continue fi violations+="$line"$'\n' done < <(grep -rInE "$PATTERN" src/ 2>/dev/null || true) if [[ -n "$violations" ]]; then echo "" echo "โŒ Frontend boundary violation: an item-type array literal defines a" echo " category in the presentation layer. That taxonomy belongs in Rust โ€”" echo " send an opaque scope/enum and let the backend expand it to item types" echo " (see SearchScope::item_types() in src-tauri/src/repository/types.rs)." echo " Assigning the array to a const does not make it presentation." echo " See docs/specs/scoped-search-boundary.md and CLAUDE.md." echo "" echo "$violations" | sed 's/^/ /' echo " If this is a genuine exception, add the file + reason to ALLOWLIST in" echo " scripts/check-frontend-boundary.sh โ€” but prefer moving it to Rust." exit 1 fi echo "โœ… No multi-type taxonomy queries in the frontend." echo " (Reminder: this is a tripwire, not a proof โ€” the spec-review checklist is" echo " the real gate for subtler leaks.)"