From a1ab84045a389544fe73866286dd0acc9bd39c16 Mon Sep 17 00:00:00 2001 From: MuhammadRafay1 Date: Tue, 28 Jul 2026 16:32:13 +0500 Subject: [PATCH 1/2] fix: surface actionable auth hint on validate, transform and publishing 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) --- .../services/publishing-api-service.ts | 18 ++++++++++++------ .../services/transformation-service.ts | 3 ++- .../services/validation-service.ts | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/infrastructure/services/publishing-api-service.ts b/src/infrastructure/services/publishing-api-service.ts index e207c638..f03f0b55 100644 --- a/src/infrastructure/services/publishing-api-service.ts +++ b/src/infrastructure/services/publishing-api-service.ts @@ -24,8 +24,10 @@ export class PublishingApiService { shell: string ): Promise> { const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString()); - if (authInfo === null) { - return err(ServiceError.UnAuthorized); + // `auth logout` blanks config.json rather than deleting it, so a logged-out user + // still has a non-null AuthInfo with an empty key — check the key, not the object. + if (!authInfo?.authKey) { + return err(ServiceError.unauthorizedWithHint(null)); } try { @@ -51,8 +53,10 @@ export class PublishingApiService { shell: string ): Promise> { const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString()); - if (authInfo === null) { - return err(ServiceError.UnAuthorized); + // `auth logout` blanks config.json rather than deleting it, so a logged-out user + // still has a non-null AuthInfo with an empty key — check the key, not the object. + if (!authInfo?.authKey) { + return err(ServiceError.unauthorizedWithHint(null)); } const sdkFileStream = await this.fileService.getStream(sdkFilePath); @@ -107,8 +111,10 @@ export class PublishingApiService { shell: string ): Promise> { const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString()); - if (authInfo === null) { - return err(ServiceError.UnAuthorized); + // `auth logout` blanks config.json rather than deleting it, so a logged-out user + // still has a non-null AuthInfo with an empty key — check the key, not the object. + if (!authInfo?.authKey) { + return err(ServiceError.unauthorizedWithHint(null)); } try { diff --git a/src/infrastructure/services/transformation-service.ts b/src/infrastructure/services/transformation-service.ts index 677aa186..119d4268 100644 --- a/src/infrastructure/services/transformation-service.ts +++ b/src/infrastructure/services/transformation-service.ts @@ -17,6 +17,7 @@ import { apiClientFactory } from "./api-client-factory.js"; import { FilePath } from "../../types/file/filePath.js"; import { CommandMetadata } from "../../types/common/command-metadata.js"; import { err, ok, Result} from "neverthrow"; +import { ServiceError } from "../service-error.js"; export interface TransformViaFileParams { file: FilePath; @@ -86,7 +87,7 @@ export class TransformationService { return "Your API Definition is invalid. Please use the APIMatic VS Code Extension to fix the errors and try again."; } else if (apiError.statusCode === 401) { const message = JSON.parse(apiError.body as string).message; - return `${message} You are not authorized to perform this action. Please run 'auth:login' or provide a valid auth key.`; + return ServiceError.unauthorizedWithHint(message).errorMessage; } return `Error ${apiError.statusCode}: An error occurred during the transformation. Please try again or contact support@apimatic.io for assistance.`; } else { diff --git a/src/infrastructure/services/validation-service.ts b/src/infrastructure/services/validation-service.ts index 8a20d753..462af9dd 100644 --- a/src/infrastructure/services/validation-service.ts +++ b/src/infrastructure/services/validation-service.ts @@ -247,7 +247,7 @@ export class ValidationService { case 400: return "Your API Definition is invalid. Please fix the issues and try again."; case 401: - return "You are not authorized to perform this action. Please run 'auth:login' or provide a valid auth key."; + return ServiceError.unauthorizedWithHint(null).errorMessage; case 403: return "You do not have permission to perform this action."; case 500: From 2993889c62306eefc14ed539cb3b37dcc580fa2f Mon Sep 17 00:00:00 2001 From: MuhammadRafay1 Date: Tue, 28 Jul 2026 18:11:12 +0500 Subject: [PATCH 2/2] fix: report logged-out state correctly in auth status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/actions/auth/status.ts | 6 +++++- src/prompts/auth/status.ts | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/actions/auth/status.ts b/src/actions/auth/status.ts index e77432bd..fc042abe 100644 --- a/src/actions/auth/status.ts +++ b/src/actions/auth/status.ts @@ -13,7 +13,11 @@ export class StatusAction { public async execute(authKey: string | null): Promise { const accountInfo = await getAuthInfo(this.configDir.toString()); - if (accountInfo === null) { + // `auth logout` blanks config.json rather than deleting it, so a logged-out user still + // has a non-null AuthInfo with an empty key. Checking the key catches both that and a + // missing config file, and avoids a request that can only come back 401. + if (!accountInfo?.authKey) { + this.prompts.notLoggedIn(); return ActionResult.failed(); } const result = await this.prompts.accountInfoSpinner( diff --git a/src/prompts/auth/status.ts b/src/prompts/auth/status.ts index 32fc48e6..5b3f9786 100644 --- a/src/prompts/auth/status.ts +++ b/src/prompts/auth/status.ts @@ -16,12 +16,17 @@ export class StatusPrompts { ); } + public notLoggedIn() { + log.error(`You are not logged in. Please run ${format.cmdAlt("apimatic", "auth", "login")} to log in.`); + } + public invalidKeyProvided(serviceError: ServiceError) { const message = serviceError === ServiceError.UnAuthorized ? "Invalid API key provided." : serviceError.errorMessage; log.error(message); } + public showAccountInfo(info: SubscriptionInfo) { const languages = mapLanguages(info.allowedLanguages); const message = `Account Information: