Airwave

Changelog

The most recent Airwave releases and what changed in each.

The 20 most recent releases. For the complete history (874 releases), see CHANGELOG.md on GitHub.

0.16.10 (2026-10-23)

Internal

  • Node baseline moved from 22 to 24 (current active LTS): the Docker base image (node:24-bookworm-slim) and the desktop / tv-tauri CI workflows. The server runs on Bun and is unaffected; this is build tooling only, with no change to the shipped runtime. Verified locally on Node 24.22.1: clean install, check-fast across all 16 workspaces, the web/tv-web vite builds, the server build, and the Expo CLI all pass.

0.16.9 (2026-10-21)

Internal

  • macOS release build retries stapler staple. Apple's notarytool --wait returns "Accepted" before the notarization ticket is reliably published to its CloudKit ticket-delivery service, so an immediate xcrun stapler staple can fail to fetch it (NSURLErrorDomain -1005, Error 68) and fail the desktop release job even though notarization itself succeeded (seen on the macOS Intel runner during the v0.16.8 release). The signing script now retries the staple with backoff (up to ~5 minutes), so a transient ticket-delivery flake no longer breaks the build. CI-only; no change to the shipped app.

0.16.8 (2026-10-21)

Security: the SSE endpoint no longer accepts the API key in the URL at all (completes GHSA-fjx9-jwmr-2r58).

Changed

  • GET /api/public/v1/events no longer accepts ?api_key=. The key is accepted only via the X-API-Key / Authorization: Bearer header (fetch-based clients) or exchanged for a single-use ?ticket= (browser EventSource, via POST /events/ticket, added in 0.16.7). This removes the last path that put a long-lived credential in a URL — and thus in proxy/CDN access logs and browser history.

Breaking / Migration

  • A browser EventSource that connected with ?api_key=<key> must switch to the ticket flow: POST /api/public/v1/events/ticket with the key in the X-API-Key header, then open GET /api/public/v1/events?ticket=<ticket>. Because a ticket is single-use, reconnect by minting a fresh one. Fetch-based clients (Tauri, dashboards, Home Assistant) are unaffected — they already use the header. Docs updated at /docs/integrations/tauri.

0.16.7 (2026-10-21)

Security follow-up to 0.16.6: a single-use connection ticket for the SSE stream, so the API key need not ride in the URL at all (GHSA-fjx9-jwmr-2r58).

Added

  • POST /api/public/v1/events/ticket mints a single-use, ~60-second connection ticket (header-authed). A browser EventSource can't send an auth header, so it now exchanges its key for a ticket and opens the stream with ?ticket= — the long-lived key never travels in a URL, so it can't leak via proxy/CDN access logs or browser history. The stream (GET /api/public/v1/events) accepts ?ticket= alongside the existing header and ?api_key= auth (both kept for compatibility). Tickets are in-memory, single-use, and expire in ~60s, so a leaked ticket is dead on arrival.

Dev

  • scripts/sse-listen-ticket.ts — an SSE listener that exercises the ticket path (mint → ?ticket=), the browser-style counterpart to sse-listen.ts (which tests the header/key path).

0.16.6 (2026-10-21)

Security: stops the public API key (and other secrets) from being written to logs (GHSA-fjx9-jwmr-2r58).

Fixed

  • Secrets passed in a URL are now redacted from the request log. The SSE endpoint accepts the key as ?api_key= (a browser EventSource can't set headers), and Hono's request logger writes the full path including the query string — so the key was landing in stdout / docker logs in plaintext. The logger now masks api_key / token / X-Plex-Token / code / state / ticket query values before printing. ?api_key= still works unchanged; only the log output is affected. (A short-lived connection ticket for SSE, so the key need not ride in the URL at all, follows next.) Reported by @cashcon57.

0.16.5 (2026-10-21)

Security: closes an open redirect in the Plex sign-in flow (GHSA-6p5v-m89f-x7hg).

Fixed

  • Plex web sign-in now validates the redirect_uri. /api/plex/authorize built Plex's forwardUrl from an unvalidated redirect_uri, so a crafted sign-in link could bounce the Plex pin to an attacker's URL after the victim approved on plex.tv. The proxy now accepts a redirect_uri only when its path is the better-auth Plex callback AND its origin is trusted — the configured BETTER_AUTH_URL / CORS_ORIGIN / EXTRA_CORS_ORIGINS, or the forwarded host when the request arrived over the Airwave Cloud tunnel — so the pin can only ever return to your own callback. Every real sign-in path (localhost, reverse proxy, LAN, tunnel) is unaffected. Reported by @cashcon57.

0.16.4 (2026-10-21)

Security: closes an unauthenticated path-traversal in the /img artwork proxy (GHSA-vc4j-53mw-85mx).

Fixed

  • The /img artwork proxy can no longer be tricked into reaching arbitrary Plex endpoints. It took a caller-supplied path and checked only that it started with /library or /photo, but .. / %2e%2e segments are resolved by fetch AFTER that check — so a crafted ?path= could reach any admin-token Plex endpoint (sessions, preferences, library/account data) and the /photo transcoder's url= (SSRF), unauthenticated and reachable over Remote Access. Artwork paths are now resolved first and must stay on the same Plex server under /library/, with no traversal, host change, or query string; w/h are clamped to a sane range; the public artwork route additionally constrains the rating key to digits. No change to legitimate artwork (every real art path is /library/metadata/...). Reported by @cashcon57.

0.16.3 (2026-10-15)

Resolves apps/desktop's electrobun type errors, so pnpm check-fast is green across the whole repo.

Internal

  • apps/desktop resolves electrobun / electrobun/bun to the Hutch devkit for types. electrobun v2 ships its SDK through the Hutch devkit (.hutch/, gitignored, projected by electrobun dev), not npm — the electrobun npm package is a stub that throws on import, so TypeScript fell back to it and reported TS7016 (no declarations) and a knock-on TS7006 (implicit-any callback param). A paths mapping in apps/desktop/tsconfig.json points the two imported specifiers at the devkit's real types. It's inline rather than extends .hutch/devkit/tsconfig.json, so the config stays valid when the devkit is absent (fresh checkout, or the 1.18.1 macOS-Intel build job that uses the classic CLI, no Hutch). No build impact — electrobun resolves its SDK through Hutch, not this tsconfig.

0.16.2 (2026-10-15)

Removes vestigial TypeScript declaration-emit config so the server and internal packages type-check cleanly. No behavior change.

Internal

  • apps/server and packages/{api,auth,db} no longer set composite / declaration / outDir. These packages are consumed as source (their exports point at ./src, and the server is bundled by tsdown, not tsc), so the project-reference and declaration-emit options were dead config — yet they switched on tsc's declaration-portability (TS2883) and rootDir (TS6059 / TS6307) checks, leaving ~133 phantom errors that bun check (and a forced tsc) surfaced. The internal packages now use the { "noEmit": true } baseline; the server keeps its paths / jsx and its check-types moves from tsc -b to tsc --noEmit. pnpm check-fast is now green across the repo except apps/desktop (tracked separately). The Docker build uses tsdown and is unaffected.

0.16.1 (2026-10-15)

Adds check-fast, a fast bun check typecheck across the monorepo.

Internal

  • pnpm check-fast runs Bun's TypeScript type checker (bun check, TS7, roughly 6x faster than tsc) across every workspace, and pnpm -F <pkg> check-fast checks a single package. Each workspace gains a check-fast script; web/tv-web run vite build first to generate their (gitignored) route tree. Named check-fast rather than check because bun check runs a same-named script if one exists, which would recurse. The existing tsc check-types is unchanged; this is purely additive.

0.16.0 (2026-10-15)

Type-correctness pass: fixes the real latent type errors in shipped code and holds the server's diagnostic scripts to the project's strict typing. No behavior change.

Fixed

  • GUID remap resolver (guid-remap.ts) is now type-proven. Its array indexing relied on length/empty guards that were already runtime-correct; the code now satisfies noUncheckedIndexedAccess too (guarded the resolved row and the live-match candidate, and narrowed the guid-scheme filter to its real union). Behavior is unchanged.
  • Remote-access relay buffers type as Uint8Array<ArrayBuffer>. encodeBody/send now carry the ArrayBuffer-backed buffer type the browser WebSocket.send requires.
  • AI-lineup replay view: removed a dead onScrub prop from the gantt scrubber (its slider is currently disabled and the prop was never passed).
  • Workflow engine: cast the World value across the @workflow/world-postgres ↔ workflow/runtime type skew (the queue-name brand), matching the existing workaround in the same file.

Internal

  • Server diagnostic scripts (apps/server/scripts/*) held to strict typing (removed unused bindings, guarded possibly-undefined indexing, loosened a probe helper to accept null ids), since the server tsconfig includes scripts/.

0.15.66 (2026-10-13)

Fixes the Apple TV dropping back to server setup after a restart (issue #64).

Fixed

  • Apple TV now keeps its server and session across a reboot. On tvOS, the server URL, token, and session were stored in AsyncStorage, which the OS keeps in a purgeable cache and clears when storage runs low — so after a restart the Apple TV had forgotten its server and returned to onboarding. Those small values now live in UserDefaults (via React Native's built-in Settings), the one tvOS store with a durability guarantee, so they survive a reboot. No new native module or dependency; iPad, iPhone, and Android are unchanged (they keep the Keychain/Keystore and AsyncStorage). A one-time migration carries over any values that haven't been purged yet; an Apple TV whose cache was already cleared re-onboards once.

Internal

  • session-storage.ts backs the Apple-TV durable store with core RN Settings (NSUserDefaults), gated on Platform.OS === "ios" && Platform.isTV, using an empty-string tombstone for deletes (UserDefaults has no remove).

0.15.65 (2026-10-12)

Fixed

  • Cloning a membership or manual channel now loads its preview automatically. Cloning pre-fills the create page with the source channel's setup (all three modes already worked). But the Preview only auto-resolved for filter clones; a playlist/collection or hand-picked clone sat on "Nothing to preview yet" until you nudged it. The preview's "load when the pool goes from empty to populated" detection now fires on the create/clone page for every mode, so a clone's pre-filled pool previews on load. (The edit page is unchanged — it still shows the saved preview without a redundant re-resolve.)

0.15.64 (2026-10-12)

Fixed

  • Re-importing a lineup now detects hand-picked and collection/playlist channels as "already imported." Filter channels were correctly flagged as duplicates (and skipped) on a re-import, but manual and membership channels weren't — they showed up ready to import again even though they already existed. The duplicate check now includes a manual channel's picked items and a membership channel's collections/playlists in its content signature, so re-importing the same lineup onto the same server flags all of them as already imported and leaves them unchecked by default.

0.15.63 (2026-10-12)

Added

  • Export a single channel or package, not just the whole lineup. exportLineup takes an optional selection (channel numbers or package keys) and produces the same versioned document with just those entries (a single-channel export brings its package along), via a new transfer.exportSelection endpoint. Groundwork for sharing one channel/package.

Internal

  • Documented that the channel-editor filter Copy/Paste envelope (airwave/filter, client-only, filter-only) stays deliberately separate from the server-side lineup-exchange format — different payload, transport, and scope, so they aren't unified.

0.15.62 (2026-10-12)

Importing a lineup now recreates hand-picked and collection/playlist channels instead of dropping them.

Added

  • Manual and membership channels import for real. Previously only filter channels survived an import; hand-picked (manual) and collection/playlist (membership) channels were dropped. Now:
    • Same Plex server (re-importing onto the box the lineup came from): the channel imports exactly as-is — a membership channel stays a live collection/playlist, a manual channel keeps its picks. No data lost.
    • Different server: the channel's items are matched to your library by their metadata ids (tmdb/tvdb/imdb) — a fast indexed lookup, with a live Plex fallback for anything not yet synced — and rebuilt as a hand-picked snapshot. The import reports "matched X of Y titles"; unmatched titles are skipped, never a hard failure.
  • Staging preview shows portability. A manual/membership channel from another server now shows how many of its titles match your library before you import.

Internal

  • Carries the source Plex server id in the export (schema 1.2) to detect the same-server case; the import plan resolves each definition (predicate / same-server membership+manual / cross-server remap) and executeChannelPlan performs the GUID remap. The content signature now includes manual/membership item identity so distinct hand-picked channels don't collide. Verified with a dry-run round-trip probe.

0.15.61 (2026-10-12)

Exports now carry portable identity for hand-picked and collection/playlist channels (schema 1.2).

Added

  • Manual and membership channels export their items by external GUID. A hand-picked (MANUAL) channel exports each pick's tmdb/tvdb/imdb id (a whole-show pick stays a show, so it keeps expanding live after import); a collection/playlist (MEMBERSHIP) channel exports its container's direct members by GUID (shows stay shows, not exploded into episodes). This is what lets these channels rebuild against a different server's library on import. Bumps the exchange format to schema 1.2 (additive).
  • Export-time GUID backfill. If a referenced item's id isn't cached yet (e.g. exported before a metadata re-sync), the exporter fetches it live from Plex on the fly and persists it, so an export is always complete regardless of sync state. Bounded to only the referenced items missing an id.

0.15.60 (2026-10-12)

The engine that resolves a shared channel's items to your own library (for cross-server import).

Added

  • GUID → ratingKey remap resolver (services/transfer/guid-remap.ts): given a shared item's external ids (tmdb/tvdb/imdb), find the matching item in this server's library. Tiered for speed and completeness: a single indexed lookup over the new MediaItem id columns resolves everything that's synced; anything missing (added since the last sync, or in an unsynced library) falls back to a live GET /library/matches call to Plex, and the ids it finds are written back to the cache so the next import is a pure indexed hit. Reports matched vs. unmatched; an unmatched item is skipped with a note, never a failure. Not wired into import yet — that's next.
  • matchByGuid Plex client helper wrapping /library/matches (external guid → local ratingKey, score-gated for movies/shows), and includeGuids=1 on the batched metadata fetch.

Dev

  • probe-guid-remap.ts exercises the resolver end to end against the real library.

0.15.59 (2026-10-12)

Makes the external metadata ids first-class, indexed columns on cached media (for cross-server channel import).

Added

  • MediaItem.tmdbId / tvdbId / imdbId columns (migration add_mediaitem_external_ids), each indexed per source, so a shared channel's items can be resolved to this server's ratingKey by a fast indexed lookup. The ids move out of the guide JSON blob (where 0.15.55 first stashed them) onto the item itself, since they're identity, not display metadata. Populated by the metadata sync.

Note

  • The columns start empty and fill in on the next metadata sync (no backfill — a re-sync self-repairs). Nothing reads them yet; the import resolver that uses them is next.

Dev

  • probe-external-ids.ts now reports column coverage; probe-guid-match.ts samples ground truth from the columns.

0.15.58 (2026-10-12)

Lineup export/import stops dropping four channel settings (schema 1.1).

Fixed

  • Exporting a channel now carries its scheduling config. The export predated several Channel columns and silently dropped them, so an imported channel lost its grouping/rotation strategy, its Exclude specials and Keep multi-part together toggles, and its shuffle seed (so it re-shuffled differently). All four now travel with the channel and are applied on import, so a round-tripped channel matches the original. Correctly-omitted instance-specific fields (provenance, schedule cursor, media binding) stay omitted.

Internal

  • Bumps the exchange format to schema 1.1 (additive/minor) with a 1.0→1.1 migration that defaults the new fields, so older 1.0 exports still import cleanly. Covered by schema.test.ts.

On this page