Skip to content

docs(contract): §5 — headless env var wins over the saved token file#28

Merged
DTTerastar merged 1 commit into
mainfrom
docs/auth-precedence
Jul 24, 2026
Merged

docs(contract): §5 — headless env var wins over the saved token file#28
DTTerastar merged 1 commit into
mainfrom
docs/auth-precedence

Conversation

@DTTerastar

Copy link
Copy Markdown
Contributor

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:

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:

  • The headless variable takes precedence over any saved token file when set. A container with a stale mounted config and a freshly injected secret must use the secret.
  • auth status reports the env as the source and exits 0 with no network call — exit 0 there means "a token was supplied", not "the token works".
  • For upstreams that rotate refresh tokens, the headless path writes the rotated value back best-effort. A failed write is a stderr warning, not an error (§4): read-only rootfs is a normal container shape, and a minted access token is usable whether or not the cache write landed.

Status table gains a Headless auth precedence row, 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

CLI State
crono-export n/a — username/password env, no token file
liftoff-export already conforms, no change needed
withings-export quantcli/withings-export-cli#43 changes to match; that PR is blocked on this one

Docs-only — no compat/ bundle covers §5, so these cells stay human-attested. A compat/auth bundle is the obvious follow-up if a second reason to build one shows up.

🤖 Generated with Claude Code

https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo

§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
DTTerastar merged commit fec40e8 into main Jul 24, 2026
7 checks passed
@DTTerastar
DTTerastar deleted the docs/auth-precedence branch July 24, 2026 01:31
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>
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