The old file documented a single anonymisation pass and embedded the
exact historical mappings (real LAN IPs, real MACs, real account IDs
on the "Original" side of each row). Those values are sensitive even
when presented as "what we replaced" — and they're already in git
history, so reprinting them in tracked content adds nothing.
Replaced with a concise reference that:
- lists the canonical placeholders to USE in new examples and tests
(RFC-5737 IPs, AA:BB:CC:DD:EE:FF MACs, generic device names,
1000001/1000002 account IDs)
- explains why RFC-5737 instead of 192.168.1.x
- gives detection regexes that catch *any* non-placeholder value,
rather than naming the specific leaked values
180 → 65 lines net, and the file no longer contains any of the
sensitive strings it used to track.
Completes the .md / .txt portion of the rfc-5737-cleanup branch.
Test files (.go / .xml / .http) + the convert_mitm_script.py and
the broader 192.168.1.* sweep remain — separate scope per
_/RFC-5737-cleanup/assessment.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two real Bose customer account IDs were embedded in documentation
examples: 3230304 (16 files repo-wide, 5 of them .md/.txt) and
9569497 (2 files, 1 .md). Account IDs look numeric and innocuous but
they're tied to a specific Bose customer — same exposure class as
MACs and home-LAN IPs.
Mapping:
3230304 → 1000001
9569497 → 1000002
6 .md files touched in this commit. Remaining occurrences live in
test files and one Python script (scripts/convert_mitm_script.py) —
those are out-of-scope for the docs sweep and will be handled in a
dedicated test-fixtures commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The maintainer's two test-speaker MACs (A81B6A536A98 / A81B6A849D99,
plus colon-separated forms) appeared throughout documentation, runbooks,
and example READMEs. Public repo — same hygiene argument as the LAN-IP
sweep in 787c4fa.
Mapping:
A81B6A536A98 → AABBCCDDEEFF
A81B6A849D99 → AABBCCDDEE01
A8:1B:6A:53:6A:98 → AA:BB:CC:DD:EE:FF
A8:1B:6A:84:9D:99 → AA:BB:CC:DD:EE:01
The placeholders use the IANA-reserved AA:BB:CC:DD:EE:FF address that's
clearly synthetic, matching the convention the earlier anonymisation
pass had already adopted. 13 .md files touched; no tests, no code.
ANONYMIZATION-SUMMARY.md left for a dedicated rewrite commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Public-repo hygiene: docs and READMEs carried the maintainer's home
LAN range (192.168.178.x) and personal speaker names ("Sound
Machinechen", "A Sound Machine"). Swapped to RFC-5737 documentation
IPs (192.0.2.x — reserved for examples, won't collide with anyone's
real network) and generic names ("Living Room SoundTouch",
"Kitchen SoundTouch").
12 files touched, all .md / .txt documentation. No code or tests
changed in this commit; subsequent commits will address the
docs/analysis/ANONYMIZATION-SUMMARY.md mapping log and the wider
real-MAC/real-account-ID footprint surfaced by the audit at
_/RFC-5737-cleanup/assessment.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the proxy/mirror removal there is no proxy left in the service,
but the parallel partial-update endpoint /setup/proxy-settings stuck
around with its legacy name. It serves a legitimate purpose distinct
from the bulk /setup/settings POST: the three checkboxes
(Redact / Log Bodies / Record) use onchange-triggered live save,
while /setup/settings drives a Save-button form for dozens of fields.
Folding the two endpoints together would either lose the live-toggle
UX or send half-edited draft form data on every toggle, so the
partial-update endpoint earns its keep — it just needed the right
name.
Renamed symbols (no behaviour change):
Go handler funcs:
HandleGetProxySettings → HandleGetLoggingSettings
HandleUpdateProxySettings → HandleUpdateLoggingSettings
GetProxySettings → GetLoggingSettings
Route:
/setup/proxy-settings → /setup/logging-settings
JS:
fetchProxySettings() → fetchLoggingSettings()
updateProxySettings() → updateLoggingSettings()
HTML element IDs (cosmetic, kept consistent):
proxy-redact / proxy-log-body / proxy-record
→ logging-redact / logging-log-body / logging-record
HTML heading:
"Proxy Logging:" → "Logging:"
JSON payload shapes (request + response keys) are UNCHANGED: the
endpoint still emits / accepts {"redact", "log_body", "record"}.
Persisted Settings on disk are UNCHANGED. CLI flags are UNCHANGED.
Server struct fields redactLogs / logBodies / recordEnabled
(renamed earlier this session) are UNCHANGED.
testdata/router_routes.txt regenerated. go build clean. go test
./... clean except pre-existing TestDocsConsistency (untracked-file
issue, unrelated). golangci-lint 0 issues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Enabling the Stockholm UI" section listed the binary/env-var/Docker
forms but not the new dev-service-stockholm make target — which is the
shortest path through the local roundtrip and the one most contributors
will want.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The port guide was written when only v1 and v2 existed; today the
upstream krahl/soundcork-stockholm-app ships v1..v4. The Go code path
already scans dynamically (no hardcoded version list), so future
versions get picked up without code changes — only the documentation
was stale.
Update three spots:
- The patch-application section now notes the dynamic scan and lists
the four current versions with one-line summaries.
- The shell instructions for a plain-process install use a for-loop
over stockholm-changes_v*.patch instead of hardcoding v1 and v2.
- The "Patches summary" appendix gains v3 (now_play.js guard) and
v4 (app_comm.js clientId polish).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements pkg/service/stockholm with bridge (appSend/runQueue), HTTP
proxy, static serving, config URL rewriting, native state persistence,
and device discovery. Mounts under a configurable base path (/stockholm
by default) with correct http.StripPrefix routing and apiBase-prefixed
bridge API routes matching the patched JS window.__stockholmBase calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add three new entries to docs/guides/TROUBLESHOOTING.md so the next
reporter who hits these symptoms finds the answer without needing the
issue thread.
- "Every cloud source shows status=UNAVAILABLE / can't stream anything"
(Connection Issues). Three-step diagnostic checklist: :443
reachability preflight, margeAccountUUID check, filtered
`logread -f`. Distilled from the diagnostic ping on #224 plus
Thatboioofy's resolution (missing margeAccountUUID was the cause).
Sidebar clarifies that the firmware-internal placeholder sources
(SpotifyConnectUserName, SpotifyAlexaUserName, UPnPUserName,
StoredMusicUserName, QPlay{1,2}UserName, AirPlay2DefaultUserName)
are speaker-synthesized and their UNAVAILABLE status is never an
AfterTouch problem on its own.
- New section "Music Service & Preset Issues" with "Spotify preset
fails with 'Current content cannot be saved as preset'". Explains
the firmware-side isPresetable="false" gate on Connect-pushed
playback (foob61451's NowPlaying capture in #235), why an
OAuth-linked account flips it to true, and cross-links to
MUSIC-SERVICES.md and the new spotify-overview.md.
- "TuneIn (or Internet Radio) missing from /sources after a factory
reset". TuneIn is not a default source; the speaker only registers
it after first play. Captured from the #253 side-thread with both
app and `soundtouch-cli source content` recipes plus the
no-SSH caveat for newer hardware (SA-5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
spotify-oauth.md (and any future docs) embed mermaid sequence/flow
diagrams as fenced code blocks. Kramdown emits those as
<pre><code class="language-mermaid">, which is not what Mermaid's
auto-renderer looks for, so on the rendered site they show up as raw
code instead of diagrams.
Add docs/_includes/head-custom.html (a hook the pages-themes/minimal
remote theme already exposes) to load Mermaid 11 as an ES module from
jsDelivr, rewrite pre/code.language-mermaid nodes into div.mermaid, and
call mermaid.run() once.
No Jekyll plugin or _config.yml change needed — the include slot is
honoured by the remote theme as-is.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
storePreset on the speaker was failing with "AddPreset - failed due to
invalid SourceID" because the watchdog priming path only pushed ZeroConf
credentials and never registered a SPOTIFY ConfiguredSource in marge.
PrimeDeviceWithSpotify now:
- resolves the device's paired account via live :8090/info
(margeAccountUUID), falling back to ServiceDeviceInfo.AccountID — same
order as setup.populateDeviceInfo;
- writes a SPOTIFY ConfiguredSource under that account (providerID=15,
BoseSecret as credential), mirroring bridgeSpotifyToMarge;
- POSTs `<updates><sourcesUpdated/></updates>` so the speaker re-fetches
its on-device Sources.xml from marge.
Also introduce zeroconf.ErrAddUserNoOp for the narrow firmware quirk
(404 + empty body on ?action=addUser when activeUser already matches).
Recognised only on that exact pattern; real 4xx/5xx still surface loudly
with full response details. Same treatment applied to Amazon priming.
Docs:
- new docs/concepts/spotify-overview.md anchors the topic (mental model,
streamingoauth.bose.com DNS gotcha, token lifecycle, clientId notes,
troubleshooting table);
- spotify-oauth.md drops the removed install-primer endpoint and the
on-device boot-primer install sections, adds /mgmt/spotify/prime;
- spotify-priming-strategy.md and MUSIC-SERVICES.md link to the
overview.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The web-UI wizard is in-place migration: it preserves the speaker's
existing pairing and synced data. The CLI sequence is a different
shape — full factory-reset → wifi-push → pair against AfterTouch
from scratch — and it's the right tool when you want a clean,
scriptable, reproducible setup (automation, batched onboarding, or
just starting from a reset speaker).
Documents the full 6-step CLI flow (plan / factory-reset / wait-ap
/ wifi-push / wait-online / setup pair --mode=full), the verification
checks, and a side-by-side comparison so users can pick the right
path. Placed after "Repeat for each speaker" so the wizard remains
the recommended default for one-off migrations.
The flow assumes #195 and #269 are fixed in v0.80.2 — without the
AUX/sources filter, the CLI factory-reset path produces a speaker
where AUX won't dispatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Section 4 captures the presentation-only follow-up to #252: collapse
the two halves of a stereo pair into a single device-list entry using
each speaker's GET /getGroup metadata. Pair lifecycle (add/rename/remove)
already works end-to-end via pkg/client + soundtouch-cli, so this is
purely a soundtouch-web UI concern.
Drafted after BirdyBA's stereo-pair confirmation on the closed#252:
https://github.com/gesellix/Bose-SoundTouch/issues/252#issuecomment-4458140305
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the AfterTouch-side half of issue #234. After a factory reset
the speaker's /sources only lists the always-on local entries (AUX,
BLUETOOTH, AIRPLAY, NOTIFICATION, QPLAY, plus a SpotifyConnectUserName
placeholder); TUNEIN, LOCAL_INTERNET_RADIO, DEEZER, and linked
Spotify accounts are absent until the device receives the
<sourcesUpdated/> notification the reporter ran by hand. SyncDeviceData
now POSTs that notification as the final step, so users get the
visible-source-list recovery for free when they click Data Sync.
The other half — re-creating Marge.xml so playback resumes — is
already handled by the wizard's pair-account flow: it detects an
empty <margeAccountUUID/> in /info and prompts the user to pick a
known account or generate a new one. The wizard's pairing UI is
deliberately user-driven (the user picks the ID); the notification
nudge is purely automatic because there's no choice to make.
Implementation routes through the existing client surface rather
than reinventing it. setup.notifySpeakerSourcesUpdated delegates to
pkg/client.Client.NotifySourcesUpdated — the same path
handlers_mgmt.go already uses after music-service account changes
(handlers_mgmt.go:304, :637). The wire shape lives in one place
(pkg/models.NewSourcesUpdatedNotification). Fire-and-forget: a
notification failure logs but doesn't fail the sync.
Adjacent UX changes:
- docs/guides/TROUBLESHOOTING.md: new section "Presets flash then
revert to 'Select a preset' after a factory reset". Names the
symptom, the Marge.xml + reduced-/sources cause, and walks the
user through re-opening the Migration tab + Data Sync.
- pkg/service/handlers/web/js/script.js: devices list now renders
a "⚠ Not paired — re-pair" badge in the account-ID column for
speakers whose live /info reports an empty margeAccountUUID.
Clicking it opens the Migration tab pre-filled with that device,
surfacing the wizard's existing "Not paired (factory-reset or
never paired)" flow without making users discover it cold.
- pkg/service/testing/fakespeaker/testdata/info.xml: demo speaker
now reports margeAccountUUID=1234567 instead of the misleading
0000000 (which AfterTouch happens to accept as syntactically
valid but is not a documented sentinel anywhere — the convention
is empty for factory-reset, a real 7-digit number otherwise,
matching pkg/client/testdata/info_response_st{10,20}.xml).
Screenshots regenerated accordingly.
Test scaffolding:
- fakespeaker grows a POST /notification recorder that captures
body + Content-Type; tests assert on s.Notifications().
- TestIssue234_FactoryResetSpeakerSyncsReducedSources now drives
SyncDeviceData end-to-end (exercises the wiring) and asserts
the notification fires with the right deviceID and shape.
- TestFakeSpeakerNotificationRecorder pins the recorder contract
and the POST-only method gate.
Refs #234.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
make screenshots was producing artifacts: a ghost Spotify pill on
ui-devices, empty Plan-card URL inputs on ui-migration with cascading
"localhost" warnings, and "Checking configuration…" placeholder text
instead of "❌ Not configured" on ui-settings. Two root causes, fixed
together so the run is deterministic again.
1. Fakespeaker too thin for the post-wizard inspect pipeline. The new
migration wizard probes /supportedURLs and reads /networkInfo and
/sources alongside the existing /info, /presets, /recents. Those
routes now exist with sanitized fixtures (deviceID DEADBEEFCAFE,
loopback IPs, no real MACs or account IDs). The full group endpoint
set is also wired: /getGroup and /removeGroup return the empty
<group/> shape a real un-paired device emits; /addGroup and
/updateGroup echo the posted body with <status>GROUP_OK</status>
inserted before </group>, matching the success path documented in
issue #252. /supportedURLs lists everything the fake now serves so
any caller that probes capabilities first (e.g. marge_pairing.go)
sees a coherent picture. Tests cover the GET routes' XML roots, the
POST echo + GROUP_OK insertion contract, and /removeGroup's
GET-only contract (405 with Allow: GET on other methods).
2. run.sh seed hit a DNS cliff. The :443 preflight shipped in 3727ae6
resolves server_url on every /setup/settings call, and the
populatePlannedNetworkConfig step does it again. With the previous
seed of http://aftertouch.local:8000 each lookup burned ~5s on DNS
timeout, which compounded across the wizard calls and pushed
ui-migration past chromedp's 30s per-shot budget. Switched the seed
to http://aftertouch.localhost:8000 — RFC 6761 means *.localhost
resolves to loopback via the system resolver in milliseconds
(verified ~8ms on macOS / glibc / systemd-resolved) — so the brand-
friendly hostname survives in the captured PNGs without the
timeout. Manifest settle times bumped (ui-settings 300→2000ms,
ui-devices 500→2500ms, ui-sync 300→1000ms) to give fetchSettings +
fetchSpotifyStatus time to complete in headless Chrome.
While here, softened validateURL's loopback message to acknowledge the
on-device-install case (AfterTouch running on the speaker itself, where
loopback works) instead of unconditionally telling users they're
wrong. The validation still flags 127.0.0.1 / localhost since it's the
wrong answer 99% of the time, but the message now frames the
constraint rather than scolding.
docs/images/ui-*.png regenerated against the new pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the loopback-filtered command `logread -f | grep -v '127.0.0.1'` to
DEVICE-LOGGING.md's Pro-Tip section with a one-line rationale (strips
the speaker's in-device localhost chatter so cloud/AfterTouch attempts
are readable). Cross-link from the new Curl 7 entry in TROUBLESHOOTING
so users hitting that symptom find the SSH/logread how-to.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Speakers connect to Bose hostnames over implicit HTTPS (:443) while
AfterTouch's listener defaults to :8443. Without iptables / setcap /
reverse-proxy in front, the speaker side sees Curl 7 / connection
refused and AfterTouch's HTTP log stays silent — a recurring source
of confusion (see #214, #269).
Add a server-side probe (Check443Reachability) that dials both
localhost:443 and the DNS-resolved LAN IP on :443. Run it once at
service startup with a 2s timeout and emit a [WARN] log with the
exact iptables/setcap commands keyed to the configured listener port.
Expose the result via GET /setup/settings (with a shorter inline
timeout) so the web UI renders a ✅/❌ line next to Target Domain
and a complementary browser-side fetch probe — the browser sits on
the LAN exactly where speakers do, and timing-to-error distinguishes
TCP refused from TLS handshake started even with an untrusted CA.
Both the startup WARN and the UI row are gated on dns_enabled,
since :443 only matters for the DNS migration path; SDK-override
migration uses the port from the configured URL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the remaining feature gap between soundtouch-web and the
Stockholm app's local-control functionality (seek/scrub, queue view,
per-device settings) and the explicit non-goals (anything cloud-bound
that is either shut down or already handled by soundtouch-service).
Acts as both a contributor checklist and a public statement of what
the web UI will and won't try to cover.
Link the page under the Concepts section in SUMMARY.md so it shows up
in the published docs and satisfies the docs-consistency test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users on systems without a local telnet binary (modern macOS, Windows
without OptionalFeatures, minimal Linux distros) need a workable
recipe to reach the speaker's port-17000 shell. Add a one-line docker
run snippet that uses busybox-extras telnet inside an alpine
container, parameterised by the target speaker IP.
Placed at the top of the reference page so a reader who lands there
asking "how do I run telnet?" sees the fallback before the command
listings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `soundtouch-cli setup` subcommand group covering the full reset →
re-provision → pair lifecycle as a scriptable alternative to the web UI:
inspect, verify, plan, factory-reset, wait-ap, wifi-push, wait-online,
ssh-check, install-ca, migrate, reboot, pair (bare | full state machine)
Supporting library code lives in pkg/service/setup: factory_reset.go,
wifi_provision.go, inspect.go, init_plan.go, setup_session.go.
Confirmed against ST10 firmware 27.0.6 that bare setMargeAccount over
WebSocket — no SETUP_START/SETUP_ENTER/SETUP_LEAVE bracket — is
sufficient to pair a factory-reset speaker; the firmware materializes
SystemConfigurationDB.xml and Sources.xml itself and the pairing
survives reboot. Result and field-by-field SystemConfigurationDB
comparison documented in docs/analysis/SETUP-WEBSOCKET-EXPERIMENT.md.
Captures the device's pre-reset DELETE-to-marge plus its LAN peer
notification flow in docs/analysis/FACTORY-RESET-PROTOCOL.md.
Perf: batch GetMigrationSummary's SSH probes into one Run() call via
ssh_probe.go / ssh_probe_apply.go — was ~8 sequential dials at
500-1000 ms each on FW 27 crypto, now one round-trip. Same data shape,
same MigrationSummary fields populated.
Fixes /clockTime and /clockDisplay wire formats — firmware 27 rejects
the legacy flat XML ("Error parsing request"). ClockTimeRequest now
uses utcTime attribute; ClockDisplayRequest emits the nested
<clockConfig> envelope with timezoneInfo/timeFormat/brightnessLevel.
Removes cmd/example-init-speaker (superseded by setup pair).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refreshes docs/images/ui-{settings,devices,sync,migration}.png by
driving the web UI in chromedp against a synthetic speaker, so
documentation can be regenerated without real hardware and without
leaking personal data from the local network.
Three independent pieces:
- pkg/service/testing/fakespeaker — embeddable library serving the
HTTP and telnet surface the migration wizard probes (/info,
/presets, /recents and a getpdo CurrentSystemConfiguration reply
that places the device on the unmigrated happy path).
- cmd/dummy-speaker — thin CLI wrapping the library; self-registers
with a running service via POST /setup/devices.
- scripts/screenshots — chromedp runner driven by a JSON manifest;
decoupled from speaker/service setup so it can target any backend
URL. run.sh orchestrates a one-shot end-to-end capture and seeds
settings.json with a generic hostname plus discovery disabled to
keep real-network state out of the captures.
Captures are at DPR=2 for retina-sharp text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MIGRATION-GUIDE.md step 5 — replaces the "Telnet round-trip probe"
bullet with two honest variants: the new passive observer for
already-migrated speakers, and a skip-row explainer for not-yet-
migrated speakers pointing at the Apply + reboot cycle. The rollback
section drops the obsolete tangent about the probe step leaving
persisted URLs untouched (the probe no longer exists, and the wizard
already writes both layers).
TELNET-MIGRATION-METHOD.md — §9.4's pre-flight table swaps the
deprecated `POST /setup/telnet-probe` row for the new
`POST /setup/peer-probe` row plus a skip-explainer row for the
not-yet-migrated case. §9.5 gains a "REMOVED — see §9.8" header
pointer (the section is kept as historical record of what was
tried). §9.6's backend-additions table replaces the deleted
`probeRegistry` + `RunTelnetRoundTripProbe` + `/setup/telnet-probe`
row with the `peerObserver` + `RunPeerReachabilityProbe` +
`/setup/peer-probe` row that supersedes it.
NEXT.md is local-working-tree only (deliberately untracked) and
gains a ✅ Resolved header pointing at §9.8; not part of this
commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The swUpdate daemon caches its target URL at boot and ignores live
`sys configuration` writes, so the active flip in
RunTelnetRoundTripProbe never reaches the running daemon — confirmed
empirically on a fully-migrated speaker (FW 27.0.6) where both the
runtime and persistence layers were flipped and the device still
dialed the previously-cached `/updates/soundtouch` URL plus
DNS-intercepted `/streaming/software/update/account/*`. The probe URL
was never observed.
Marks DEPRECATED:
- pkg/service/setup/telnet_probe.go: ProbeRegistrar,
TelnetProbeResult, generateProbeToken, RunTelnetRoundTripProbe.
- pkg/service/handlers/handlers_telnet_probe.go: HandleTelnetProbe,
HandleProbeInbound, telnetProbeTimeout, telnetProbeResponse.
- pkg/service/handlers/probe_registry.go: probeRegistry.
- Server.probes field.
- /probe/{token}[/*] and /setup/telnet-probe/{deviceId} routes.
Adds §9.8 to docs/analysis/TELNET-MIGRATION-METHOD.md documenting the
daemon-cache finding, the diagnostic that confirmed it, the passive
observer replacement, the pre-flight branch on migration state, and
the canonical telnet flow (Apply config → reboot → passive
validation). All code symbols remain in place this commit; the
follow-up commit performs the hard delete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /setup/migrate/{deviceIP} reference table covered only the legacy
self/proxied/original mode selectors, with a one-line "Custom service
URL" mention of target_url. The wizard has been writing literal
per-field URLs via marge_url / stats_url / sw_update_url / bmx_url
for weeks; external API callers had nothing to read.
Expanded the table into three blocks with precedence rules:
1. Top-level params — method, target_url, proxy_url with the
four migration mechanisms (xml / telnet / resolv, hosts marked
deprecated).
2. Per-field implementation mode — the legacy self/proxied/original
family, kept for API back-compat with a note that the UI no
longer sets them.
3. Per-field literal URL overrides — marge_url / stats_url /
sw_update_url / bmx_url with a "literal wins over mode" rule
and the soundcork-suffix-propagates-to-envswitch note.
Three example curl invocations (canonical XML, soundcork telnet,
resolv with HTTPS) replace the old proxy=original-only snippet up
top.
Also added stub reference entries for POST /setup/telnet-probe and
the internal GET /probe/{token}[/*] catch-all — the SSH-less
reachability check the wizard runs automatically in its pre-flight
panel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The guide still described the pre-wizard UI: "SSH status, CA trust
status, and connection test results before letting you apply the
redirect" and two methods (XML / DNS). The migration tab now opens
with the state card + Plan card + Customize three-axis form + visible
pre-flight panel, and a third transport (Telnet:17000) lets users
without SSH access migrate too.
Updates:
- Step 3 retitled "Enable shell access on each speaker" with two
sub-sections: SSH (the richest option, required for XML / DNS /
CA install) and Telnet:17000 (the SSH-less fallback, no setup
required, HTTP-only).
- Step 5 rewritten to walk through the actual UI:
* the state card's three rows (Transports, Migration State,
Preconditions) with the action affordances inline
* the Plan card — target URL with Save as default, per-field
Service URLs editor with validation and soundcork-mode,
account pairing, and Apply Suggested Plan
* the visible pre-flight checks panel with its three or four
checks per method and the Proceed Anyway / Cancel branch
* Customize this migration with three independent axes
- Step 6 mentions the auto-expand of Customize on Apply success
and the per-transport reboot picking.
- Rollback section adds the telnet-only "reboot reverts the
runtime layer if envswitch isn't written" property, plus the
rename to "Revert to Defaults" matching the button label.
The image reference (ui-migration.png) stays pointing at the
existing screenshot; a fresh capture is needed once the wizard is
final but the surrounding prose is now accurate either way.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The feasibility analysis (§§1–8) was written before any of the wizard
shipped, and §7 forecast the surface area roughly. The migration tab
grew considerably during implementation — three-axis state model,
Plan card with per-field URL editor and validation, Customize
three-axis form, visible pre-flight panel, account pairing folded
into the wizard, and the SSH-less round-trip probe — none of which
the original §7 captures faithfully.
Added §9 "What actually shipped (post-implementation addendum)" with:
§9.1 Three-axis state model (per-axis migration booleans, IsPaired,
the state-card layout)
§9.2 Plan card per-field URL editor (single source of URL overrides
for both XML and Telnet, live optimistic preview)
§9.3 Customize three-axis form (URL flip / DNS / CA radios driving
applyCustomPlan)
§9.4 Pre-flight panel (visible check list, decision tree, override
affordances)
§9.5 Telnet round-trip probe (the SSH-less reachability check via
swUpdateUrl flip + :8090/swUpdateCheck trigger + probe-token
registry)
§9.6 Backend additions worth knowing (applyURLOverrides, parser,
option allow-list, telnet timeout bumps)
§9.7 Future probe candidates (pushCustomerSupportInfoToMarge;
running the round-trip probe on SSH-capable speakers too)
§§1–8 stay verbatim as the historical feasibility record, with a
forward-pointer at the head of §7 so readers know the as-shipped
state is documented further down.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire up X-Real-IP / X-Forwarded-For / True-Client-IP support for
deployments fronted by a reverse proxy, while staying safe on flat-LAN
deployments where a malicious speaker could spoof those headers
directly.
Two new fields on `datastore.Settings`:
* TrustForwardedHeaders (bool, default false) — opt-in switch.
* TrustedProxyCIDRs ([]string, default `["127.0.0.0/8", "::1/128"]`)
— only requests whose immediate TCP peer falls in one of these
blocks may have their source IP rewritten from forwarded headers.
Loopback default matches the documented same-host nginx layout in
docs/guides/HTTPS-SETUP.md.
New middleware in `pkg/service/handlers/middleware_realip.go`:
* TrustedRealIP wraps `chi/middleware.RealIP` with a trusted-peer
gate. When the immediate TCP peer is in the allowlist, chi's
parsing handles the actual header → IP rewrite. When it isn't
(e.g. a speaker sending forwarded headers itself), we ignore the
headers and r.RemoteAddr stays as-is.
* ParseTrustedProxyCIDRs converts string CIDRs into *net.IPNet,
applying the loopback default on empty input and erroring loudly
on invalid entries.
Server.TrustedRealIPMiddleware() returns the middleware (or nil) by
reading the live settings; the router setup in
cmd/soundtouch-service/main.go installs it as the very first
middleware so SnapshotMiddleware and downstream handlers see the
correct r.RemoteAddr.
HandleMargePowerOn now prefers r.RemoteAddr over the body's
self-reported `<IPAddress>` for outbound credential push:
* The body field is treated as a hint only — a malicious LAN speaker
could set it to any value; using it for outbound HTTP requests is
the SSRF surface the previous zeroconf hardening was guarding
against from the sink side. Fixing it at the source as well closes
the gap entirely.
* When body IP and TCP source disagree, a log line names both and
the device ID so the discrepancy is investigable.
* RemoteAddr is unparseable → fall back to the body so we don't
silently drop the priming.
docs/guides/HTTPS-SETUP.md gains a follow-up note next to the existing
nginx snippet explaining the new flag, the loopback-only default, and
the explicit warning against enabling the flag on a flat-LAN
deployment without a real proxy.
Eleven test cases in middleware_realip_test.go lock in the gate
behaviour: trusted peers honoured for X-Real-IP / X-Forwarded-For /
no-headers / IPv6, untrusted peers' headers ignored, garbage values
rejected, ParseTrustedProxyCIDRs covers default / override / invalid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Building on the strict literal-IP validator from the previous commit,
make the runtime error self-explanatory so anyone tripping on a
hostname URL can fix it in one shot:
* Errors now lead with the offending zeroconf URL and the rejected
host, so wrapping by GetInfo / PushCredentials / pushSimplifiedToken
doesn't bury the actual bad value.
* The "host must be a literal IP" error suggests two concrete one-liner
resolutions (`getent hosts <name>` and `dig +short <name>`) so the
user has a copy-paste fix.
* The "host is not on a local network" error names the accepted ranges
(loopback / RFC1918 private / link-local v4+v6) so the user knows
what they're allowed to pass.
docs/guides/SOUNDTOUCH-SERVICE.md gains a bullet under Security
Considerations explaining the constraint and the rationale (LAN-resident
SSRF surface), so the strict behaviour is documented rather than a
surprise.
The 17 TestValidateZcBaseURL cases still pass — only the message bodies
changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New section §8 records what is currently known about which devices and
firmware our migrateViaTelnet flow handles end-to-end, derived from the
six community sources catalogued in TELNET-COMMAND-REFERENCE.md plus our
issue threads.
* §8.1 — proven to work end-to-end (ST 10, 20, 300, Wave III, Wave IV on
FW 27.0.6 with multi-reporter agreement).
* §8.2 — proven to need the PairAccount telnet fallback (ST Portable,
BST20 Portable: /setMargeAccount missing or wedged on those firmware
builds).
* §8.3 — likely to fail (SA-5 on FW 9.x with the older shell generation;
newer ST Portable builds with shrunk command set). The preflight +
abort-on-first-rejection design ensures these fail cleanly, leaving no
half-configured state.
* §8.4 — unverified targets that are expected to work but lack concrete
captures (ST 30, ST 520, Wave Music System I/II).
* §8.5 — flags the apparent contradiction between S5's enumerated
"valid roots" on ST 10 / FW 27.0.6 (which omits envswitch) and #221's
successful envswitch use on the same firmware. Most plausible reading:
S5 is a non-exhaustive probe, not a negative claim; preflight catches
any real absence.
* §8.6 — maps every failure mode to its observable outcome and the unit
test that exercises it.
* §8.7 — TL;DR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Synthesises every Bose SoundTouch port-17000 telnet command we have evidence
for, across six community sources: flarn2006's 2014 root-shell post,
Sam Hobbs's 2016 ST 10 setup-mode walkthrough, izndgroup's 2021 reissue,
sijeffrey's 2017 `bose` remote-control script, the 2026 r/bose telnet
probing thread (FW 27.0.6 ST 10), and our own #221 / #236 / soundcork#141
findings.
Groups the commands by family — `key` (front-panel button emulation, the
addition the Reddit thread brought in), `network` (WiFi profile management),
`sys` (verbs + the XML-tag-keyed `sys configuration` setter our migration
uses), `envswitch` (parallel persistence layer), `getpdo` (PDO read), `scm`,
`ws`, `swupdate`, and the historic shell-unlock commands. Each entry notes
firmware-era availability so implementations know whether to expect
"Command not found" on newer builds.
Records the four top-level command roots that S5 confirmed reachable on a
vanilla FW 27.x ST 10 (`key`, `net`, `sys`, `getpdo`), and flags that
`envswitch` works on other ST 20 / Wave models running the same firmware
family — a per-model variation the migration's preflight already handles.
Cross-linked from TELNET-MIGRATION-METHOD.md §2 and indexed in SUMMARY.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an SSH-free third migration path that drives the SoundTouch device's
diagnostic shell on TCP port 17000, plus a hardened replacement for the
fragile /setMargeAccount HTTP pairing call.
* `pkg/telnet` — new reusable, dependency-free client (sibling of `pkg/ssh`)
with deadline-driven Dial / Probe / SendCommand / Close. Mock-server tests
cover happy path, command-not-found, mid-stream close, and the wedged-device
read-timeout scenario.
* `setup.MigrationMethodTelnet` — runs `sys configuration` for all four URLs
plus the parallel `envswitch boseurls set` persistence layer that otherwise
wins on reboot, then verifies with `getpdo CurrentSystemConfiguration`.
Aborts on the first non-OK response so configuration is never half-written.
No SSH backup or rw pre-flight (the path is SSH-free by design).
* `setup.PairAccount` — probes :8090/supportedURLs first, time-bounds
POST /setMargeAccount aggressively (5s connect / 12s total) to avoid the
hangs reported in #236, and falls back to `envswitch accountid set <id>`
over telnet when the HTTP endpoint is missing or wedged. Returns a
PairAccountResult breadcrumb so the UI can show which path actually
succeeded.
* `setup.Reboot(deviceIP, method)` — gains a RebootMethod selector;
RebootMethodSSH stays the default (preserving prior behavior),
RebootMethodTelnet sends `sys reboot` over a fresh telnet session and
treats the inevitable socket-close as success.
* New endpoints on `/setup`:
- GET /account-id-suggestions/{deviceId} — returns the device's current
margeAccountUUID (from :8090/info) plus known account IDs from the
datastore, so the UI can offer reuse.
- POST /pair-account/{deviceId}?account_id=NNNNNNN — invokes PairAccount;
the existing reboot endpoint reads ?method=ssh|telnet from the query
string.
* Helpers `IsValidAccountID` (exactly 7 digits) and `GenerateAccountID`
(crypto/rand, retries on collision against a known-IDs list).
Documentation in docs/analysis/TELNET-MIGRATION-METHOD.md is updated to match
the implementation: bare-URL convention for `soundtouch-service`, no automatic
`sys reboot` (user-initiated via the existing button with a method selector),
and the realised package layout. The /etc/hosts method is intentionally not
exposed in the new flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the SSH-free third migration path on top of the device's diagnostic
shell, synthesised from #221, #236, scheilch/opencloudtouch#167,
deborahgu/soundcork#228, and deborahgu/soundcork#141.
Captures the URL configuration command sequence, the dual persistence layers
(`sys configuration` + `envswitch boseurls set`), the `/setMargeAccount`
failure modes (404, hang, post-migration 502 on power_on) with their bounded
fallbacks, port-17000 preflight requirements, and account-ID sourcing rules
(reuse from `:8090/info`, pick from `DataStore.ListAccounts`, or 7-digit
manual/randomized entry). Cross-links the new doc from
DEVICE-REDIRECT-METHODS.md, marks the `/etc/hosts` method as deprecated, and
fixes the existing margeServerUrl example to use our service's bare-URL
convention with an explicit note for soundcork's `/marge` sub-path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Docker Compose (recommended for home servers and VMs)" section
to Step 1, pointing users to the existing docker-compose.yml and
.env.example. Clarifies the purpose of docker-compose.ci.yml (CI tests
only) and docker-compose.override.yml (local modifications, not in VCS).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- resolveIP now returns (string, error): error when result did not come
from the device's own SSH ping (service-side fallback or total
failure)
- migrateViaResolvConf and parseTargetURLAndResolveIP abort on error,
preventing a bad IP from being written to the device
- GetMigrationSummary captures the error in ResolveIPError and falls
back
to the hostname for the preview display; XML migration is unaffected
- Web UI shows a warning box with the error and a docs link when
resolution
is uncertain; migrate button stays enabled for the XML method
- Add hostname resolution troubleshooting section to TROUBLESHOOTING.md
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy 5 screenshots from _/screenshots/ into docs/images/ and wire them
into the migration guide (Settings, Devices, Sync, Migration tabs) and
the device initial setup guide (speaker AP mode Wi-Fi page). Replace the
images README wishlist with a table of what is actually present.
Also correct the AP mode IP address (192.0.2.1, verified on ST10) and
update the Settings step to match actual UI labels (Target Domain, DNS
Bind Address).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the placeholder MIGRATION-GUIDE.md (which had a "planned to be"
header, a nonexistent install.sh reference, and 9 broken screenshot links)
with a complete, image-free step-by-step walkthrough covering all 6 steps:
install, configure URL, enable SSH via USB stick, discover/sync, migrate
(XML or DNS/DHCP), and verify.
Add the Migration Guide to the README docs section and link to it from
the Survival Guide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrite README.md to be concise and tool-focused (no code snippets),
clearly presenting all five tools and their use cases. Expand the
soundtouch-service section to cover both user scenarios and redirect
method trade-offs.
Rewrite SURVIVAL-GUIDE.md around the same two scenarios with step-by-step
instructions. Remove deprecated hosts-file method from all user-facing
docs; update MIGRATION-SAFETY.md, HTTPS-SETUP.md, and SOUNDTOUCH-SERVICE.md
to reflect only the two supported methods (XML redirect and DNS/DHCP).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Endpoints:
- POST /streaming/music/musicprovider/{id}/trial/is_eligible (reuses
is_eligible handler)
- POST /bmx/tunein/v1/favorite/{stationID} with datastore persistence
(SaveTuneInFavorite)
- DELETE /bmx/tunein/v1/favorite/{stationID} (DeleteTuneInFavorite)
- POST /bmx/core02/svc-bmx-adapter-orion/prod/orion/token (anonymous
Orion token)
- GET /bmx-icons/* serving embedded static/media assets (media.bose.io)
- GET /ced/* serving embedded firmware index, release notes, and 10
app-help XMLs (downloads.bose.com)
Add media.bose.io and downloads.bose.com to DNS redirect lists (setup.go
both domain slices, dns.go shouldIntercept list, main.go getDomains
map). Document implemented endpoints in
tests/interactions_20260502_missing_external.md; mark rows 0246–0247 as
self/☑ in the interactions table.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CAPTURE-MIGRATION-TRAFFIC.md with step-by-step migration runbook
- Include session trace from first interactive ST10 migration run
- Genericize example IP addresses in BOSE-APP-ADB-Emulator.md and CAPTURE-DEVICE-PAIRING.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add scripts/android/ with setup-mitm-avd.sh (one-time) and start-mitm-session.sh (per-session)
- Move frida Dockerfile to scripts/android/; extract frida-server + SSL scripts via Docker
- Use native macOS mitmproxy app for capture (Docker NAT blocks emulator traffic)
- Add native-connect-hook.js to Frida launch — required for Bose app's native networking
- Document verified AP mode Wi-Fi provisioning endpoint (POST :8090/addWirelessProfile)
- Correct factory reset sequences for ST10/ST20 from official Bose guides
- Remove old scripts/setup-mitm-avd.sh and scripts/start-mitm-session.sh (moved to android/)
- Add session trace with lessons learned from first interactive capture run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a standalone `soundtouch-backup` CLI with three subcommands:
- `all`: authenticates with the Bose cloud, backs up account data, then
reads device IPs from devices.xml and backs up each reachable speaker
- `cloud`: fetches account profile, devices, sources, presets, and full
endpoint from streaming.bose.com
- `local`: backs up each speaker via HTTP API (12 endpoints) and
optionally via SSH (individual files + /opt/Bose/etc/ and
/mnt/nv/BoseApp-Persistence/1/ directories)
Also centralises pkg/service/ssh → pkg/ssh so both the service and the
backup tool share the same SSH client; adds ReadFile and ReadDir
methods, and handles the firmware quirk where cat exits 1 on empty
files.
Output is a single dated .tar.gz or .zip archive.
Example flow:
```shell
gesellix@Mac Bose-SoundTouch % go run ./cmd/soundtouch-backup all --output _/cloud-backup --email user@example.com
Password:
Authenticating as user@example.com...
✓ Authenticated (account ID: 1234567)
✓ email address (107 bytes)
✓ devices (1492 bytes)
✓ sources (1111 bytes)
✓ presets (2585 bytes)
✓ full account (55037 bytes)
Found 2 device(s) in cloud account, attempting local backup...
✓ ST20: 12 files via HTTP
⚠ ST20: SSH skipped /etc/remote_services (Process exited with status 1)
⚠ ST20: SSH empty file /mnt/nv/remote_services
✓ ST20: 64 files via SSH
✓ ST10: 12 files via HTTP
⚠ ST10: SSH empty file /etc/remote_services
⚠ ST10: SSH skipped /mnt/nv/remote_services (Process exited with status 1)
✓ ST10: 48 files via SSH
Archive written: _/cloud-backup/soundtouch-backup-2026-05-02.tar.gz (141 files)
```
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Attempting to request amazon_music:access with a standard application
client ID (amzn1.application-oa2-client.*) returns HTTP 400
lwa-invalid-parameter-bad-scope from the LWA authorization endpoint.
The scope is gated to Amazon Music partner device client IDs.
Revert scope to "profile" (working state) and document the confirmed
blocker with the exact error. Path forward: Amazon Music partner
registration for a device client ID; one-line change to AmazonScopes
when available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Amazon bridge: fall back to sync/legacy on any error from
SetMusicServiceOAuthAccount (not only error 1029); timeouts from
unresponsive speakers no longer silently skip the fallback chain
- Amazon bridge: reduce speaker client timeout from 30s to 5s for
faster failure on local network calls
- marge: resolveSourceName now prefers SourceName/DisplayName over
SourceKeyAccount, so Amazon (and Spotify) sources show the account
holder's name instead of the raw account ID
- docs: update amazon-music-oauth.md with real-world test results;
music-api.amazon.com returns 401 because standard LWA apps lack
music::* partner scopes — infrastructure is complete but streaming
is blocked pending Amazon partner access
- docs: add SELF-HOSTING.md and MUSIC-SERVICES.md user guides; link
both in SUMMARY.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mark status as Implemented
- Add "Trying It Out" section: LWA app setup, service flags, OAuth flow,
account verification, speaker priming, DNS requirement, site_id open question
- Fix stale endpoint table entry (no longer a stub)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>