Skip to content

feat(auth): add 'aqora auth token' for external consumers#187

Merged
stubbi merged 1 commit intomainfrom
feat/aqora-auth-token
Apr 24, 2026
Merged

feat(auth): add 'aqora auth token' for external consumers#187
stubbi merged 1 commit intomainfrom
feat/aqora-auth-token

Conversation

@stubbi
Copy link
Copy Markdown
Contributor

@stubbi stubbi commented Apr 24, 2026

This pull request was authored by a coding agent on behalf of @stubbi (jannes@aqora.io).

Motivation

External consumers (aqora-io/skills, CI jobs, other agents, third-party tools) need a stable contract to get a valid aqora access token. Today they have to read credentials.json directly, which does not trigger the CLI's existing refresh logic, so tokens silently go stale after their short lifetime.

This adds aqora auth token, matching the well-established pattern from gh auth token, gcloud auth print-access-token, and aws configure export-credentials. One clean contract: print a fresh access token on stdout, refreshing as needed.

Context: aqora-io/skills#1 (separate issue, but the skills repo was where this need surfaced).

What this PR does

  • Adds an auth command group and a token subcommand under it.
  • aqora auth token prints a valid access token on stdout for the current API URL.
  • Reuses credentials::load_refreshed_credentials so refresh is transparent; no new auth code.
  • Optional --url URL flag to target a specific API URL when logged into multiple environments.
  • Exits 1 with a clear stderr message when not logged in.
  • No change to existing aqora login behavior.

What this PR explicitly does not do

  • No aqora auth login, logout, or status subcommands. Those are obvious follow-ups but scope creep here.
  • No change to credential storage format.
  • No change to login, shell, lab, or any other existing command.

Testing

  1. Build: cargo build --release clean, no new warnings.

  2. Clippy: cargo clippy --all-targets no new warnings.

  3. Live token test against the production API:

    $ tok=$(cargo run --release --quiet -- auth token)
    $ echo "Token length: ${#tok}"
    Token length: 50
    $ curl -fsS -X POST -H "Authorization: Bearer $tok" -H "Content-Type: application/json" \
        --data '{"query":"{ viewer { id } }"}' https://aqora.io/graphql
    {"data":{"viewer":{"id":"AFVzZXIBjTXKssp7bIhK6mkQ35U9"}}}
    
  4. Error path: used --url https://bogus.example and confirmed the expected stderr message and exit 1:

    $ cargo run --release --quiet -- auth token --url https://bogus.example
    ERROR Oh no! Not logged in to https://bogus.example/
    
    To try and fix this, you can:
     - Run 'aqora login' to authenticate.
    $ echo $?
    1
    

Reviewer notes

Small surface area. The entire change is wiring plus a call into credentials::load_refreshed_credentials (previously a private helper, now exported). Suggested follow-ups for a future PR: aqora auth login as alias, aqora auth logout, aqora auth status.

Summary by CodeRabbit

  • New Features
    • Added auth command with token subcommand to retrieve and display access tokens
    • Support for optional --url parameter to specify a custom API endpoint
    • Clear error messages guide users to authenticate when credentials are unavailable

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

A new auth token CLI command is introduced that retrieves and outputs refreshed access tokens from the Aqora API. The implementation adds a new auth command module with a token subcommand, integrates it into the CLI dispatcher, and exposes the credentials loading function publicly.

Changes

Cohort / File(s) Summary
New Auth Command Module
src/commands/auth/mod.rs, src/commands/auth/token.rs
Introduces Auth enum with Token subcommand and an auth() dispatcher function. The Token struct accepts an optional --url argument; the token() function resolves the API endpoint, creates an unauthenticated GraphQL client, loads refreshed credentials, and outputs the access token. Handles credential absence and refresh failures with contextual error messages.
CLI Integration
src/commands/mod.rs
Registers the new Auth command variant in the Commands enum and extends the dispatch logic in Cli::do_run to route to the auth handler.
Credentials Visibility
src/credentials.rs
Exposes load_refreshed_credentials() as a public function to enable the token command to access credential loading functionality.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant AuthCmd as Auth Token<br/>Command
    participant Creds as Credentials<br/>Module
    participant GraphQL as GraphQL<br/>Client
    participant FileSystem as File<br/>System

    User->>AuthCmd: Run auth token [--url]
    AuthCmd->>AuthCmd: Resolve API URL
    AuthCmd->>AuthCmd: Compute credentials path
    AuthCmd->>GraphQL: Create unauthenticated client
    AuthCmd->>Creds: load_refreshed_credentials()
    Creds->>FileSystem: Read cached credentials
    alt Credentials present
        Creds->>Creds: Check expiration
        alt Token expired
            Creds->>GraphQL: Refresh token
        end
        Creds->>FileSystem: Reload/verify credentials
        Creds-->>AuthCmd: Return LoadCredentials
        AuthCmd->>User: Output access_token
    else Credentials absent
        Creds-->>AuthCmd: Error: No credentials
        AuthCmd->>User: Error: Run 'aqora login'
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • volgar1x

Poem

🐰 A token for thee, fresh from the cache,
With GraphQL magic and credentials stashed,
The auth command hops through the API's door,
Refreshing your tokens like never before! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(auth): add 'aqora auth token' for external consumers' directly and clearly describes the main change: introducing a new auth command with a token subcommand for external consumers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/aqora-auth-token

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@stubbi stubbi marked this pull request as ready for review April 24, 2026 10:44
@stubbi stubbi requested a review from jpopesculian April 24, 2026 10:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/commands/auth/token.rs (1)

19-31: Consider: --url overlaps with the existing global --url.

GlobalArgs already exposes --url (used via global.aqora_url() on line 30), so users can already do aqora --url https://... auth token. Adding a second subcommand-level --url means both aqora --url X auth token and aqora auth token --url X work, with subcommand winning if both are set. That's functional, but a bit confusing.

Two options, in decreasing order of invasiveness:

  1. Drop the subcommand-level flag entirely and rely on the global --url.
  2. Keep it, but mention in the help text that it overrides the global --url, so users passing both understand the precedence.

If the rationale is discoverability for external consumers (so aqora auth token --help surfaces the flag), option 2 is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/auth/token.rs` around lines 19 - 31, The Token subcommand
defines its own --url flag (Token.url) while the global GlobalArgs already
provides a --url used via global.aqora_url() in the token() function, causing
overlapping flags; either remove the Token.url field and always use
global.aqora_url(), or update the Token.url arg help to explicitly state it
overrides the global --url so precedence is clear (and keep the existing match
in token() that prefers args.url over global.aqora_url()); reference Token
struct, its url field, and the token(args: Token, global: GlobalArgs) ->
Result() function where global.aqora_url() is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/auth/token.rs`:
- Around line 34-41: The error hint when load_refreshed_credentials(...) fails
is misleading for refresh-related failures; update the error::system call in the
error mapping (the closure passed to load_refreshed_credentials) to use a more
accurate hint message indicating possible expired/revoked session or
network/refresh-token issues (mentioning refresh token expiration or session
revocation and suggesting re-authentication or checking network), rather than
"Check your configuration and try again."; reference load_refreshed_credentials,
refresh_credentials and the error::system invocation so you change the hint text
in that closure.

---

Nitpick comments:
In `@src/commands/auth/token.rs`:
- Around line 19-31: The Token subcommand defines its own --url flag (Token.url)
while the global GlobalArgs already provides a --url used via global.aqora_url()
in the token() function, causing overlapping flags; either remove the Token.url
field and always use global.aqora_url(), or update the Token.url arg help to
explicitly state it overrides the global --url so precedence is clear (and keep
the existing match in token() that prefers args.url over global.aqora_url());
reference Token struct, its url field, and the token(args: Token, global:
GlobalArgs) -> Result() function where global.aqora_url() is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fed5c58-021a-4317-9c4f-e56a88c36455

📥 Commits

Reviewing files that changed from the base of the PR and between 99f40e1 and 1ace39e.

📒 Files selected for processing (4)
  • src/commands/auth/mod.rs
  • src/commands/auth/token.rs
  • src/commands/mod.rs
  • src/credentials.rs

Comment on lines +34 to +41
let loaded = load_refreshed_credentials(&path, &url, &client)
.await
.map_err(|e| {
error::system(
&format!("Failed to load credentials for {url}: {e}"),
"Check your configuration and try again.",
)
})?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: error hint may mislead for expired/revoked refresh tokens.

load_refreshed_credentials hits the network via refresh_credentials when the cached token is expired, so failures here commonly surface as "refresh token invalid / session revoked / network error" rather than a configuration problem. The current hint "Check your configuration and try again." will send users down the wrong path for the most common case (session expired after being logged in a while).

💡 Suggested wording tweak
         .map_err(|e| {
             error::system(
                 &format!("Failed to load credentials for {url}: {e}"),
-                "Check your configuration and try again.",
+                "Your session may have expired or the API is unreachable. Try running 'aqora login' again, or check your network/configuration.",
             )
         })?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/auth/token.rs` around lines 34 - 41, The error hint when
load_refreshed_credentials(...) fails is misleading for refresh-related
failures; update the error::system call in the error mapping (the closure passed
to load_refreshed_credentials) to use a more accurate hint message indicating
possible expired/revoked session or network/refresh-token issues (mentioning
refresh token expiration or session revocation and suggesting re-authentication
or checking network), rather than "Check your configuration and try again.";
reference load_refreshed_credentials, refresh_credentials and the error::system
invocation so you change the hint text in that closure.

@stubbi stubbi added this pull request to the merge queue Apr 24, 2026
Merged via the queue into main with commit fe7c4e1 Apr 24, 2026
17 checks passed
stubbi added a commit to aqora-io/skills that referenced this pull request Apr 24, 2026
Try the refresh-aware 'aqora auth token' path before falling back to
reading credentials.json directly. Older aqora-cli versions that do not
yet ship the auth subcommand fall through transparently.

When 'aqora auth token' is available, expired access tokens are
refreshed automatically via the refresh_token and do not surface as
INVALID_AUTHORIZATION errors for the user. This closes the
roughly-24-hour footgun documented in the repo's known issues and
mirrors the pattern from aqora-io/cli#187 (merged).

Co-Authored-By: Claude Opus 4.7 (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.

2 participants