docs(contract): §5 — headless env var wins over the saved token file#28
Merged
Conversation
§5 mandates a headless `{SERVICE}_*` variable but never said how it resolves
against a saved token file, so the two CLIs that have both landed on opposite
answers: liftoff lets the env var win, withings-export-cli#43 lets the file
win. Same variable pattern, same invocation, different behavior — exactly the
drift this contract exists to prevent.
Rules the environment wins, and pins the two consequences that fall out of it:
`auth status` reports the env source and exits 0 without a network call, and
for rotating upstreams the headless path writes the rotated token back
best-effort — a failed write is a stderr warning, not an error, because a
read-only rootfs is a normal container shape and a minted access token is
usable whether or not the cache write landed.
Verified against the family before writing it down: liftoff already conforms
(internal/auth/auth.go:84, env checked before load, never persisted); crono
has no token file, so precedence is vacuous there — hence the new n/a cell.
withings-export-cli#43 is the one that changes to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo
DTTerastar
added a commit
to quantcli/withings-export-cli
that referenced
this pull request
Jul 24, 2026
…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
DTTerastar
added a commit
to quantcli/withings-export-cli
that referenced
this pull request
Jul 24, 2026
* auth: honor WITHINGS_REFRESH_TOKEN for headless use 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> * auth: env wins over the saved token, and a failed cache write is not 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 * chore(security): bump go directive to 1.25.12 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 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
§5 requires a headless
{SERVICE}_*variable but says nothing about how it resolves against a saved token file. The two CLIs that have both have landed on opposite answers:internal/auth/auth.go:84, checked beforeload(), deliberately never persisted).So
WITHINGS_REFRESH_TOKEN=… withings-export …on a machine with a saved login silently ignores the variable, while the identical liftoff invocation honors it. That's the "same tool with a different verb" property this document exists to protect, and it was decided by whoever implemented it second rather than by the contract.Change
Adds a Precedence subsection to §5:
auth statusreports the env as the source and exits 0 with no network call — exit 0 there means "a token was supplied", not "the token works".Status table gains a
Headless auth precedencerow, and the legend gains n/a for cells that don't apply to an upstream (crono has no token file, so precedence is vacuous there).Conformance of the existing three
Docs-only — no
compat/bundle covers §5, so these cells stay human-attested. Acompat/authbundle is the obvious follow-up if a second reason to build one shows up.🤖 Generated with Claude Code
https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo