Skip to content

feat(auth): collapse and restyle entire auth status - #2512

Open
gtrrz-victor wants to merge 17 commits into
mainfrom
refactor-entire-status
Open

gtrrz-victor wants to merge 17 commits into
mainfrom
refactor-entire-status

Conversation

@gtrrz-victor

@gtrrz-victor gtrrz-victor commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1359

Why

entire auth status buried its own answer:

Logged in to https://us.auth.entire.io
  User:         @gtrrz-victor <gtrrz.victor@gmail.com>
  Identity:     github/881031
  Jurisdiction: us
  Context:      us.auth.entire.io
  Token:        stored in macOS Keychain

Active sessions (23):
NAME                      CREATED     LAST USED   EXPIRES
Console login 2026-09-16  2026-09-16  2026-09-16  2026-09-23
OIDC login 2026-09-16     2026-09-16  2026-09-16  2026-10-16
… 21 more rows …

Three problems. The table dwarfed the answer. No row said which session was
this CLI's — the one entire logout would end. And every timestamp was a
bare YYYY-MM-DD, so three consecutive rows read 2026-09-16 with no sense of
recency.

It was also the one command rendering outside the shared statusStyles
helpers, with a hand-rolled %-13s label column, and the last rendering verb
in the CLI with no --json.

What it looks like now

● Logged in · expires in 29d

  user          github:gtrrz-victor
  jurisdiction  us
  context       us.auth.entire.io
  token         macOS Keychain

~32 lines down to 9. --sessions prints the full table with the caller's row
marked:

── Active Sessions ─────────────────────────────────────────

NAME                   CREATED  LAST USED  EXPIRES
OIDC login 2026-09-17  3m ago   3m ago     in 29d   (current)
────────────────────────────────────────────────────────────
1 session

Identifying the caller's own session

This is the one new capability, and it needs nothing new server-side. A
login session is an OAuth refresh-token family (already documented at
api/auth_sessions.go:12), and the login JWT already carries the family id as
its fid claim. SessionFamilyIDFromLoginJWT reads it exactly the way
HomeJurisdictionFromLoginJWT reads its own — unverified, because it only
selects a row out of a listing the server itself authorized.

It fails closed. An absent, unreadable, or unmatched claim renders neither
the marker nor the expiry rather than borrowing another session's. Everything
reachable from this screen (logout, logout --everywhere) ends a session,
and ending someone else's is worse than saying nothing.

Verified against the live API before building on it: the claim matches, and
current_session_id in --json round-trips to the marked row.

Also here

  • --json, with both views derived from one resolved value
    (resolveAuthStatus) so they cannot drift. Timestamps stay RFC3339 there —
    the humanised form is a reading aid, not something to parse.
    active_sessions is a pointer, so a failed listing is absent rather than
    reported as zero: "we could not ask" and "you have none" are different
    answers.
  • user is the provider-qualified handle (github:alice) — the spelling
    entire grant … accepts and grant … list prints, so status now hands you a
    value usable in the next command. Display name, email and the numeric
    provider id are dropped.
  • Count rows dropped at exactly one. The sole session and sole context are
    already described by the verdict line and the context row. Zero sessions
    still reports, being a contradiction worth seeing. Text-only: --json always
    carries the counts.
  • formatRelativeDuration is now signed with a month bucket, so one ladder
    serves 3h ago and in 30d. ⚠️ This changes entire status for sessions
    older than 60 days (68d ago2mo ago) — deliberate, to keep one ladder.
  • formatQualifiedHandle lands beside parseQualifiedHandle so the
    grantee spelling the CLI prints and the one it accepts cannot drift.
  • formatAuthDate deleted — nothing renders an absolute auth date any more.

Review notes

  • The ForeignRegion note's comment justified it partly as explaining absent
    display name/email. Those are gone from this view, and handle/provider/
    jurisdiction all come from /me's global and auth blocks (never
    regional, the only block a foreign core drops), so nothing can go missing
    now. The note is retargeted to the region split alone.
  • authProfile.DisplayName / Email / ProviderUserID become unread here
    but are still read by setup_identity.go, so they stay.
  • --json deliberately mirrors the text collapse rather than always dumping
    the array, so --sessions keeps its meaning in both modes.

Testing

mise run fmt && mise run lint clean; full mise run test:ci green (unit,
integration, Vogon e2e canary).

New coverage: signed/bucketed duration ladder incl. the 60-day boundary and
both sides of zero; fid claim read (present, absent, malformed); the marker
landing on the right row and on no row when unmatched; count rows at 0/1/many;
context row with name==host, name!=host, and env-token mode; the --json
envelope decoded back into its struct.

Manually verified against a real login: default view, --sessions, --json,
--json --sessions, piped (no ANSI, columns still aligned), NO_COLOR=1, and
entire status to confirm the shared time helper didn't regress it.

Note: the branch name says "entire status" but the work is entirely
entire auth status — happy to rename.

🤖 Generated with Claude Code


Note

Medium Risk
Changes auth/session UX and script-facing --json output plus unverified JWT fid matching for logout hints, but does not alter server auth; the shared relative-time helper also shifts how entire status formats very old sessions.

Overview
entire auth status is redesigned: a short verdict line (with relative expiry when the current session is known), aligned metadata rows via shared statusStyles, and no default session table. Use --sessions for the full table; --json / --json --sessions for scripting, both built from one resolveAuthStatus result so text and JSON stay aligned.

The caller’s session is picked by matching the login JWT fid claim to listed session ids (SessionFamilyIDFromLoginJWT); unmatched claims show no (current) marker and no borrowed expiry. Output emphasizes a provider-qualified user (github:alice), drops display name/email from this view, adds active sessions / available contexts count rows (hidden when the count is 1 in text only), and uses relative session timestamps instead of formatAuthDate.

formatRelativeDuration now handles future times (in 30d) and a month bucket, shared with entire status (sessions older than ~60 days read as months). Docs in CLAUDE.md describe the new flags and collapse rules.

Reviewed by Cursor Bugbot for commit 1b4daad. Configure here.

@gtrrz-victor
gtrrz-victor requested a review from a team as a code owner September 17, 2026 08:30
Copilot AI lite review requested due to automatic review settings September 17, 2026 08:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Address the three moderate JSON and single-session count handling issues in auth.go.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Redesigns entire auth status with compact output, session identification, optional listings, and JSON support.

Changes:

  • Adds signed relative-duration and qualified-handle formatting.
  • Identifies the current session via JWT fid.
  • Adds --sessions and --json modes.
  • Updates documentation and related tests.
File summaries
File Summary
cmd/entire/cli/status.go Adds signed, bucketed duration formatting.
cmd/entire/cli/status_test.go Tests duration formatting.
cmd/entire/cli/resolveref.go Adds qualified-handle formatting.
cmd/entire/cli/resolveref_test.go Tests handle formatting.
cmd/entire/cli/auth/session_family_test.go Tests JWT family-ID extraction.
cmd/entire/cli/auth/cell_data_api.go Extracts JWT fid claims.
cmd/entire/cli/auth.go Implements auth-status rendering and JSON output.
CLAUDE.md Documents updated auth-status behavior.
Review details

Suppressed comments (1)

cmd/entire/cli/auth.go:768

  • This suppresses the count for every one-session result, even when currentSessionIndex is -1 (for example, an older core with no fid, an opaque token, or an unmatched claim). In that case the verdict has no expiry and no marker, so hiding the only session count makes the output omit the fact that a session exists. Only drop this row when the sole session was actually identified (or pass the current index into this helper and gate on it).
	if len(sessions) == 1 {
		return explainRow{}, false
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/entire/cli/auth.go Outdated
Comment thread cmd/entire/cli/auth.go Outdated
gtrrz-victor added a commit that referenced this pull request Sep 17, 2026
Three count-ish fields used omitempty on plain values, so a real zero and a
value we never computed encoded identically.

- `sessions` was dropped when `--sessions` returned an empty listing, making a
  satisfied full-list request indistinguishable from the collapsed default,
  where the key is genuinely absent. It now emits an explicit [].
- `available_contexts` was dropped at zero while its `active_sessions` sibling
  was already a pointer. Zero was doubly ambiguous: ENTIRE_TOKEN mode never
  reads contexts.json (resolveEnvTokenStatusTarget leaves the count at 0), so
  absent-because-uncounted and genuinely-none looked the same. It is now set
  only when the count was actually taken.

Both become pointers, matching `active_sessions`, so the rule across the
envelope is one thing: absent means "not known", zero means zero.

Reported by Copilot on #2512.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2Q8TV28XAYQXH23TGP5GTDC
gtrrz-victor added a commit that referenced this pull request Sep 17, 2026
Three count-ish fields used omitempty on plain values, so a real zero and a
value we never computed encoded identically.

- `sessions` was dropped when `--sessions` returned an empty listing, making a
  satisfied full-list request indistinguishable from the collapsed default,
  where the key is genuinely absent. It now emits an explicit [].
- `available_contexts` was dropped at zero while its `active_sessions` sibling
  was already a pointer. Zero was doubly ambiguous: ENTIRE_TOKEN mode never
  reads contexts.json (resolveEnvTokenStatusTarget leaves the count at 0), so
  absent-because-uncounted and genuinely-none looked the same. It is now set
  only when the count was actually taken.

Both become pointers, matching `active_sessions`, so the rule across the
envelope is one thing: absent means "not known", zero means zero.

Reported by Copilot on #2512.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2Q8TV28XAYQXH23TGP5GTDC
@gtrrz-victor
gtrrz-victor force-pushed the refactor-entire-status branch 2 times, most recently from 832e2f6 to efb0b2b Compare September 17, 2026 09:26
gtrrz-victor added a commit that referenced this pull request Sep 17, 2026
Four findings from the trail review on #2512.

**Every relative-time unit band now starts at 1.** humanizeDuration ran days
to 60, so the first reachable month was "2mo" and a value ticking past the
boundary read 59d then 2mo — as if it had doubled. Days now stop at 30, which
costs the precise "in 30d" for a month-long session expiry and buys back a
continuous ladder: 1m, 1h, 1d, 1mo. This reaches every timeAgo caller
(status, sessions, search, trail list), not just auth.

**`logout --everywhere` is offered only alongside the table.** It ends every
session at once, and the collapsed view turned those sessions into a count the
reader cannot inspect — on a real login, 3 of 4 were browser Console logins
invisible there. Before the redesign the table always rendered, so the
destructive hint arrived with its subject attached; now the hint waits for
`--sessions`. The count row already says how to get there.

**One spelling for the login server.** The logged-out and invalid-token lines
printed the full URL while every other path printed the bare host. Both now go
through authServerHost.

**The JSON contract doc contradicted itself**, and the code was the correct
half: it claimed `--json` "always carries" the counts, then listed the cases
where it does not. The accurate claim is narrower — the drop-at-one collapse is
text-only, and JSON omits only what it could not determine, which is what the
pointers encode. The `sessions` claim was wrong the same way: it is emitted
whenever `--sessions` reached the listing, and the paths that return before it
(not logged in, env token, failed fetch) omit it with everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2QCPCZM6056W23XED33EFT7
gtrrz-victor and others added 5 commits September 17, 2026 13:07
`auth status` buried its own answer. A 23-row session table took over the
screen, no row said which session was this CLI's — the one `entire logout`
would end — and every timestamp was a bare YYYY-MM-DD, so three consecutive
rows read 2026-09-16 with no sense of recency. It was also the one command
rendering outside the shared `statusStyles` helpers, with a hand-rolled
`%-13s` label column.

The default view is now the verdict plus an aligned block, ~32 lines down to 9:

    ● Logged in · expires in 29d

      user          github:gtrrz-victor
      jurisdiction  us
      context       us.auth.entire.io
      token         macOS Keychain

Identifying the caller's own session is what makes that expiry possible, and
it needs nothing new server-side: a login session IS an OAuth refresh-token
family, and the login JWT already carries the family id as its `fid` claim.
`SessionFamilyIDFromLoginJWT` reads it the way `HomeJurisdictionFromLoginJWT`
reads its own, and the match marks the row `(current)` under `--sessions`.

It fails closed. An absent, unreadable, or unmatched claim renders neither the
marker nor the expiry rather than borrowing another session's, because
everything reachable from here (`logout`, `logout --everywhere`) ends a
session, and ending someone else's is worse than saying nothing.

Also here:

- `--sessions` prints the full table; `--json` was the last rendering verb in
  the CLI without one. Both views derive from one resolved value
  (`resolveAuthStatus`) so they cannot drift.
- `user` is the provider-qualified handle, the spelling `entire grant` accepts
  and `grant … list` prints — so status hands you a value usable in the next
  command. Display name, email and the numeric provider id go.
- Count rows for sessions and contexts, dropped at exactly one: the sole
  session and sole context are already described above them. Zero sessions
  still reports, being a contradiction worth seeing. Text-only — `--json`
  always carries the counts, since omitting one from a machine reader loses
  information rather than saving a line.
- `formatRelativeDuration` becomes signed and gains a month bucket, so one
  ladder serves both "3h ago" and "in 30d". The sign test must precede the
  near-zero test, or a future duration reports "just now". This changes
  `entire status` for sessions older than 60 days (68d ago → 2mo ago).
- `formatQualifiedHandle` lands beside `parseQualifiedHandle` so the grantee
  spelling the CLI prints and the one it accepts cannot drift.

`formatAuthDate` is deleted: nothing renders an absolute auth date any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2Q7S55TXBA9A73STV776QR4
Three count-ish fields used omitempty on plain values, so a real zero and a
value we never computed encoded identically.

- `sessions` was dropped when `--sessions` returned an empty listing, making a
  satisfied full-list request indistinguishable from the collapsed default,
  where the key is genuinely absent. It now emits an explicit [].
- `available_contexts` was dropped at zero while its `active_sessions` sibling
  was already a pointer. Zero was doubly ambiguous: ENTIRE_TOKEN mode never
  reads contexts.json (resolveEnvTokenStatusTarget leaves the count at 0), so
  absent-because-uncounted and genuinely-none looked the same. It is now set
  only when the count was actually taken.

Both become pointers, matching `active_sessions`, so the rule across the
envelope is one thing: absent means "not known", zero means zero.

Reported by Copilot on #2512.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2Q8TV28XAYQXH23TGP5GTDC
The note existed for two reasons, and the stronger one no longer applies.
It explained why the server host and the jurisdiction slug name different
regions, and — mostly — why the display name and email were silently missing,
since a core outside your home region withholds /me's regional block for an
account it does not host.

This view stopped rendering display name and email, so nothing goes missing
for it to explain. What was left restated the two rows it sat between:

    jurisdiction        us
    note                served by eu.auth.entire.io, outside your home region
    context             eu.auth.entire.io

The condition is still real server state (/me's regionalUnavailable block), so
it keeps reaching machine readers as the JSON `foreign_region` flag; it just no
longer spends a row in a block that is meant to be scanned.

authProfileRows loses its styles and target arguments with it, and
api.OriginOnly now has no caller in auth.go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four findings from the trail review on #2512.

**Every relative-time unit band now starts at 1.** humanizeDuration ran days
to 60, so the first reachable month was "2mo" and a value ticking past the
boundary read 59d then 2mo — as if it had doubled. Days now stop at 30, which
costs the precise "in 30d" for a month-long session expiry and buys back a
continuous ladder: 1m, 1h, 1d, 1mo. This reaches every timeAgo caller
(status, sessions, search, trail list), not just auth.

**`logout --everywhere` is offered only alongside the table.** It ends every
session at once, and the collapsed view turned those sessions into a count the
reader cannot inspect — on a real login, 3 of 4 were browser Console logins
invisible there. Before the redesign the table always rendered, so the
destructive hint arrived with its subject attached; now the hint waits for
`--sessions`. The count row already says how to get there.

**One spelling for the login server.** The logged-out and invalid-token lines
printed the full URL while every other path printed the bare host. Both now go
through authServerHost.

**The JSON contract doc contradicted itself**, and the code was the correct
half: it claimed `--json` "always carries" the counts, then listed the cases
where it does not. The accurate claim is narrower — the drop-at-one collapse is
text-only, and JSON omits only what it could not determine, which is what the
pointers encode. The `sessions` claim was wrong the same way: it is emitted
whenever `--sessions` reached the listing, and the paths that return before it
(not logged in, env token, failed fetch) omit it with everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2QCPCZM6056W23XED33EFT7
`!strings.Contains(got, "note")` matched four letters anywhere in the rendered
block, so a context named "notebook", a token-source description, or any
future label carrying them would answer for a row that is not there.

The flagged assertion now goes through the row builder and the existing
hasLabel. Three others I wrote had the same shape with no helper that fits —
they only have rendered output, and hasMetadataRow needs a value an absence
assertion does not have — so this adds hasMetadataLabel, its value-agnostic
sibling, and routes the drop-at-one, env-token and no-jurisdiction checks
through it.

Substring matching stays where it is correct: "outside your home region",
"console.entire.io", "#/profile", "keychain" and "Active Sessions" are content
and headings rather than labels, and the comments say so, since telling the two
apart is the whole point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2QEZ6XTHTHE3FA59VCN504A
gtrrz-victor and others added 12 commits September 17, 2026 14:53
The drop-at-one rule rests on the verdict line's `expires` standing in for the
single session, and nothing checked that premise. The expiry is printed only
when the caller's own session was identified, so a login whose `fid` matches no
listed session lost the row anyway and the default view fell to:

    ● Logged in

      user     github:alice
      context  a · eu.auth.entire.io
      token    …

    Run 'entire logout' to end this session.

No count, no expiry, no route to --sessions — and the one session listed is the
login that replaced yours, which is precisely the one worth looking at.

Reachable whenever a family is revoked inside its access token's own lifetime:
resolveStatusTarget falls back to the stale bearer (its comment already names
"revoked family" as a refresh-failure cause), /me still honours it, and fid
names a family the listing no longer holds. Observed on a live account when a
concurrent session logged out everywhere and back in.

TestRunAuthStatus_CountRowsAreDroppedAtOne was passing for the wrong reason: it
used an opaque token, so current was already -1 and it asserted the drop in
exactly the case that must now suppress it. It carries a matching JWT now, and
TestRunAuthStatus_UnidentifiedSingleSessionStillCounts pins the other half.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2QPWDGPX6N30CFDCXG9Q11R
A revoked session does not stop working immediately. The family dies but the
access token it minted keeps being honoured for its own lifetime, so `auth
status` printed a cheerful "Logged in" for a login that could not be renewed
and would lapse without warning:

    ● Logged in
      …
      active sessions  0

Zero active sessions while authenticated is the contradiction, and it is now
named rather than left to be inferred:

    ● Logged in · expires in 6h
      ! this login was ended elsewhere and cannot be renewed · run 'entire login'

The verdict line deliberately switches source here. Normally `expires` is the
session family's, weeks out; with nothing left to renew the bearer, its own
expiry is when the user is logged out, so that is the honest number to show.
JSON gets login_revoked and token_expires_at, while expires_at stays absent —
no session was attributed, and the two must not be conflated.

What counts as evidence depends on the listing. Zero sessions settles it alone:
the endpoint includes the caller's own session — that is how a matched fid
finds itself — so none listed means none exist, and no truncation explains
zero. With sessions listed, absence is the only evidence, so a fid must have
named something; a core too old to mint one is evidence of nothing and stays
quiet.

An earlier revision also required the refresh to have failed, as corroboration
against a truncated listing. That was wrong: refresh failure is only observable
when a refresh is attempted and fails, and a token far from expiry is returned
without contacting the server at all — so the notice stayed silent in the
commonest case, every session revoked while the bearer still had hours to run.
Verified against a live account in exactly that state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2QRP9B6J41Z1MHC2KY1V5GG
With a session listed, a revoked login printed the notice and then immediately
contradicted it:

    ! this login was ended elsewhere and cannot be renewed · run 'entire login'
      …
    Run 'entire logout' to end this session.

There is no session of the caller's left to end — that is what the notice says
— and the session that IS listed belongs to the login that replaced this one,
so offering to end it would be wrong a second way. The banner's `entire login`
is the only action that applies, so the hint is withheld entirely once revoked
is established.

Invisible in the state this was verified against: the hint only prints when at
least one session is listed, and the live account had none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2QSHXZ3GPG1KK1PDY2MQXHB
main's logout sweep landed while this branch was rewriting `auth status`,
and the two met in the status view's logout hint. `entire logout` no
longer ends just the caller's session — it ends every CLI session on
every saved login and removes them locally, with `--everywhere` adding
browser and web. The hint now says so: "end every CLI session" rather
than "end this session".

The revoked-login gate stays. Its wording leaned on the contradiction
between "end this session" and the notice above it; the reason that
survives the rename is the one that was always doing the work — the
sessions still listed belong to the login that replaced this one, so
there is nothing here worth ending, and the banner's `entire login` is
the action.

docs/development/cli-conventions.md keeps both accounts: this branch's
`auth status` collapse and main's `logout` sweep, with the branch's
stale description of `logout`'s old flags dropped in favour of main's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M31MA2GZ44A3GV50SN2R055V
Both context rows are a way of saying "this login, not the others", so
both now wait until there are others. With a sole saved login there is
nothing to distinguish it from: naming it describes a choice the user
does not have, and the count beneath it counts the only thing there is.

The count row already stopped at one, on the grounds that the row above
named it. That reasoning kept the wrong row — the pair is only worth two
lines when there is a selection to describe, and worth none when there
is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M322T1G78GJZNSVH7MGE1W99
One conflict, in resolveref.go: main's repo-resolution rework inserted
resolveProjectRefResolved at the same point this branch inserted
formatQualifiedHandle — the line after parseQualifiedHandle. Both are
wanted and neither touches the other, so both stay, with
formatQualifiedHandle still adjacent to the parser it inverts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M36Z0P8XTTE7HZ4KWJCA00G5
Thirteen findings, most of them about the verdict line making claims the
data behind it does not support.

The deadline printed beside "Logged in" is now tense-checked: a lapsed
instant reads `expired` rather than `expires 19h ago`, which contradicted
the word next to it, and an unreadable one is dropped instead of echoed
mid-sentence. Because that deadline is the whole premise of dropping the
sole-session row, the drop is gated on it having actually rendered —
`expires_at` is a plain string with no omitempty, so a session can arrive
without one and leave the default view carrying no session information at
all.

The login server is held to the same test as the context rows: the host
joins the `context` row only when the saved logins are spread across more
than one server, since logins sharing a server are told apart by their
names and the host they share names none of them.

`auth status` takes no positional arguments; `auth status sessions` was
being read as the collapsed default with the word dropped. It now has
command-level tests, so renaming a flag fails something.

`--json` keeps its promise of a parseable object: a hard /me failure
prints an envelope carrying `error` instead of empty stdout, and
`env_token`/`token_source` survive a bearer /me rejected — a script told
only `logged_in:false` could not otherwise see that ENTIRE_TOKEN supplied
the token and is still winning over every stored context.

An account /me returns without a handle falls back to its provider user
id, rather than being described by nothing at all. The logout hint names
`--everywhere` as a flag instead of sizing it with a count of one
server's rows, when logout sweeps every login server.

The two hand-rolled JWT claim readers become one `decodeLoginJWTClaims`,
gated by tokens.ParseClaims. That drops a duplicated shape check and
three duplicated error strings, and settles their disagreement over
alg:none on the policy the package already applies to ENTIRE_TOKEN:
refuse it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M374VAS6SQV0YK6AGSD2PNN0
The lapsed-deadline finding named two sites and the first fix reached
only one. The session table's EXPIRES cell still rendered a dead session
as "19h ago", which is the weaker case but the sharper defect: the column
heading supplies the verb, so the cell states its tense by implication
alone and a reader scanning for a dead session will not pick it up.

CREATED and LAST USED keep the plain relative formatter, the past being
the tense they report. An unreadable value still reaches the cell
verbatim — a column of its own is where a server value this code cannot
parse belongs, which is the distinction the prose line draws by dropping
it instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M378AJKYJ1C17RF4DS4EK3B5
Reporting where a bearer came from had been hoisted above the
not-logged-in return so that a rejected ENTIRE_TOKEN stays visible. That
went one step too far: with no token at all, the envelope still named the
keychain, telling a script a token is filed there when none is. Gated on
a bearer actually existing, which is the condition that was meant all
along.

Also restores authProfileRows' doc comment, which the new
authIdentityLabel had been inserted underneath — leaving the comment
attached to the wrong function and authProfileRows with none — and says
on HomeJurisdictionFromLoginJWT that it now refuses alg:none, since that
is a behaviour change to a helper git-remote-entire shares.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M37AETSE8YRWHMYWFQ6Z5FJ0
The previous version carried a comment excusing its error branches as
compiler ceremony. That was half wrong and wholly the wrong fix: only the
base64 decode is unreachable once ParseClaims has passed, while the
unmarshal is genuinely reachable — ParseClaims type-checks its own fields
only, so a claim of the wrong JSON type (a numeric fid) sails through it
and fails here.

The two now share one error path, which removes the unreachable branch
without pretending the reachable one beside it was ceremony too, and the
comment states what the structure is rather than apologising for it.
TestSessionFamilyIDFromLoginJWT pins the wrong-typed claim, which nothing
covered before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M37B2AC09PDZ7AQCG1T63B60
…ones

The envelope was guaranteed inside runAuthStatus while the two failures
raised before it — an unresolvable target (malformed ENTIRE_TOKEN,
unreadable contexts.json, unknown --context) and the TLS refusal on an
http:// login server — still returned bare errors. `auth status --json |
jq .logged_in` failed to parse on exactly the inputs a script is least
able to anticipate. A promise that holds only past a certain line in the
command is not one a caller can use.

All three now route through authStatusJSONFailure.

Writing the tests turned up the other half: cobra prints its usage dump
to stdout, so on --json it would follow the envelope there and leave
nothing parseable. The root command silences usage, but the contract
should not depend on where this command is mounted, so RunE silences it
itself — argument validation has already run by then, which is the only
error a usage dump helps with, and `auth status sessions` still gets one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M37BN196R04KZEYVPC6021RD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants