diff --git a/docs/content/docs/reference/SPEAKER-ENDPOINT.md b/docs/content/docs/reference/SPEAKER-ENDPOINT.md index 3a7d977..8edd7c0 100644 --- a/docs/content/docs/reference/SPEAKER-ENDPOINT.md +++ b/docs/content/docs/reference/SPEAKER-ENDPOINT.md @@ -339,9 +339,15 @@ box. soundtouch-web proxies it to the service, so start it with `--service-url` ### Notes and limitations -- **The `app_key` still applies.** Cloud TTS does not bypass the `/speaker` - requirement; without a working `app_key` the speaker will reject playback. -- **Model support** is the same as the direct path (primarily ST-10 Series III). +- **`app_key` validation is handled automatically (speaker method).** The + speaker validates the key by calling `GET /v1/auth` on Bose's audio + notification host (`audionotification.api.bosecm.com`, and a `…dev…` variant), + which AfterTouch intercepts (DNS substring match on `bosecm.com`, plus + `/etc/hosts` seeding during migration) and answers `200`. So any non-empty + `app_key` works; you don't need a real Bose-issued key. The `radio` method + needs no `app_key` at all. +- **Model support** for the `speaker` method is the same as the direct `/speaker` + path (primarily ST-10 Series III). Use `--method radio` on models without it. - **Reachability:** the speaker must be able to reach the service's `/media/tts/{id}` URL. The service builds it from its configured `server-url`. - Synthesized clips are cached in memory for a short time and identical requests diff --git a/pkg/service/handlers/handlers_setup_test.go b/pkg/service/handlers/handlers_setup_test.go index c40e983..f9ecf18 100644 --- a/pkg/service/handlers/handlers_setup_test.go +++ b/pkg/service/handlers/handlers_setup_test.go @@ -412,7 +412,7 @@ func (m *mockSSH) Run(command string) (string, error) { m.runCount++ if m.runCount > 1 { // Return updated hosts for verification - return "127.0.0.1 localhost\n192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil + return "127.0.0.1 localhost\n192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\taudionotification.api.bosecm.com\n192.0.2.100\taudionotificationdev.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil } return "127.0.0.1 localhost", nil } diff --git a/pkg/service/setup/setup.go b/pkg/service/setup/setup.go index 83424e5..bb6e51e 100644 --- a/pkg/service/setup/setup.go +++ b/pkg/service/setup/setup.go @@ -426,6 +426,13 @@ func (m *Manager) populatePlannedNetworkConfig(summary *MigrationSummary, _, tar "bmx.bose.com", "content.api.bose.io", "events.api.bosecm.com", + // app_key validation for /speaker audio notifications (TTS). Without + // these redirects the speaker validates against the dead Bose cloud and + // reports an invalid app key. Both the prod and dev hosts are seeded + // (firmware may use either). DNS interception already covers them via + // the bosecm.com substring; seed here for /etc/hosts migrations too. + "audionotification.api.bosecm.com", + "audionotificationdev.api.bosecm.com", "bose-prod.apigee.net", "worldwide.bose.com", "music.api.bose.com", @@ -1424,6 +1431,13 @@ func (m *Manager) migrateViaHosts(deviceIP, targetURL string) (string, error) { "bmx.bose.com", "content.api.bose.io", "events.api.bosecm.com", + // app_key validation for /speaker audio notifications (TTS). Without + // these redirects the speaker validates against the dead Bose cloud and + // reports an invalid app key. Both the prod and dev hosts are seeded + // (firmware may use either). DNS interception already covers them via + // the bosecm.com substring; seed here for /etc/hosts migrations too. + "audionotification.api.bosecm.com", + "audionotificationdev.api.bosecm.com", "bose-prod.apigee.net", "worldwide.bose.com", "media.bose.io", diff --git a/pkg/service/setup/setup_test.go b/pkg/service/setup/setup_test.go index 054de86..7a1a186 100644 --- a/pkg/service/setup/setup_test.go +++ b/pkg/service/setup/setup_test.go @@ -154,7 +154,7 @@ func TestMigrateViaHosts(t *testing.T) { if command == "cat /etc/hosts" { // Handle both initial read and verification read if len(runCalls) > 2 { // Rough heuristic: verification happens after upload - return "192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil + return "192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\taudionotification.api.bosecm.com\n192.0.2.100\taudionotificationdev.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil } return "127.0.0.1 localhost", nil } @@ -232,7 +232,7 @@ func TestMigrateViaHosts_UpdateExisting(t *testing.T) { runCount++ if command == "cat /etc/hosts" { if runCount > 1 { - return "127.0.0.1 localhost\n192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil + return "127.0.0.1 localhost\n192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\taudionotification.api.bosecm.com\n192.0.2.100\taudionotificationdev.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil } return "127.0.0.1 localhost\n1.2.3.4\tstreaming.bose.com\n1.2.3.4\tupdates.bose.com", nil } @@ -771,7 +771,7 @@ func TestMigrateViaHosts_SkipCAIfTrusted(t *testing.T) { if command == "cat /etc/hosts" { // Handle both initial read and verification read if len(runCalls) > 2 { // Rough heuristic: verification happens after upload - return "192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil + return "192.0.2.100\tstreaming.bose.com\n192.0.2.100\tupdates.bose.com\n192.0.2.100\tstats.bose.com\n192.0.2.100\tbmx.bose.com\n192.0.2.100\tcontent.api.bose.io\n192.0.2.100\tevents.api.bosecm.com\n192.0.2.100\taudionotification.api.bosecm.com\n192.0.2.100\taudionotificationdev.api.bosecm.com\n192.0.2.100\tbose-prod.apigee.net\n192.0.2.100\tworldwide.bose.com\n192.0.2.100\tmedia.bose.io\n192.0.2.100\tdownloads.bose.com\n192.0.2.100\tvoice.api.bose.io", nil } return "127.0.0.1 localhost", nil }