REPLY-51256: reply-cli v1 — auth, profiles, team headers, packaging - #1
Conversation
REPLY-51256 — first release of the Reply.io CLI (`reply`).
Authentication & identity:
- auth login (OAuth authorization-code + PKCE, 127.0.0.1 loopback),
auth login --with-token (API key via stdin), auth logout, auth status,
auth whoami. Transport is always Authorization: Bearer; v3 auto-detects
JWT vs API key. whoami maps the v3 contract {userId, username, teamId}.
- Credential precedence: --api-key flag > REPLY_API_KEY env > stored credential
(flag/env ephemeral, never persisted); no .env resolution. Expired OAuth
tokens refresh automatically.
Credential store & profiles:
- CredentialStore interface; file backend at 0600 in a 0700 dir, keyed by
PROFILE NAME so multiple accounts stay isolated (OS keychain deferred behind
the same interface).
- --profile selects a named account (--profile > REPLY_PROFILE > current >
built-in default/prod); profiles inherit every field from the embedded
default and are optional. profile add/set/use/list/current.
Team & acting-user headers:
- X-TEAM-ID from --team-id > REPLY_TEAM_ID > profile team_id (pins the OAuth
workspace / required for organization keys). Org-key acting user via flag-only
--user-id / --user-email -> X-USER-ID / X-User-Email, validated client-side.
Output, packaging & CI:
- Data to stdout, messages to stderr; --json/--pretty; exit 0/1/2; secrets redacted.
- Published to GitHub Packages (@reply-team/reply-cli): every main build -> @next,
tested build promoted to @latest. Windows supported (browser-open + CI matrix).
- Fully offline test suite (149 tests) — no test hits the real API or IdP; CI
builds + tests on Ubuntu and Windows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01894nPAQK7eWpiQzsgr4LW2
02b9c10 to
9ac3d96
Compare
ArtemKosolap
left a comment
There was a problem hiding this comment.
Reviewed reply-cli v1 locally (branch dev/REPLY-51256-reply-cli-v1). Marking Comment — not blocking, nice work overall.
The REPLY-51235 design is implemented faithfully across the board: single reply binary + --profile accounts (no replyrc, no --env), file credential store 0600-in-0700 keyed by profile, precedence --api-key > REPLY_API_KEY > stored (flag/env never written, no .env), OAuth code+PKCE (crypto.randomBytes(32) + S256) on an ephemeral loopback port, client_id = Reply.Cli, X-TEAM-ID / X-USER-ID / X-User-Email headers, GitHub Packages @next → @latest, Windows support + fully-offline tests. Clean and well-tested.
Should fix before merge
package-lock.json:16still declares"bin": {"replyrc": ...}— a stale, now-forbidden name;package.jsonbin is correctlyreply. Risk:npm ci(build.yml) can fail on a lockfile/manifest mismatch, and the old name shouldn't exist anywhere. Regenerate the lockfile (npm install) and commit.- OAuth principal dropped on refresh —
to_oauth_record(token.ts:50) doesn't carryuser, andresolve.ts:88stores that user-less record; after the first silent refresh,auth statusloses theUser:line until a full re-login. Preserveprev.user. - Reflected
errornot HTML-escaped in the loopback page (oauth-flow.ts:114) — low severity (loopback-only, no secret reflected), but escape it (or serve a static message).
Minor / non-blocking
- Interactive
--with-tokenechoes the pasted key (auth.ts:88); the documentedecho <key> | reply auth login --with-tokenpath is fine — mute echo on the TTY fallback. - Login fetches
/whoamibefore persisting (auth.ts:105); a transient whoami failure discards a good login → an extra browser round-trip. Store first, best-effort whoami after. - Dead
--timingbranch (client.ts:99);@types/node ^20vs Node 22 engines.
Tests
Strong offline coverage: PKCE shape/S256, token grant/refresh mapping, credential precedence (incl. ephemeral + clear-on-fail), file-store round-trip/corruption/perms, profile inheritance/current, client Bearer/retry/error mapping, redaction, and an end-to-end loopback login with CSRF-state rejection. fetch stubbed, loopback over local 127.0.0.1 — no external calls. Gaps (non-blocking): index.ts commander wiring / exit-code mapping; handle_login / handle_login_token paths (incl. the whoami-drops-tokens case above); a command-layer assertion that flag/env creds are never written to disk.
…, store-first login Addresses Artem's review of PR #1 (REPLY-51256): - token: to_oauth_record carries the previous principal, so a silent refresh no longer wipes the user shown by `auth status`. - oauth-flow: HTML-escape the loopback page so a reflected ?error cannot inject markup. - auth login: persist the credential before the /whoami lookup (identity enriched best-effort after), so a transient whoami failure can't discard a good login; mute the TTY echo on interactive --with-token. - client: drop the dead --timing branch. - package-lock.json: regenerate (bin was still the removed `replyrc`). Tests (offline): carry-user-on-refresh, loopback error-escaping, store-first login, verify-then-store token, ephemeral-creds-never-written. 156 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01894nPAQK7eWpiQzsgr4LW2
|
Thanks for the thorough review, @ArtemKosolap! All addressed in Should fix
Minor
Test gaps — added One I'd leave as-is: |
ArtemKosolap
left a comment
There was a problem hiding this comment.
Re-reviewed on the updated branch — all three must-fix items are addressed. Approving.
package-lock.jsonregenerated: the stalereplyrcbin is gone, root bin isreply.- Identity preserved on refresh:
refresh_storedpasses the prior record intoto_oauth_record, which now carriesuser—auth statuskeeps the principal after a silent refresh. - Reflected loopback
erroris escaped:html_escapeapplied insuccess_page.
Thanks for the quick turnaround. The earlier non-blocking nits (mute echo on interactive --with-token, store-before-whoami on login) are fine as follow-ups whenever. LGTM.
reply-cli v1 (REPLY-51256)
First release of the Reply.io CLI (
reply) — authentication, identity, multi-account profiles, team/acting-user context, and packaging.Auth & identity
auth login(OAuth authorization-code + PKCE,127.0.0.1loopback),auth login --with-token(API key via stdin),auth logout,auth status,auth whoami.Authorization: Bearer; v3 auto-detects JWT vs API key.whoamimaps the v3 contract{userId, username, teamId}.--api-key>REPLY_API_KEY> stored credential (flag/env ephemeral, never persisted); no.envlookup. Expired OAuth tokens auto-refresh.Credential store & profiles
CredentialStoreinterface; file backend at0600in a0700dir, keyed by profile name so multiple accounts stay isolated; OS keychain deferred behind the same interface.--profileselects a named account (--profile>REPLY_PROFILE> current > built-in default/prod); profiles inherit every field from the embedded default and are optional.profile add / set / use / list / current.Team & acting-user headers
X-TEAM-IDfrom--team-id>REPLY_TEAM_ID> profileteam_id(pins the OAuth workspace; required for organization keys).--user-id/--user-email→X-USER-ID/X-User-Email, validated client-side.Output, packaging & CI
--json/--pretty; exit0/1/2; secrets redacted.@reply-team/reply-cli): everymainbuild →@next, tested build promoted to@latest. Public npm release is a separate story (REPLY-51261).Docs
README.md(install / auth / profiles / env vars) andCONTRIBUTING.md(contributor access model, build/test, conventions, credential-store internals, release process).Tracked separately (not in this PR)
reply call-api(REPLY-51259), team discovery + conflict guidance (REPLY-51258), self-documenting manifest (REPLY-51281).🤖 Generated with Claude Code
https://claude.ai/code/session_01894nPAQK7eWpiQzsgr4LW2