Skip to content

Show each output's Sendspin pairing token on the Audio tab - #224

Merged
bbangert merged 2 commits into
mainfrom
feat/sendspin-pairing-token-ui
Oct 21, 2026
Merged

bbangert merged 2 commits into
mainfrom
feat/sendspin-pairing-token-ui

Conversation

@bbangert

Copy link
Copy Markdown
Owner

Summary

Since #222, every output's player offers Sendspin's pairing_psk pairing method, backed by a pairing key it saves in its state dir. Music Assistant 2.10.7 (current stable) already lets you pair such a client by pasting its SP:… pairing token. This PR makes that token visible, so an output can be paired with MA today without waiting for Home Assistant's automatic pairing (planned for HA 2026.11).

Outputs only: inputs still use the older wire format with PIN pairing, which has no pairing token.

Changes

  • Player (C++): after start(), writes client.pairing_token() to <state_dir>/pairing_token. The file is mode 0600, written atomically through the existing persistence path, and rewritten on every start. A missing token or failed write is non-fatal: it emits {"event":"error","kind":"pairing_token"} with no secret in the message, and the player keeps running.
  • Audio.PairingToken: reads and validates the file (SP: prefix, at most 127 bytes, printable ASCII). It returns :not_ready or :unavailable and never puts the file contents in an error.
  • Audio.pairing_token/1: follows the catch :exit idiom. Audio.Server.state_dir/2 comes from Player.state_dir/1, now public, so the path logic isn't duplicated.
  • Audio tab: the output card's details get a "Pairing token" row with Show/Hide and Copy, plus a hint to paste the token into MA's Sendspin "Pairing token" option.

Keeping the token secret

The token is a long-lived pairing secret that can't be revoked.

  • It never goes to stdout/stderr, PubSub or logs. A contract test runs the real binary at verbose log level and checks for this.
  • It's read on demand only, so it isn't in the page's HTML until the user clicks Show.
  • Copy re-reads it and pushes the copy event, so copying never puts it in the DOM.
  • It's dropped from assigns on Hide, when the details close, and when the output is removed.
  • While shown, it's held in a struct whose Inspect excludes the token, so a LiveView crash report can't print it.

Like the API encryption key on the Security tab, anyone who can open the web UI can reveal it.

Test plan

  • mise run test: 1687 passed, 0 failures. New coverage includes the contract test (0600 file, format, stable across a restart, never in output), pairing_token_test.exs, a check that no PubSub message carries the token, and LiveView tests (not in the HTML before Show, Copy pushes the event, not-ready hint, stale events ignored).
  • format, compile --warnings-as-errors, credo --strict, dialyzer; mix assets.build includes the new classes
  • On hardware: flash the rpi3, paste an output's token into MA 2.10.7 and confirm it pairs

🤖 Generated with Claude Code

The player writes its Pairing PSK token to a 0600 file in its state dir,
and the output card's details can reveal or copy it on demand for
pasting into Music Assistant's "Pairing token" option. The token never
goes to stdout, PubSub, logs or the page until the user asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Clipboard feedback reports success on unsupported HTTP origins, and token validation can accept truncated malformed files.

2 open issues
What changed in this PR

Exposes each output’s persisted Sendspin pairing token on the Audio tab for manual pairing with Music Assistant.

Changes:

  • Writes tokens privately through the player’s existing persistence path.
  • Adds on-demand token reading and validation APIs.
  • Adds Show/Hide/Copy controls and persistence, privacy, and UI tests.
File Description
test/​universal_proxy/​audio/​server_test.exs Tests output state-directory lookup.
test/​universal_proxy/​audio/​player_test.exs Checks token storage and PubSub privacy.
test/​universal_proxy/​audio/​pairing_token_test.exs Tests token validation and read errors.
test/​universal_proxy_web/​live/​audio_live_test.exs Tests disclosure, copying, and stale events.
test/​support/​fake_sendspin_player.py Simulates private token persistence.
test/​sendspin_player_contract_test.exs Tests real-binary persistence and privacy.
lib/​universal_proxy/​audio/​server.ex Exposes output state-directory lookup.
lib/​universal_proxy/​audio/​player.ex Publishes shared state-directory logic.
lib/​universal_proxy/​audio/​pairing_token.ex Reads and validates token files.
lib/​universal_proxy/​audio.ex Adds the public token API.
lib/​universal_proxy_web/​live/​audio_live.ex Adds token disclosure and copy controls.
c_src/​sendspin_player/​src/​main.cpp Persists the pairing token after startup.
c_src/​sendspin_player/​src/​file_persistence.h Declares private-file persistence API.
c_src/​sendspin_player/​src/​file_persistence.cpp Reuses atomic writes for token storage.
c_src/​sendspin_player/​README.md Documents token storage and pairing.

Review effort: Balanced

Comment thread lib/universal_proxy/audio/pairing_token.ex
Comment thread lib/universal_proxy_web/live/audio_live.ex Outdated
The device UI is plain HTTP, where navigator.clipboard is unavailable, so
copy falls back to execCommand and reports "Copied" only on success; the
pairing token is copied client-side once revealed. A token file that
fills the read buffer is now rejected before trimming.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Target-hardware pairing with Music Assistant remains unverified for this long-lived credential disclosure feature.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

@bbangert
bbangert merged commit e380770 into main Oct 21, 2026
8 checks passed
@bbangert
bbangert deleted the feat/sendspin-pairing-token-ui branch October 21, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants