Skip to content

auth: LIFTOFF_REFRESH_TOKEN headless path (#55)#56

Open
DTTerastar wants to merge 1 commit into
mainfrom
feat/headless-refresh-token
Open

auth: LIFTOFF_REFRESH_TOKEN headless path (#55)#56
DTTerastar wants to merge 1 commit into
mainfrom
feat/headless-refresh-token

Conversation

@DTTerastar

Copy link
Copy Markdown
Collaborator

Addresses the first half of #55.

Problem

auth login POSTs email + password to user.signIn. Accounts created via Google Sign-In have no password on the backend, and the backend explicitly refuses to provision one ("Your account was created with Google, simply login with Google!"). Those accounts have no working path to auth login, auth refresh, auth status, or any export subcommand.

Separately, liftoff-export had no headless auth path at all, which is a standing gap against common CONTRACT §5: "every CLI accepts environment variables that let a fresh container run without an interactive login. The variable names are {SERVICE}_*." Hence LIFTOFF_REFRESH_TOKEN, matching WITHINGS_REFRESH_TOKEN.

Change

Set LIFTOFF_REFRESH_TOKEN and it takes precedence over the saved token file, which is then neither read nor written. Not persisting is deliberate: writing would clobber the tokens of whoever is interactively logged in on the same machine. It costs one refresh call per invocation, which is the right trade for a CLI that runs once per export.

The refresh token is the right thing to inject rather than an access token — user.refreshToken needs no password, so one injection keeps working instead of expiring in an hour.

Mechanically: refresh() is split out of Refresh() so the headless path can exchange tokens without persisting. Refresh() keeps save-on-success for the interactive path. auth status reports the headless source, and the "not logged in" error now names both recovery paths.

Verification

Exercised against a real account with HOME pointed at a nonexistent directory, so no token file could be read:

$ HOME=/tmp/nohome LIFTOFF_REFRESH_TOKEN="$RT" liftoff-export bodyweights list --since 3d --format json
[
  { "date": "2026-07-19", "weight": 236 },
  { "date": "2026-07-20", "weight": 236 }
]

The logged-in user's auth.json was byte-identical (sha1) before and after. Also checked:

  • LIFTOFF_REFRESH_TOKEN=fake auth statususing LIFTOFF_REFRESH_TOKEN (headless; no saved token consulted), exit 0
  • LIFTOFF_REFRESH_TOKEN=bogus workouts listError: LIFTOFF_REFRESH_TOKEN refresh failed: refresh failed: Session not found, exit 1
  • unset → unchanged behavior, logged in (token expires ...)

Two tests cover the auth status fork with HOME redirected to a temp dir. go vet ./... and go test ./... pass.

What this does NOT fix

This does not by itself unblock the reporter of #55. An env var only helps someone who already has a refresh token, and a Google-only account still has no way to obtain one short of a mitmproxy capture against the phone app — a heavy ask for someone on Windows running a downloaded zip.

The actual fix is an OAuth path. There's a promising thread: Login() hardcodes "provider": "gymbros" in the user.signIn payload, which strongly implies the backend accepts "google" with some credential — plausibly a Google ID token we could mint via a loopback flow, since installed-app client IDs carry no secret. That's blocked on knowing the exact payload shape for a Google sign-in, which needs a capture from someone who has such an account.

So this PR is worth landing on its own merits (contract compliance, CI/container use, and an escape hatch for anyone who can capture a token), but #55 should stay open.

🤖 Generated with Claude Code

https://claude.ai/code/session_013JJnPtEtP97to9Rv2QMoer

@DTTerastar
DTTerastar force-pushed the feat/headless-refresh-token branch from fb71ffa to 10b9838 Compare July 21, 2026 02:49
Google Sign-In accounts have no password, so 'auth login' — which POSTs
email+password to user.signIn — has no credential pair to send and the
backend rejects password reset for them outright. Those accounts had no
working path to any authenticated subcommand.

Also closes a standing gap against quantcli/common CONTRACT §5, which
requires every CLI to accept {SERVICE}_* env vars so a fresh container
can export without an interactive login. liftoff-export had no headless
path at all.

Set LIFTOFF_REFRESH_TOKEN and the saved token file is bypassed entirely,
neither read nor written — writing would clobber the tokens of whoever
is logged in on the same machine. Costs one refresh call per invocation,
which is the right trade for a CLI that runs once per export.

Splits the unexported refresh() out of Refresh() so the headless path can
exchange tokens without persisting. Refresh() keeps its save-on-success
behavior for the interactive path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013JJnPtEtP97to9Rv2QMoer
@DTTerastar
DTTerastar force-pushed the feat/headless-refresh-token branch from 10b9838 to 684ef8f Compare July 21, 2026 02:57
@quantcli quantcli deleted a comment from github-actions Bot Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Try this PR without building it

Review binaries for commit 684ef8f — direct download, no login required:

Platform Download
macOS (Intel) liftoff-export-darwin-amd64.zip
macOS (Apple silicon) liftoff-export-darwin-arm64.zip
Linux x86_64 liftoff-export-linux-amd64.zip
Linux arm64 liftoff-export-linux-arm64.zip
Windows x86_64 liftoff-export-windows-amd64.zip

Unzip, then on macOS/Linux chmod +x the binary before running. macOS Gatekeeper blocks unsigned binaries on first run — right-click → Open, or xattr -d com.apple.quarantine <file>.

Links are resolved by nightly.link, a third-party redirector for GitHub Actions artifacts, because GitHub has no anonymous artifact URL of its own. Signed-in users can also browse the run directly. Artifacts expire 14 days after the run. Updated on each push to this PR.

@DTTerastar

Copy link
Copy Markdown
Collaborator Author

Try this PR without building it

Review binaries for commit 684ef8f — direct download, no login required:

Platform Download
macOS (Intel) liftoff-export-darwin-amd64.zip
macOS (Apple silicon) liftoff-export-darwin-arm64.zip
Linux x86_64 liftoff-export-linux-amd64.zip
Linux arm64 liftoff-export-linux-arm64.zip
Windows x86_64 liftoff-export-windows-amd64.zip
Unzip, then on macOS/Linux chmod +x the binary before running. macOS Gatekeeper blocks unsigned binaries on first run — right-click → Open, or xattr -d com.apple.quarantine <file>.

Links are resolved by nightly.link, a third-party redirector for GitHub Actions artifacts, because GitHub has no anonymous artifact URL of its own. Signed-in users can also browse the run directly. Artifacts expire 14 days after the run. Updated on each push to this PR.

Please test @KushH305

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