Skip to content

Releases: revtex/OpenScanner

v1.3.2 — Auth refresh stability

29 Apr 19:37
cf8b003

Choose a tag to compare

OpenScanner v1.3.2 — Auth refresh stability

This release fixes the recurring ~15-minute logoffs reported against 1.3.0 and 1.3.1.

Fixed

  • Refresh cookie now reaches the v1 refresh endpoint. The refresh-token cookie was scoped to Path=/api/auth, but 1.3.0 moved the silent-refresh call to /api/v1/auth/refresh. Per RFC 6265 §5.1.4 path-matching, a /api/auth cookie does not match /api/v1/auth/refresh, so the browser silently dropped the cookie on every refresh — the server returned 401 "no refresh token" the moment the 15-minute access JWT expired, and the user was bounced to the login screen. The cookie is now scoped to Path=/api, which covers both legacy and v1 endpoints. This is the actual root cause of the logoffs in 1.3.0 / 1.3.1.
  • Audio-element auth recovery now shares the same single-flighted refresh promise as the rest of the app, so a 401 on an <audio> fetch can no longer race the scheduled refresh and replay the single-use refresh cookie.

Security

  • Refresh-token rotation now honors a 30-second replay grace window for already-rotated tokens (OAuth 2.0 Security BCP §4.13). If the same refresh cookie is presented twice within the window — parallel tabs, service-worker retries, page reload mid-rotation — the server returns the cached successor instead of revoking the family. Replays after the grace window still revoke the family, preserving theft detection.

Tests

  • Regression coverage that would have caught the cookie-path bug: cookie-attribute tests now assert the cookie path is a prefix of every refresh endpoint (/api/auth/refresh and /api/v1/auth/refresh), and a new end-to-end test logs in through httptest.NewServer with a real cookiejar and asserts the browser actually sends the cookie back to both paths.

Upgrading

docker compose pull && docker compose up -d

Already-logged-in users will pick up the new cookie path on their next login. If you want to force the rotation immediately, clear site cookies for your OpenScanner origin and log back in.

v1.3.1

29 Apr 01:02
abb7f37

Choose a tag to compare

What's Changed

Full Changelog: v1.3.0...v1.3.1

v1.3.0 — Native /api/v1/* REST + WebSocket surface

29 Apr 00:35
e70f3e4

Choose a tag to compare

Added

  • Native /api/v1/* REST surface alongside the existing legacy routes. All v1 responses use a structured error envelope ({"error":{"code","message","details"}}) with stable string codes (validation_failed, unauthorized, forbidden, not_found, conflict, unprocessable, rate_limited, internal); 5xx envelopes include the request ID under details.requestId.
  • v1 call-upload endpoint (POST /api/v1/calls) with native multipart field names (systemId, talkgroupId, startedAt, frequencyHz, durationMs, unitId) and RFC 3339 startedAt enforcement (unix timestamps no longer accepted on v1). Companion POST /api/v1/calls/test returns 204 on a valid API key.
  • v1 listener endpoints: GET/PUT /api/v1/listener/tg-selection (renamed from /api/auth/tg-selection), GET /api/v1/calls, GET /api/v1/calls/:id/audio, GET /api/v1/calls/:id/transcript, share/bookmark endpoints, and unauthenticated /api/v1/health, /api/v1/setup/*, /api/v1/auth/{login,refresh,logout,password,me}.
  • v1 admin endpoints under /api/v1/admin/* for talkgroup/unit/group/tag imports, RadioReference preview (path simplified — no /csv suffix), transcription status, and Swagger session bootstrap.
  • Native JSON-object framed WebSocket protocol on GET /api/v1/ws/listener and GET /api/v1/ws/admin. Frames carry a type discriminator (connection.welcome, scanner.config, call.new, call.transcript, listener.count, listener.feedMap.snapshot/update, session.expired, connection.rejected, admin.event, admin.request, admin.response) instead of the legacy 3-letter array opcodes. Admin error responses mirror the REST {code,message,details?} envelope. The frontend connects to the v1 paths; legacy /ws, /api/ws, and /api/admin/ws keep emitting the array-framed protocol unchanged for in-the-wild clients.
  • RFC 8594 deprecation headers (Deprecation: true, Sunset, Link: <successor>; rel="successor-version", Cache-Control: no-store) on every legacy /api/* and legacy WebSocket route, pointing at the native /api/v1/* successor. Per-request structured warn log (legacy endpoint hit) records method, path, and a truncated API-key identifier — never the raw key.
  • Admin endpoint GET /api/v1/admin/legacy-usage returning a 24-hour aggregate of legacy-endpoint hits ({method, path, apiKeyIdent, count, lastSeen}), backed by an in-memory ring buffer (no schema change).
  • Admin dashboard banner that surfaces legacy-API usage from the new endpoint, with an expandable details table (method, path, API key, count, last seen) and per-session dismiss.

Changed

  • Frontend now talks to the native /api/v1/* surface for every REST call (RTK Query base URL, raw fetch() for audio downloads and silent token refresh, the service-worker passthrough rules, the dev-server proxy, and the Swagger UI bootstrap). Tg-selection moves from /api/auth/tg-selection to /api/v1/listener/tg-selection; RadioReference CSV preview moves to /api/v1/admin/radioreference/preview; legacy-usage report is consumed at /api/v1/admin/legacy-usage. Legacy /api/* routes remain available for non-frontend clients with the existing deprecation headers.
  • API-key authentication on /api/v1/* upload routes accepts only Authorization: Bearer <api-key>; the legacy X-API-Key header, ?key= query parameter, and key= form field continue to work on legacy routes only. JWT-shaped Bearer tokens on v1 API-key routes are rejected with invalid_credentials.
  • Swagger UI now documents every native /api/v1/* endpoint, not just the three previously annotated handlers. Legacy /api/* annotations remain in place until those routes are retired.

v1.2.1 — postcss security pin

25 Apr 22:53
7feb493

Choose a tag to compare

v1.2.1 — postcss security pin

Security

  • Pin transitive postcss to >=8.5.10 via a pnpm override to address the Dependabot alert "PostCSS has XSS via unescaped </style> in its CSS stringify output" (medium).

PostCSS is a dev-only transitive dependency of Vite/Tailwind and never reaches the production runtime, but the override removes the alert and ensures contributors building from source pick up the patched version.

Upgrading

  • No action needed. docker compose pull && docker compose up -d as usual.

v1.2.0 — Audio HTTP migration & backend handler decomposition

25 Apr 22:41
1d9a095

Choose a tag to compare

v1.2.0 — Audio HTTP migration & backend handler decomposition

This release moves audio playback off the WebSocket onto a proper HTTP <audio> element backed by /api/calls/:id/audio, decomposes the monolithic backend internal/api package into feature-scoped handler packages, reorganises the frontend, and ships a number of multi-device polish fixes.

Added

  • Session cookie (os_session) issued on login/refresh, cleared on logout. GET /api/calls/:id/audio now accepts either Authorization: Bearer or the cookie, so <audio> playback works without client-side header injection. httpOnly, Secure (when HTTPS), SameSite=Strict, scoped to /api. Cross-site requests are rejected via a Sec-Fetch-Site check; invalid cookies fall through to anonymous so publicAccess=true deployments are unaffected.
  • Canonical GET /api/ws listener WebSocket route. GET /ws remains as a compatibility alias.

Changed

  • WebSocket CAL messages no longer carry embedded base64 audio. Audio is fetched on demand from /api/calls/:id/audio. Frontend playback now uses the platform <audio> element backed by MediaElementAudioSourceNode.
  • Service worker passes /api/calls/:id/audio and /api/shared/:token/audio straight through, letting the browser handle Range requests natively.
  • Backend HTTP handlers decomposed from the monolithic internal/api package into feature-scoped subpackages under internal/handler/ (auth, calls, bookmarks, share, setup, health, admin/{imports,radioreference,transcriptions}, routes, shared). No route paths, methods, middleware ordering, response shapes, or status codes changed.
  • internal/handler/calls/calls.go (~1500 LOC) split into upload.go, audio.go, search.go, transcript.go, slim calls.go. internal/middleware/middleware.go split into cors.go, auth.go, logging.go, limits.go.
  • Admin CRUD business logic extracted from internal/ws into a new transport-agnostic internal/admin package. The WebSocket layer now only routes ADM_REQ frames to admin.Operations. Wire protocol and action names unchanged.
  • Frontend reorg: services/{ws,audio,util}/, hooks/{shared,scanner,admin}/, types/ split into topic modules with a barrel index.ts, app/slices/{shared,scanner,admin}/. All call sites updated; runtime behaviour unchanged.
  • Admin Options panel: dropped the noisy "Active" badge from every wired setting (only "Planned" badges remain); audio-conversion description rewritten to match the dropdown.
  • Deployment guide reverse-proxy instructions list /api/ws alongside /ws and /api/admin/ws for WebSocket-upgrade forwarding.

Fixed

  • Lock the primary admin's Allowed Systems selector in the user editor — the first user always has access to every system; badges are read-only with all systems shown as allowed.
  • Default audioEncodingPreset seeded into the settings table is now mp3_32k (matching the dropdown's "(default)" label) instead of aac_lc_32k.
  • Audio playback now silently recovers from a 401 on /api/calls/:id/audio by triggering a single token refresh and retrying. Fixes the case where a sibling-device login pushes a desktop's access JWT off the active list.
  • Per-user concurrent JWT cap raised from 5 to 20 (auth.MaxRefreshFamilies). With a 15-minute access TTL refreshing ~4×/hour, the old limit pushed a desktop's session off the active list within an hour of normal multi-device use.
  • WebSocket clients (listener and admin) detach handlers and wait for open before closing a still-CONNECTING socket during reconnect, suppressing the cosmetic "WebSocket is closed before the connection is established" browser warning.
  • dirmonitor watcher: rename local realrealPath to avoid shadowing the Go builtin.

Upgrading

  • Back up your openscanner.db before upgrading.
  • No config or schema migration required.
  • Reverse-proxy users: ensure /api/ws is also forwarded with Upgrade/Connection headers (the existing /ws and /api/admin/ws rules continue to work unchanged).
  • docker compose pull && docker compose up -d as usual.

v1.1.2

24 Apr 03:37
48d9a80

Choose a tag to compare

Security-only patch release: batch of Snyk taint-analysis hardening fixes.

Security

  • Audio reads for the call-ingest WebSocket broadcast (calls.go and dirmonitor) now go through os.Root + io.LimitReader, structurally confined to the recordings directory.
  • GET /api/calls/:id/audio and GET /api/shared/:token/audio open via os.Root and stream with http.ServeContent instead of c.File, so no joined absolute path reaches gin.
  • openscanner upgrade --binary <path> normalises both source and target paths to absolute cleaned form, requires the source to be a regular file, and confines temp-file cleanup to the target directory.
  • Dirmonitor delete_after=1 cleanup now removes audio and sidecar files via os.Root.Remove scoped to the watched directory, on top of the existing symlink-resolve guards.
  • The openscanner CLI validates --server / OPENSCANNER_SERVER URLs (scheme must be http/https, non-empty host, userinfo/fragments stripped) before any net/http call.
  • Bookmarks and search-panel download buttons now sanitise server-supplied audioName (path separators, control chars, quote/angle-bracket chars, leading dots stripped; capped at 200 chars) before assigning to <a download>. Helper lives in services/downloadFilename.ts.

See CHANGELOG.md for the full list.

v1.1.1

24 Apr 01:53
5ff2068

Choose a tag to compare

Multi-arch Docker images (linux/amd64 + linux/arm64), sha-* tag cleanup, and weekly GHCR cleanup workflow. See CHANGELOG.md for details.

v1.1.0

23 Apr 22:30
38d2990

Choose a tag to compare

[1.1.0] — 2026-04-23

Added

  • Commit GitHub ruleset definitions under .github/rulesets/ so branch
    and tag protection policy is versioned with the code.
  • Release workflow now builds standalone binaries for Linux, macOS, and
    Windows (amd64 + arm64 where applicable) on every v* tag and
    attaches them to the GitHub Release alongside a SHA256SUMS.txt.
  • Release archives ship the user guides (README, admin, deployment,
    recorder) as styled PDFs, and the same PDFs are attached to the
    GitHub Release as standalone downloads.

Fixed

  • PDF user guides rendered code blocks with ~50pt of phantom left
    padding and let long lines overflow the right margin. Pandoc's
    built-in skylighting CSS is now neutralised so code aligns with the
    block's left edge and wraps cleanly.

v1.0.0 — Initial release

23 Apr 20:47

Choose a tag to compare

OpenScanner v1.0.0

The first stable release of OpenScanner — a modern, self-hosted web-based radio call manager for monitoring, searching, and sharing scanner traffic in real time.

OpenScanner is a ground-up reimplementation of rdio-scanner, built as a single Go binary with an embedded React frontend. It keeps backward compatibility with rdio-scanner's upload API, so existing Trunk-Recorder rdioscanner_uploader configs and SDRTrunk Rdio Scanner streaming targets work unchanged.


Highlights

  • Live scanner — real-time call streaming over WebSocket with hold/avoid, per-user talkgroup selection, bookmarks, call sharing, and live transcript display.
  • Full admin dashboard — manage users, systems, talkgroups, units, groups, tags, API keys, directory monitors, downstreams, shared links, transcription, and settings.
  • Built-in transcription — optional integration with a go-whisper sidecar for automatic call transcription with model management from the UI, speaker diarization, 15 languages, and GPU acceleration.
  • Multi-recorder ingest — HTTP upload plus directory monitoring with native support for Trunk-Recorder, SDRTrunk, DSDPlus, RTLSDR-Airband, ProScan, and generic mask-based sources.
  • Auto-populate — systems, talkgroups, groups, tags, and units created automatically from incoming call metadata.
  • Audio processing — FFmpeg conversion with four modes (disabled, enabled, normalize, loudnorm) and 8 encoding presets across MP3, AAC-LC, and HE-AAC.
  • Call archive — search by system, talkgroup, group, tag, date range, transcript text, or bookmark state, with virtualized lists for large result sets.
  • Single binary deployment — embedded SQLite (WAL mode), no external database, pre-built Docker image, and a guided openscanner setup --interactive for bare-metal installs on Linux, macOS, and Windows.

Security

  • JWT authentication with refresh-token rotation and family revocation on reuse
  • bcrypt password hashing (cost ≥ 12)
  • Role-based access control (admin / listener)
  • API-key auth for uploads (X-API-Key header or ?key= query)
  • Per-IP login rate limiting with 3-strike account lockout
  • Per-user share-creation rate limiting
  • Per-API-key sliding-window upload rate limiting
  • WebSocket session revalidation with forced disconnect on user disable/delete
  • Optional AES-256-GCM secrets-at-rest encryption for the JWT signing secret and downstream API keys
  • Optional TLS with cert files (+ experimental Let's Encrypt auto-cert)
  • Hardened outbound HTTP client for transcription/downstream traffic: redirects disabled, timeouts enforced, response bodies capped; private-network targets permitted by default (homelab-friendly) and gateable with OPENSCANNER_BLOCK_INTERNAL_HTTP=1

What's new vs. rdio-scanner

OpenScanner is a complete rewrite, not a fork. A few things that didn't exist before:

  • Automatic transcription with GPU support, model management, live display, and search
  • Auto-populate for talkgroups, groups, tags, and units — not just systems
  • Public call sharing with configurable link expiry
  • Bookmarks and a filterable archive
  • Named user accounts with per-user talkgroup selection, session limits, expiration, and password-change enforcement
  • Per-API-key rate limits
  • Full CSV import/export for talkgroups and units
  • JSON configuration backup/restore
  • Dark/light theme toggle
  • RadioReference metadata preview and import
  • Guided setup, upgrade, config validate, and service doctor commands
  • Real-time admin operations over WebSocket (no REST polling)
  • Optional secrets-at-rest encryption

Install

Docker Compose (recommended):

docker compose up -d

Then browse to http://localhost:3022 and complete first-run setup.

Docker image: ghcr.io/revtex/openscanner:1.0.0 (also tagged 1.0 and latest).

From source:

make build
./build/openscanner --listen 0.0.0.0:3022 \
  --db-file ./data/openscanner.db \
  --recordings-dir ./data/recordings

Documentation

Known limitations

  • Let's Encrypt auto-cert is experimental and not yet exercised in production
  • Downstream forwarding between OpenScanner instances is experimental and untested
  • Transcription requires a separately deployed go-whisper sidecar (see the deployment guide)

Feedback

Issues and feature requests welcome at github.com/revtex/OpenScanner/issues.