Skip to content

feat(cli): device login and account management over /v2 - #1

Merged
gdameneses merged 8 commits into
mainfrom
feat/account-management
Aug 19, 2026
Merged

feat(cli): device login and account management over /v2#1
gdameneses merged 8 commits into
mainfrom
feat/account-management

Conversation

@AJCJ1

@AJCJ1 AJCJ1 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Adds browser-based login, account management, and org credential resources, ported from the urlbox/cli repo. Removes urlbox auth.

New commands:

  • login / logout: device flow in the browser; stores the session token and the active project's API key/secret, so rendering works straight after login
  • whoami (alias me), usage
  • orgs list|select
  • projects list|select|show|create|rename|enable|disable|delete and projects defaults show|set|remove
  • storage, proxies, llm: list/show/create/update/delete per group, plus llm test and llm models
  • projects storage|proxy|llm assign|unassign

Breaking:

  • urlbox auth is removed. urlbox login is the interactive path; CI and headless keep using URLBOX_API_SECRET. doctor now runs nine checks including session, active org/project, and render-credential validity. A CHANGELOG entry is included (drafted as v0.11.0; version number pending discussion).

Also:

  • session-authenticated API client for the /v2 and better-auth endpoints; profile gains session_token, active_org and active_project
  • shared table/KV renderers for text output, used by every list and detail command; secrets masked everywhere with --reveal to unhide
  • fixes two existing text-mode bugs: link didn't print the signed URL, and doctor didn't print per-check hints and showed a tick on failure
  • delete asks you to retype the name, disable asks y/n, and --yes skips all prompts; deleting the active project re-selects the survivor or offers a picker; disabling an already-disabled project is a no-op that says so
  • every create accepts the name positionally (--name works too)
  • --no-retry / --max-retries on the session commands, matching render and status
  • compatibility tests assert that existing commands behave identically with and without a logged-in config

🤖 Generated with Claude Code

@AJCJ1
AJCJ1 requested a review from gdameneses August 17, 2026 10:46
@AJCJ1
AJCJ1 marked this pull request as draft August 17, 2026 10:50
@AJCJ1
AJCJ1 force-pushed the feat/account-management branch from fee63bb to 617bad5 Compare August 19, 2026 07:44
@AJCJ1
AJCJ1 marked this pull request as ready for review August 19, 2026 07:44
- urlbox login device flow: browser approve, org/project pickers, session plus the project's render key/secret pair stored so rendering works immediately
- session commands: logout, whoami/me, orgs list/select, projects list/select/create/rename/enable/disable/delete/defaults, usage
- org credential resources: storage, proxies, llm groups (list/show/create/update/delete, llm test/models) and projects storage|proxy|llm assign/unassign
- BREAKING: urlbox auth removed; login is the interactive path, URLBOX_API_SECRET stays for CI; doctor reworked with session-aware checks; CHANGELOG v0.11.0 entry included
- text mode renders shared lipgloss tables and KV boxes; link prints its signed URL; doctor prints per-check hints with correct failure glyph
- confirm gates (retype for delete, y/n for disable, --yes for agents); creates accept a positional name; deleting the active project re-selects; disabling a disabled project is a stated no-op
- --no-retry/--max-retries on session commands; compatibility suite pins every pre-existing command across legacy and post-login configs
@AJCJ1
AJCJ1 force-pushed the feat/account-management branch from 617bad5 to e032f89 Compare August 19, 2026 08:30
AJCJ1 and others added 7 commits August 19, 2026 11:48
…not serve

The device flow needs a browser, so removing `urlbox auth` left no way to
write a secret into a fresh config on a headless box, in CI, or under an
agent. `config set api_secret` cannot fill the gap: with zero profiles it
errors "No profiles configured" on main and on this branch alike, so the
migration path the changelog suggested never worked from a clean machine.

Restores auth.go and its tests, minus the stdin-TTY helpers and maskSecret
that moved to stdin_tty.go and masking.go on this branch. SURFACE.txt gains
its nine `urlbox auth` lines back, so this release is purely additive.

Hints now name both onboarding paths via a shared credentialHint: `login`
for a browser, `auth` / URLBOX_API_SECRET for everything else. Pointing a
browserless environment at `login` is a dead end.

TestNoAuthCommandRemains becomes TestAuthCommandStillRegistered, and the
ghost-command blocklist goes back to pinning the removed `--api-key` flag
rather than the whole command.
…mpt session probe

`doctor` exited 3 on a perfectly healthy machine. The session, active_org
and active_project checks read profile fields that only `login` writes, so
any setup authenticating with a render credential — URLBOX_API_SECRET, a
stored profile, the repo overlay — failed three checks even with a valid,
working credential and render_credential reporting ok. Verified against a
local API: main exits 0 in that configuration, this branch exited 3, which
retired `doctor` as a CI health gate.

When a credential resolves, those three checks now report warn and explain
themselves; overall status returns to ok. With no credentials at all the
behaviour is unchanged — still fail, still exit 3.

Also drops the session probe to a single attempt. Every other check here
probes once; retrying four times with backoff only made `doctor` hang ~7s
against an unreachable host without changing the diagnosis.
The session retry tests assert how many attempts were made, never how long
they took, but they slept the real 1s/2s/4s budget: internal/cmd went from
6s on main to 40s here, and `make test` adds -race on top.

RetryConfig.Sleep was already injectable. sessionRetrySleep threads a no-op
through the one session-client construction site, installed by TestMain in
the test binary only — nil in production, so real backoff is untouched, and
scoped to session clients so render and status keep their own sleep.

Every assertion is unchanged.
…y in text

Masking was a text-mode affordance: `storage show` printed `sup3…3t` while the
same command with stdout on a pipe returned `"secret":"sup3rs3cr3t"` in full.
Because the CLI resolves a non-TTY stdout to JSON, that made the unmasked form
the default for exactly the consumers that persist it — agents, CI logs, `| tee`
transcripts — and left `--reveal` as a no-op there, a flag that reads like a
protection while protecting nothing.

JSON now masks the same fields the KV views mask: storage key/secret/sasToken,
every LLM provider secret (apiKey, the three AWS fields, the GCP service
account), the password component of a proxy URL, and a project's webhookKey.
`--reveal` unhides both surfaces from one switch, and joins `list` — previously
show-only, though `proxies list` and `storage list` carry the same material.

Non-secret fields pass through byte-for-byte, so `--jq` over ids, names, buckets
and regions is unaffected; only the named fields change, and only when they hold
a non-empty string, so a null stays null instead of becoming "***". The masked
copy is always fresh: the KV builders mask the raw response themselves, and
redacting in place would double-mask what a human sees.

Proxy masking keeps scheme, host and port legible and replaces only the
password, so pools stay tellable apart without exposing the credential.

TestProjectsShow_JSON_ByteIdenticalToServerResponse pinned the old contract; it
becomes …PassesServerResponseThroughWithSecretsMasked, still asserting the raw
passthrough for every non-secret field and now asserting --reveal restores the
response verbatim.
Switching organisations drops the stored render credential on purpose — it
belongs to a project in the org being left, and keeping it would let `render`
bill the previous organisation silently. That part is right and stays. Three
things around it were not.

It cleared api_secret but left api_key, so a publishable key from the previous
org survived in the profile; the credential is a pair and both halves go.

When the new org had several projects and no terminal to pick with, the switch
finished in a deliberately incomplete state — no active project, no credential —
and reported it only on stderr, returning ok:true with an unqualified "Active
organisation: X". An agent reads stdout, so it saw plain success and discovered
the missing credential on its next render. `login` hits the identical branch of
resolveActiveProject and fails loudly there; the same situation now travels in
the envelope here too: the summary names the project count and a breadcrumb
points at `urlbox projects select`. The exit code stays 0 — the org did switch —
but the envelope no longer claims the context is complete when it isn't.

And there was no way to avoid the gap at all non-interactively. `orgs select`
now takes --project, mirroring `login --org --project`, so the switch and the
new active project land in one call. An unresolvable --project is a hard error
rather than a half-finished switch: the caller named something that isn't there.

resolveActiveProject returns the project count alongside the choice so callers
can report it; login ignores it.
…y promise

v1.0.0 is published and immutable, so v1.0 cannot be reissued; v1.1.0 is the
closest available version and the correct one by semver anyway — this release
adds a command surface rather than patching one. The other free slot near 1.0,
v1.0.5, would drop a live release into the middle of the deprecated v1.0.x
range, where anyone pinning ~1.0 sees five deprecated versions and one live.

SURFACE.txt stops describing itself as pre-1.0. From here the file is the
promise: nothing listed is removed or renamed inside the v1 line without a
major bump. That is the sentence the 0.x reset existed to be able to write.

The v0.10.0 entry's closing line ("The v1.x line is deprecated on npm") is now
false and says so, pointing at the v1.1.0 entry. Clearing the five npm
deprecation notices is a publish-time step, not a code one.

Also flags the installer consequence of leaving 0.x: npm/install.js gates its
sigstore policy on the version string, so on v1 a missing bundle is a hard
install failure rather than a sha256-only fallback. goreleaser signs
checksums.txt, so this is a tightening, not a break — but it is now
load-bearing and belongs in the release notes.
@gdameneses
gdameneses merged commit 899c202 into main Aug 19, 2026
1 check passed
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.

2 participants