auth: honor WITHINGS_REFRESH_TOKEN for headless use#43
Merged
Conversation
GetToken only ever read ~/.config/withings-export/auth.json, so a CI or container run with no interactive 'auth login' failed with 'not logged in' even when WITHINGS_REFRESH_TOKEN was set — the contract (§5) already advertises that var, and crono/liftoff honor it. Add an env fallback: when no token file is present, build the store from WITHINGS_REFRESH_TOKEN + WITHINGS_CLIENT_ID/SECRET and mint an access token via the existing refresh path. 'auth status' reports the env path; prime and README document it. Note the Withings refresh-token rotation caveat for long-running headless callers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fatal Two fixes to the headless path, both from reviewing it against liftoff-export-cli, which shipped this feature first. Precedence was inverted. liftoff checks LIFTOFF_REFRESH_TOKEN before the token file (internal/auth/auth.go:84); this checked the file first and used the env var only as a fallback. Same variable pattern, opposite resolution: on a machine with a saved login, WITHINGS_REFRESH_TOKEN=… was silently ignored here while the identical liftoff invocation honored it. quantcli/ common#28 rules the environment wins; GetToken and `auth status` now do. A failed token-file write was fatal, in the exact environment this feature targets. refresh() ended with `return save(store)`, so a successfully minted access token still surfaced as "token refresh failed: …" whenever the cache could not be written — read-only rootfs, distroless, or no HOME (configPath swallows the UserHomeDir error and writes a *relative* path into CWD). The write is now best-effort: a warning on stderr, exit 0, per CONTRACT.md §4/§5. Unlike liftoff the headless path still persists, because Withings rotates the refresh token on every use — without the write-back the injected secret is unrecoverable after one run. The trade-off (on a shared machine this overwrites the interactive user's token) is noted at the call site, and the README now says that losing the write costs you the rotated token. Tests: the refresh path was previously untestable without rotating the caller's real token, so tokenURL becomes a var an httptest server can replace. That buys hermetic coverage of both fixes — env-wins-over-a-valid- saved-token (asserting the rotated value reaches disk) and survives-an-unwritable-token-file — plus cmd/auth_test.go for the three `auth status` branches, mirroring liftoff's. Also drops `auth refresh` from README and prime: init() only ever registered login/logout/status, so the documented command fell through to help. Refs quantcli/common#28 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo
govulncheck fails on the 1.25.10 standard library: GO-2026-5037 (crypto/x509 hostname parsing), GO-2026-5039 (net/textproto error escaping), and GO-2026-5856 (crypto/tls), the last fixed only in 1.25.12. All are reached through pre-existing call paths (client.Call, auth.Login) and are unrelated to the auth change on this branch — main last ran green on 2026-05-25, before these were published, so it would fail there too today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo
DTTerastar
force-pushed
the
headless-refresh-token
branch
from
July 24, 2026 01:37
94e2ce7 to
c82de14
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
GetToken()only read~/.config/withings-export/auth.json, so headless runs (CI, containers) with no interactiveauth loginfailed withnot logged in — run: withings-export auth logineven whenWITHINGS_REFRESH_TOKENwas set. CONTRACT.md §5 already advertises that var as the headless path, and crono/liftoff honor it — withings was the outlier. (Found it failing in the quantified dashboard's data-refresh CI.)Change
GetToken(): resolveWITHINGS_REFRESH_TOKENbefore the token file, building a store from it plusWITHINGS_CLIENT_ID/SECRETand minting an access token through the existingrefresh()path.auth status: same order — reportsusing WITHINGS_REFRESH_TOKEN (headless; no saved token consulted), still no network call.refresh(): the token-file write is now best-effort.prime+ README document the headless var and its precedence.auth statusbranches.Precedence: env wins
The first draft of this PR had the file win and the env var act as a fallback. That was wrong on consistency grounds: liftoff already checks
LIFTOFF_REFRESH_TOKENbefore the token file (internal/auth/auth.go:84), so the same variable pattern would have resolved in opposite directions across two CLIs —WITHINGS_REFRESH_TOKEN=… withings-export …silently ignoring the variable on a machine with a saved login, while the identical liftoff invocation honored it. quantcli/common#28 rules the environment wins; this now matches.A failed cache write is no longer fatal
refresh()ended withreturn save(store), so a successfully minted access token still surfaced astoken refresh failed: …whenever the token file could not be written — read-only rootfs, distroless, or noHOME(configPath()swallows theUserHomeDirerror and writes a relative.config/withings-export/auth.jsoninto CWD). That is exactly the environment this PR exists to serve. The write is now best-effort: warning on stderr, exit 0, per §4/§5.Rotation
Unlike liftoff, the headless path here still persists, because Withings rotates the refresh token on every use — without the write-back the injected secret is unrecoverable after one run. Two consequences, both documented rather than solved:
auth.json. Noted at the call site.A static
WITHINGS_REFRESH_TOKENremains single-use; the quantified workflow captures the rotated value and rewrites the secret.Drive-by
auth refreshis gone from README andprime—init()only ever registered login/logout/status, so the documented command fell through to help.Test
gofmt/go build ./.../go vet ./.../go test ./...green, and the-tags=compatconformance suite still passes against a fresh build.The refresh path was previously untestable without rotating my own real token, so
tokenURLbecomes avaranhttptestserver can replace. That buys hermetic coverage of both fixes:TestGetToken_EnvWinsOverSavedToken— saved token is valid and unexpired, so it would be used if the file won; also asserts the rotated value reaches disk.TestGetToken_HeadlessSurvivesUnwritableTokenFile— config dir chmod0500, mint succeeds, warning on stderr, no error. Skips if permissions aren't enforced (root).cmd/auth_test.go— the threeauth statusbranches, mirroring liftoff's.Precedence also verified end-to-end against my real (expired)
auth.json, no network:Live E2E is still the quantified CI run once released — deliberately not run here, since a live refresh would rotate and invalidate my own local token.
🤖 Generated with Claude Code
https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo