Skip to content

fix: OAuth auth, gzip decompression, and adjustable base endpoints#22

Open
mmto-io wants to merge 1 commit intosteipete:mainfrom
mmto-io:fix/auth-gzip-base-v2
Open

fix: OAuth auth, gzip decompression, and adjustable base endpoints#22
mmto-io wants to merge 1 commit intosteipete:mainfrom
mmto-io:fix/auth-gzip-base-v2

Conversation

@mmto-io
Copy link

@mmto-io mmto-io commented Feb 28, 2026

PR: Fix auth, gzip handling, and base/motor API endpoints

Title

fix: OAuth auth, gzip decompression, and adjustable base endpoints

Description

Several bugs prevent eightctl from authenticating and controlling the adjustable base on current Eight Sleep firmware (tested on a TriMix v5 pod, firmware as of Feb 2026). This PR fixes all of them.

1. OAuth token endpoint sends empty client_secret (critical)

Bug: authTokenEndpoint() hardcodes "client_id": "sleep-client" and "client_secret": "" instead of using c.ClientID and c.ClientSecret, which are correctly populated from defaultClientID/defaultClientSecret in New().

Result: Every OAuth attempt returns 400 Bad Request. The client falls back to the legacy /login endpoint, which rate-limits aggressively (429).

Fix: Two-line change — use c.ClientID and c.ClientSecret from the struct.

2. Gzip responses not decompressed

Bug: do() sets Accept-Encoding: gzip but never decompresses gzip responses. When the API responds with Content-Encoding: gzip, raw gzip bytes are passed to json.Decode(), causing parse errors.

Fix: Check Content-Encoding header in do(), wrap resp.Body in gzip.NewReader when needed. The decompressed reader is used for both error responses and JSON decoding.

3. Base/motor endpoints use wrong API host

Bug: All base endpoints (/users/{id}/base, /users/{id}/base/angle, /users/{id}/base/presets, /devices/{id}/vibration-test) are served by app-api.8slp.net, not client-api.8slp.net. The client-api host returns 404 for these paths.

Fix: Add AppBaseURL field to Client (defaults to https://app-api.8slp.net/v1), add a doApp() helper that temporarily swaps the base URL for the request, and update all base endpoints to use doApp().

4. SetAngle sends wrong field names

Bug: SetAngle() sends {"head": N, "foot": N} but the API expects {"torsoAngle": N, "legAngle": N}. The request succeeds (200) but the bed doesn't move.

Fix: Use the correct field names.

5. RunPreset doesn't actually move the bed

Bug: RunPreset() does POST /base/presets with {"name": "relaxing"}, which updates the UI preset label on the server but doesn't command the motors. The actual motor endpoint is POST /base/angle with torso/leg angles.

Fix: RunPreset now fetches the presets list, resolves the matching preset's angles, and calls SetAngle() to physically move the base. Preset resolution follows this priority:

  1. User's custom variant (name-custom) with non-zero angles
  2. Exact name match with non-zero angles
  3. First sub-preset (metaOf == name) with non-zero angles
  4. Exact name match even at 0/0 (e.g. "flat")

CLI improvements (minor)

  • base angle and base preset-run now accept positional arguments (eightctl base angle 20 10 or eightctl base preset-run relaxing) in addition to flags
  • base preset-run validates that a name is provided

Files changed

  • internal/client/eightsleep.go — auth fix, gzip decompression, doApp() helper, AppBaseURL field
  • internal/client/base.go — correct field names, correct API host, preset resolution logic
  • internal/cmd/base.go — positional arg support for angle and preset-run

Testing

  • Tested on a real Eight Sleep Pod (TriMix v5)
  • OAuth authentication now succeeds on first attempt (no 400, no fallback to legacy login)
  • eightctl base info returns base status (was 404)
  • eightctl base angle 20 10 physically moves the bed
  • eightctl base preset-run relaxing resolves angles from preset list and moves the bed
  • eightctl base presets lists all presets with correct angles
  • Existing unit tests pass (go test ./...)

Several bugs prevent eightctl from authenticating and controlling the
adjustable base on current Eight Sleep firmware.

1. authTokenEndpoint() hardcodes empty client_secret instead of using
   c.ClientID/c.ClientSecret from the struct. Every OAuth attempt
   returns 400, falling back to legacy login which rate-limits (429).

2. do() sets Accept-Encoding: gzip but never decompresses responses.
   Raw gzip bytes fed to json.Decode() cause parse errors.

3. Base endpoints are served by app-api.8slp.net, not client-api.
   Add AppBaseURL field and doApp() helper for base commands.

4. SetAngle sends {head, foot} but API expects {torsoAngle, legAngle}.

5. RunPreset only updates the UI label server-side without commanding
   motors. Now fetches preset angles and calls SetAngle to move.

Tested on TriMix v5 pod. All existing tests pass.
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.

1 participant