Skip to content

fix: surface actionable auth hint on validate, transform and publishing commands - #300

Open
MuhammadRafay1 wants to merge 2 commits into
devfrom
error-message-enhancement
Open

fix: surface actionable auth hint on validate, transform and publishing commands#300
MuhammadRafay1 wants to merge 2 commits into
devfrom
error-message-enhancement

Conversation

@MuhammadRafay1

@MuhammadRafay1 MuhammadRafay1 commented Jul 28, 2026

Copy link
Copy Markdown

Fix misleading auth error messages across CLI commands

This PR fixes gaps in Gap#2 found by QA and reported here https://github.com/apimatic/apimatic-io/issues/2180
Note: These are not regression errors from previous changes on this branch, instead new issues identified

Four commands reported unauthenticated and invalid-credential states with stale,
duplicated, bare, or outright wrong messages. None of them reached the
ServiceError.unauthorizedWithHint factory introduced earlier on this branch.

Command Before After
api validate …Please run 'auth:login' or provide a valid auth key. You are not authorized to perform this action. Please run apimatic auth login to log in, or provide a valid auth key using the --auth-key flag.
api transform API reason + the same legacy string appended (doubled message) Authorization has been denied for this request. Please run apimatic auth login to log in, or provide a valid auth key using the --auth-key flag.
publishing profile list, sdk publish Unauthorized access. same hint as api validate
auth status (logged out) Invalid API key provided. You are not logged in. Please run apimatic auth login to log in.
auth status (no config file) nothing — silent Failed same message as above
auth status (stored key rejected) Invalid API key provided. unchanged — now the only case that reaches it

Two commits: the service-layer messages, then auth status.

The blank-config bug behind most of this

removeAuthInfo (auth logout) blanks config.json instead of deleting it:

const credentials: AuthInfo = { email: "", authKey: "" };

So getAuthInfo returns a non-null AuthInfo with an empty key, and every
authInfo === null guard in the codebase silently fails to fire for a logged-out
user. Those call sites sent a request with an empty X-Auth-Key purely to
receive a 401, then rendered whatever that 401 mapped to.

That is why publishing profile list showed the bare singleton message, and why
auth status claimed an invalid key had been "provided" to a command that
accepts no auth-key flag. Guards now test the key, not the object:

-if (authInfo === null) {
+if (!authInfo?.authKey) {

Besides fixing the message, this removes a pointless network round-trip from
every logged-out invocation (auth status drops from ~1.5s to ~1s).

auth status specifics

Two separate defects, one condition:

  • Logged out (blank config.json): fell through the guard, got a wire 401,
    and printed the login flow's "invalid key" copy. Nothing was provided — and
    StatusAction.execute(authKey) is always called with null from
    commands/auth/status.ts, so a provided key is impossible here.
  • Never logged in (no config.json): the guard did fire, but returned
    ActionResult.failed() without printing anything — a bare Failed with no
    explanation.

With the guard fixed, both print the actionable "not logged in" message, and
Invalid API key provided. is now reachable only where it's accurate: a
non-empty stored key that the API rejects as expired or revoked. Verified in all
three states, including with a planted invalid key.

Service-layer message fixes

  • validation-service and transformation-service each keep a private
    message catalog and never call the shared mapper, so both carried the legacy
    'auth:login' text — a command syntax this CLI doesn't have. transform
    additionally concatenated it on top of the API's own 401 reason, producing a
    visibly doubled message.
  • Only the 401 branches were redirected to the shared factory. Existing
    400/403/500 text in both catalogs is untouched, so no non-auth message changes.
  • All three publishing pre-checks were fixed, not just the reported
    profile list one — they share the identical condition, and fixing one would
    leave sdk publish printing the bare string.

Decisions

  • handleServiceError's axios-401 branch was not touched.
    prompts/auth/status.ts identity-compares against the
    ServiceError.UnAuthorized singleton to select its invalid-key message;
    unauthorizedWithHint returns a fresh instance, so changing that branch would
    silently break it.

Known gap:

Below Gap is reported by QA as Gap#3 in the Test report added earlier in the message.
This will be handled in a seperate issue here #301. This is a Bug.

  1. portal toc new when unauthenticated — separate issue. It swallows the
    401 into "falling back to the default TOC structure", overwrites a real
    toc.yml with an empty-component one, returns exit 0, and then hangs
    indefinitely: generateTocData is a stream-response endpoint, so the SDK
    returns ApiError.body as an undrained IncomingMessage, the TLS socket stays
    open, and outro() only sets process.exitCode rather than calling
    process.exit. Confirmed via handle probe (TLSSocket … readable=true);
    destroying that stream turns a 90s hang into a 3s exit. Latent in any
    stream-endpoint error, including the portal/SDK downloads.

MuhammadRafay1 and others added 2 commits July 28, 2026 16:32
…ng errors

Three unauthenticated code paths bypassed the shared unauthorizedWithHint
factory and printed stale or bare messages:

- api validate had its own message catalog referencing a nonexistent
  'auth:login' command syntax
- api transform concatenated the API's own 401 reason with the same legacy
  string, rendering a doubled message
- publishing profile list and sdk publish returned the bare
  ServiceError.UnAuthorized singleton ("Unauthorized access.")

The publishing pre-checks additionally tested the wrong condition: auth
logout blanks config.json rather than deleting it, so getAuthInfo returns a
non-null AuthInfo with an empty key. A logged-out user therefore skipped the
pre-check, sent an empty X-Auth-Key and got a wire 401 mapped to the bare
singleton. The guard now checks the key rather than the object.

Both service-level message catalogs keep their existing 400/403/500 text; only
the 401 branches were redirected, so no non-auth message changes.

Verified end to end against a logged-out config: all three commands now print
the login command and the --auth-key flag. Cognitive complexity is unchanged
for every function touched (sonarjs S3776).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
auth status printed "Invalid API key provided." when the user was simply
logged out, and printed nothing at all when config.json was absent.

Both stemmed from the same wrong condition. removeAuthInfo (auth logout) blanks
config.json rather than deleting it, so getAuthInfo returns a non-null AuthInfo
with an empty key. The action's `accountInfo === null` guard therefore never
fired for a logged-out user: it sent /account/profile a request with an empty
X-Auth-Key purely to receive a 401, which was then rendered with the login
flow's "invalid key" copy — misleading, since auth status accepts no auth key
flag at all. When config.json was genuinely missing the guard did fire, but
returned a failed ActionResult without printing any message.

The guard now tests the key rather than the object, so both logged-out shapes
are caught locally and print an actionable message with no wasted request.
"Invalid API key provided." is now only reachable where it is accurate: a
non-empty stored key that the API rejects as expired or revoked.

Verified in all three states: blank config.json, missing config.json, and a
planted invalid key.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@sonarqubecloud

Copy link
Copy Markdown

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