From 658cc0636d716b92f3a0ae8838a933a473a9150d Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 00:11:12 +0000 Subject: [PATCH 01/16] fix(deploy): throw modeled error on missing env file --- src/cli/commands/deploy/actions.ts | 6 +-- .../deploy/__tests__/assert-env-file.test.ts | 51 ++++++++++++------- .../operations/deploy/pre-deploy-identity.ts | 15 ++++-- src/lib/errors/types.ts | 9 ++++ src/lib/result.ts | 20 +++++++- 5 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 404291381..b9070c3e7 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -192,10 +192,10 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise { vi.clearAllMocks(); }); - it('returns null when no credentials exist (file missing is fine)', () => { + it('returns ok when no credentials exist (file missing is fine)', () => { mockExistsSync.mockReturnValue(false); const result = assertEnvFileExists(makeSpec(), BASE_DIR); - expect(result).toBeNull(); + expect(result.success).toBe(true); }); - it('returns null when file exists', () => { + it('returns ok when file exists', () => { mockExistsSync.mockReturnValue(true); const spec = makeSpec({ credentials: [{ name: 'mykey', authorizerType: 'ApiKeyCredentialProvider' } as any], }); const result = assertEnvFileExists(spec, BASE_DIR); - expect(result).toBeNull(); + expect(result.success).toBe(true); }); it('lists ApiKey env vars when file is missing', () => { @@ -57,8 +57,11 @@ describe('assertEnvFileExists', () => { credentials: [{ name: 'openai', authorizerType: 'ApiKeyCredentialProvider' } as any], }); const result = assertEnvFileExists(spec, BASE_DIR); - expect(result).toContain('agentcore/.env.local not found'); - expect(result).toContain('AGENTCORE_CREDENTIAL_OPENAI'); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.message).toContain('agentcore/.env.local not found'); + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_OPENAI'); + } }); it('lists OAuth2 env vars when file is missing', () => { @@ -67,8 +70,11 @@ describe('assertEnvFileExists', () => { credentials: [{ name: 'google-oauth', authorizerType: 'OAuthCredentialProvider' } as any], }); const result = assertEnvFileExists(spec, BASE_DIR); - expect(result).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_ID'); - expect(result).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_SECRET'); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_ID'); + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_GOOGLE_OAUTH_CLIENT_SECRET'); + } }); it('lists CoinbaseCDP payment env vars when file is missing', () => { @@ -83,9 +89,12 @@ describe('assertEnvFileExists', () => { ], }); const result = assertEnvFileExists(spec, BASE_DIR); - expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID'); - expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_SECRET'); - expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_WALLET_SECRET'); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID'); + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_SECRET'); + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_WALLET_SECRET'); + } }); it('lists StripePrivy payment env vars when file is missing', () => { @@ -102,10 +111,13 @@ describe('assertEnvFileExists', () => { ], }); const result = assertEnvFileExists(spec, BASE_DIR); - expect(result).toContain('APP_ID'); - expect(result).toContain('APP_SECRET'); - expect(result).toContain('AUTHORIZATION_PRIVATE_KEY'); - expect(result).toContain('AUTHORIZATION_ID'); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.message).toContain('APP_ID'); + expect(result.error.message).toContain('APP_SECRET'); + expect(result.error.message).toContain('AUTHORIZATION_PRIVATE_KEY'); + expect(result.error.message).toContain('AUTHORIZATION_ID'); + } }); it('combines all credential types in a single error', () => { @@ -124,9 +136,12 @@ describe('assertEnvFileExists', () => { ], }); const result = assertEnvFileExists(spec, BASE_DIR); - expect(result).toContain('AGENTCORE_CREDENTIAL_OPENAI'); - expect(result).toContain('AGENTCORE_CREDENTIAL_GOOGLE_CLIENT_ID'); - expect(result).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID'); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_OPENAI'); + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_GOOGLE_CLIENT_ID'); + expect(result.error.message).toContain('AGENTCORE_CREDENTIAL_PAYMGR_CDPCONN_CDP_API_KEY_ID'); + } }); }); diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index 84cb9186a..9c5ca6106 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -1,4 +1,4 @@ -import { SecureCredentials, readEnvFile } from '../../../lib'; +import { MissingCredentialsError, SecureCredentials, readEnvFile } from '../../../lib'; import type { AgentCoreProjectSpec, Credential } from '../../../schema'; import { getCredentialProvider } from '../../aws'; import { @@ -23,6 +23,7 @@ import { updateApiKeyProvider, updateOAuth2Provider, } from '../identity'; +import { type Result, err, ok } from '@/lib/result'; import { BedrockAgentCoreControlClient, GetTokenVaultCommand } from '@aws-sdk/client-bedrock-agentcore-control'; import { CreateKeyCommand, KMSClient } from '@aws-sdk/client-kms'; import { existsSync } from 'fs'; @@ -302,15 +303,19 @@ export function getAllCredentials(projectSpec: AgentCoreProjectSpec): MissingCre * so the user can populate the file in one shot rather than discovering missing vars * one at a time across separate setup steps. */ -export function assertEnvFileExists(projectSpec: AgentCoreProjectSpec, configBaseDir: string): string | null { +export function assertEnvFileExists(projectSpec: AgentCoreProjectSpec, configBaseDir: string): Result { const allCredentials = getAllCredentials(projectSpec); - if (allCredentials.length === 0) return null; + if (allCredentials.length === 0) return ok(); const envFilePath = join(configBaseDir, '.env.local'); - if (existsSync(envFilePath)) return null; + if (existsSync(envFilePath)) return ok(); const varList = allCredentials.map(c => ` ${c.envVarName}`).join('\n'); - return `agentcore/.env.local not found. Credentials require environment variables.\n\nRequired variables:\n${varList}\n\nTo fix: create agentcore/.env.local with the variables above, or re-run the relevant 'agentcore add' command to enter credentials interactively.`; + return err( + new MissingCredentialsError( + `agentcore/.env.local not found. Credentials require environment variables.\n\nRequired variables:\n${varList}\n\nTo fix: create agentcore/.env.local with the variables above, or re-run the relevant 'agentcore add' command to enter credentials interactively.` + ) + ); } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/lib/errors/types.ts b/src/lib/errors/types.ts index db9d4f5e9..0aa5b0ef6 100644 --- a/src/lib/errors/types.ts +++ b/src/lib/errors/types.ts @@ -100,6 +100,15 @@ export class AwsCredentialsError extends BaseError { } } +/** + * Error thrown when non-AWS credentials are missing but required.. (ex. API Keys) + */ +export class MissingCredentialsError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'user', ...options }); + } +} + /** * Error indicating a packaging operation failed. */ diff --git a/src/lib/result.ts b/src/lib/result.ts index b79a9919a..edf7cd321 100644 --- a/src/lib/result.ts +++ b/src/lib/result.ts @@ -1,5 +1,7 @@ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- discriminated union member; interface would allow declaration merging which breaks type narrowing type FailureResult = { success: false; error: E }; + +type SuccessResult> = { success: true } & T; /** * Discriminated union for fallible operations, inspired by Rust's Result. * @@ -13,7 +15,7 @@ type FailureResult = { success: false; error: E }; */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export type Result = {}, E extends Error = Error> = - | ({ success: true } & T) + | SuccessResult | FailureResult; /** @@ -62,3 +64,19 @@ export function failureResult(e: E): FailureResult { error: e, }; } + +export function ok(): SuccessResult>; +export function ok>(data: T): SuccessResult; +export function ok(data?: Record) { + return { + success: true, + ...(data ?? {}), + }; +} + +export function err(e: E): FailureResult { + return { + success: false, + error: e, + }; +} From c9e10ff5e19a858e91ba35a450e491dbf1086c6e Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 01:40:44 +0000 Subject: [PATCH 02/16] fix(deploy): propagate api key error to top level --- .../operations/deploy/pre-deploy-identity.ts | 42 +++++++------- .../api-key-credential-provider.test.ts | 57 ++++++++++++++----- .../identity/api-key-credential-provider.ts | 33 +++++------ 3 files changed, 76 insertions(+), 56 deletions(-) diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index 9c5ca6106..f8fd10a05 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -1,4 +1,4 @@ -import { MissingCredentialsError, SecureCredentials, readEnvFile } from '../../../lib'; +import { AwsCredentialsError, MissingCredentialsError, SecureCredentials, readEnvFile, toError } from '../../../lib'; import type { AgentCoreProjectSpec, Credential } from '../../../schema'; import { getCredentialProvider } from '../../aws'; import { @@ -37,7 +37,7 @@ export interface ApiKeyProviderSetupResult { providerName: string; status: 'created' | 'updated' | 'exists' | 'skipped' | 'error'; credentialProviderArn?: string; - error?: string; + error?: Error; } export interface PreDeployIdentityResult { @@ -87,7 +87,7 @@ export async function setupApiKeyProviders(options: SetupApiKeyProvidersOptions) { providerName: 'TokenVault', status: 'error', - error: `Failed to configure KMS: ${kmsResult.error}`, + error: new Error(`Failed to configure KMS: ${kmsResult.error}`), }, ], hasErrors: true, @@ -118,7 +118,7 @@ async function setupTokenVaultKms( region: string, credentials: ReturnType, projectSpec: AgentCoreProjectSpec -): Promise<{ success: boolean; keyArn?: string; error?: string }> { +): Promise> { try { const controlClient = new BedrockAgentCoreControlClient({ region, credentials }); @@ -129,7 +129,7 @@ async function setupTokenVaultKms( vaultResponse.kmsConfiguration?.keyType === 'CustomerManagedKey' && vaultResponse.kmsConfiguration.kmsKeyArn ) { - return { success: true, keyArn: vaultResponse.kmsConfiguration.kmsKeyArn }; + return ok({ keyArn: vaultResponse.kmsConfiguration.kmsKeyArn }); } } catch { // Vault may not exist yet or access denied — fall through to create key @@ -145,17 +145,14 @@ async function setupTokenVaultKms( ); const keyArn = response.KeyMetadata?.Arn; if (!keyArn) { - return { success: false, error: 'Failed to create KMS key' }; + return err(new Error('Failed to create KMS key')); } const result = await setTokenVaultKmsKey(controlClient, keyArn); - if (!result.success) { - return { success: false, error: result.error }; - } - - return { success: true, keyArn }; + if (!result.success) return result; + return ok({ keyArn }); } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) }; + return err(toError(error)); } } @@ -171,7 +168,7 @@ async function setupApiKeyCredentialProvider( return { providerName: credential.name, status: 'skipped', - error: `No ${envVarName} found in agentcore/.env.local`, + error: new Error(`No ${envVarName} found in agentcore/.env.local`), }; } @@ -183,8 +180,8 @@ async function setupApiKeyCredentialProvider( return { providerName: credential.name, status: updateResult.success ? 'updated' : 'error', - credentialProviderArn: updateResult.credentialProviderArn, - error: updateResult.error, + credentialProviderArn: updateResult.success ? updateResult.credentialProviderArn : undefined, + error: updateResult.success ? undefined : updateResult.error, }; } @@ -192,22 +189,23 @@ async function setupApiKeyCredentialProvider( return { providerName: credential.name, status: createResult.success ? 'created' : 'error', - credentialProviderArn: createResult.credentialProviderArn, - error: createResult.error, + credentialProviderArn: createResult.success ? createResult.credentialProviderArn : undefined, + error: createResult.success ? undefined : createResult.error, }; } catch (error) { // Provide clearer error message for AWS credentials issues - let errorMessage: string; if (isNoCredentialsError(error)) { - errorMessage = `AWS credentials not found. ${await getAwsLoginGuidance()}`; - } else { - errorMessage = error instanceof Error ? error.message : String(error); + return { + providerName: credential.name, + status: 'error', + error: new AwsCredentialsError(`AWS credentials not found. ${await getAwsLoginGuidance()}`), + }; } return { providerName: credential.name, status: 'error', - error: errorMessage, + error: error as Error, }; } } diff --git a/src/cli/operations/identity/__tests__/api-key-credential-provider.test.ts b/src/cli/operations/identity/__tests__/api-key-credential-provider.test.ts index e7cbafb62..8018b6c1e 100644 --- a/src/cli/operations/identity/__tests__/api-key-credential-provider.test.ts +++ b/src/cli/operations/identity/__tests__/api-key-credential-provider.test.ts @@ -65,28 +65,51 @@ describe('apiKeyProviderExists', () => { describe('createApiKeyProvider', () => { afterEach(() => vi.clearAllMocks()); - it('returns success on creation', async () => { - mockSend.mockResolvedValue({}); + it('returns success with credentialProviderArn', async () => { + mockSend.mockResolvedValueOnce({}); // create + mockSend.mockResolvedValueOnce({ credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov' }); // get const result = await createApiKeyProvider(makeMockClient(), 'prov', 'key123'); - expect(result).toEqual({ success: true }); + expect(result).toEqual({ + success: true, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov', + }); }); - it('returns success on ConflictException (idempotent)', async () => { + it('returns success on ConflictException with ARN from get', async () => { const err = new Error('conflict'); Object.defineProperty(err, 'name', { value: 'ConflictException' }); - mockSend.mockRejectedValue(err); + mockSend.mockRejectedValueOnce(err); + mockSend.mockResolvedValueOnce({ credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov' }); // get const result = await createApiKeyProvider(makeMockClient(), 'prov', 'key123'); - expect(result).toEqual({ success: true }); + expect(result).toEqual({ + success: true, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov', + }); }); - it('returns success on ResourceAlreadyExistsException', async () => { + it('returns success on ResourceAlreadyExistsException with ARN from get', async () => { const err = new Error('exists'); Object.defineProperty(err, 'name', { value: 'ResourceAlreadyExistsException' }); - mockSend.mockRejectedValue(err); + mockSend.mockRejectedValueOnce(err); + mockSend.mockResolvedValueOnce({ credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov' }); // get + + const result = await createApiKeyProvider(makeMockClient(), 'prov', 'key123'); + + expect(result).toEqual({ + success: true, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov', + }); + }); + + it('returns success without ARN when conflict get fails', async () => { + const conflictErr = new Error('conflict'); + Object.defineProperty(conflictErr, 'name', { value: 'ConflictException' }); + mockSend.mockRejectedValueOnce(conflictErr); + mockSend.mockRejectedValueOnce(new Error('get failed')); // get fails const result = await createApiKeyProvider(makeMockClient(), 'prov', 'key123'); @@ -99,17 +122,23 @@ describe('createApiKeyProvider', () => { const result = await createApiKeyProvider(makeMockClient(), 'prov', 'key123'); expect(result.success).toBe(false); - expect(result.error).toBe('unexpected'); + expect((result as { success: false; error: Error }).error.message).toBe('unexpected'); }); }); describe('updateApiKeyProvider', () => { afterEach(() => vi.clearAllMocks()); - it('returns success on update', async () => { - mockSend.mockResolvedValue({}); + it('returns success with credentialProviderArn', async () => { + mockSend.mockResolvedValueOnce({}); // update + mockSend.mockResolvedValueOnce({ credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov' }); // get + + const result = await updateApiKeyProvider(makeMockClient(), 'prov', 'newkey'); - expect(await updateApiKeyProvider(makeMockClient(), 'prov', 'newkey')).toEqual({ success: true }); + expect(result).toEqual({ + success: true, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123:provider/prov', + }); }); it('returns failure on error', async () => { @@ -118,7 +147,7 @@ describe('updateApiKeyProvider', () => { const result = await updateApiKeyProvider(makeMockClient(), 'prov', 'newkey'); expect(result.success).toBe(false); - expect(result.error).toBe('update fail'); + expect((result as { success: false; error: Error }).error.message).toBe('update fail'); }); }); @@ -137,6 +166,6 @@ describe('setTokenVaultKmsKey', () => { const result = await setTokenVaultKmsKey(makeMockClient(), 'arn:aws:kms:key'); expect(result.success).toBe(false); - expect(result.error).toBe('kms fail'); + expect((result as { success: false; error: Error }).error.message).toBe('kms fail'); }); }); diff --git a/src/cli/operations/identity/api-key-credential-provider.ts b/src/cli/operations/identity/api-key-credential-provider.ts index 36ef82619..0d8040371 100644 --- a/src/cli/operations/identity/api-key-credential-provider.ts +++ b/src/cli/operations/identity/api-key-credential-provider.ts @@ -5,6 +5,8 @@ * as CDK constructs. These operations run as a pre-deploy step outside the * main CDK synthesis/deploy path. */ +import { type Result, toError } from '@/lib'; +import { err, ok } from '@/lib/result'; import { BedrockAgentCoreControlClient, CreateApiKeyCredentialProviderCommand, @@ -40,7 +42,7 @@ export async function createApiKeyProvider( client: BedrockAgentCoreControlClient, providerName: string, apiKey: string -): Promise<{ success: boolean; credentialProviderArn?: string; error?: string }> { +): Promise> { try { await client.send( new CreateApiKeyCredentialProviderCommand({ @@ -50,21 +52,18 @@ export async function createApiKeyProvider( ); // Create response doesn't include credentialProviderArn — fetch it const getResponse = await client.send(new GetApiKeyCredentialProviderCommand({ name: providerName })); - return { success: true, credentialProviderArn: getResponse.credentialProviderArn }; + return ok({ credentialProviderArn: getResponse.credentialProviderArn }); } catch (error) { const errorName = (error as { name?: string }).name; if (errorName === 'ConflictException' || errorName === 'ResourceAlreadyExistsException') { try { const getResponse = await client.send(new GetApiKeyCredentialProviderCommand({ name: providerName })); - return { success: true, credentialProviderArn: getResponse.credentialProviderArn }; + return ok({ credentialProviderArn: getResponse.credentialProviderArn }); } catch { - return { success: true }; + return ok(); } } - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + return err(toError(error)); } } @@ -75,7 +74,7 @@ export async function updateApiKeyProvider( client: BedrockAgentCoreControlClient, providerName: string, apiKey: string -): Promise<{ success: boolean; credentialProviderArn?: string; error?: string }> { +): Promise> { try { await client.send( new UpdateApiKeyCredentialProviderCommand({ @@ -85,12 +84,9 @@ export async function updateApiKeyProvider( ); // Update response doesn't include credentialProviderArn — fetch it const getResponse = await client.send(new GetApiKeyCredentialProviderCommand({ name: providerName })); - return { success: true, credentialProviderArn: getResponse.credentialProviderArn }; + return ok({ credentialProviderArn: getResponse.credentialProviderArn }); } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + return err(toError(error)); } } @@ -102,7 +98,7 @@ export async function setTokenVaultKmsKey( client: BedrockAgentCoreControlClient, kmsKeyArn: string, tokenVaultId?: string -): Promise<{ success: boolean; error?: string }> { +): Promise { try { await client.send( new SetTokenVaultCMKCommand({ @@ -113,11 +109,8 @@ export async function setTokenVaultKmsKey( }, }) ); - return { success: true }; + return ok(); } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + return err(toError(error)); } } From e6275bde684573c9c18517c43c007b434574842a Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 02:34:30 +0000 Subject: [PATCH 03/16] fix(deploy): bring identity and oauth errors up to top level --- src/cli/commands/deploy/actions.ts | 19 ++++++-- .../__tests__/pre-deploy-identity.test.ts | 9 +++- .../operations/deploy/pre-deploy-identity.ts | 47 ++++++++++-------- .../oauth2-credential-provider.test.ts | 48 +++++++------------ .../identity/oauth2-credential-provider.ts | 47 ++++++++---------- 5 files changed, 86 insertions(+), 84 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index b9070c3e7..865cf97bd 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -227,12 +227,17 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise r.status === 'error'); + const errorResult = identityResult.results.find(r => r.status === 'error' && r.error); const errorMsg = errorResult?.error && typeof errorResult.error === 'string' ? errorResult.error : 'Identity setup failed'; endStep('error', errorMsg); logger.finalize(false); - return { success: false, error: new Error(errorMsg), logPath: logger.getRelativeLogPath() }; + // + return { + success: false, + error: errorResult?.error ?? new Error('unknown error ocurred'), + logPath: logger.getRelativeLogPath(), + }; } identityKmsKeyArn = identityResult.kmsKeyArn; @@ -259,12 +264,16 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise r.status === 'error'); + const errorResult = oauthResult.results.find(r => r.status === 'error' && r.error); logger.log(`OAuth setup error: ${errorResult?.error ?? 'unknown'}`, 'error'); const errorMsg = 'OAuth credential setup failed. Check the log for details.'; endStep('error', errorMsg); logger.finalize(false); - return { success: false, error: new Error(errorMsg), logPath: logger.getRelativeLogPath() }; + return { + success: false, + error: errorResult?.error ?? new Error('unknown error'), + logPath: logger.getRelativeLogPath(), + }; } // Collect OAuth credential ARNs for deployed state @@ -362,7 +371,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise ({ } }, readEnvFile: mockReadEnvFile, + MissingCredentialsError: class MissingCredentialsError extends Error { + constructor(message: string) { + super(message); + this.name = 'MissingCredentialsError'; + } + }, + toError: (e: unknown) => (e instanceof Error ? e : new Error(String(e))), })); vi.mock('../../../aws/index.js', () => ({ @@ -359,7 +366,7 @@ describe('setupOAuth2Providers', () => { expect(result.hasErrors).toBe(false); expect(result.results).toHaveLength(1); expect(result.results[0]!.status).toBe('skipped'); - expect(result.results[0]!.error).toContain('Missing'); + expect(result.results[0]!.error?.message).toContain('Missing'); }); it('returns error on failure', async () => { diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index f8fd10a05..b901a47a6 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -1,4 +1,11 @@ -import { AwsCredentialsError, MissingCredentialsError, SecureCredentials, readEnvFile, toError } from '../../../lib'; +import { + AwsCredentialsError, + MissingCredentialsError, + SecureCredentials, + ValidationError, + readEnvFile, + toError, +} from '../../../lib'; import type { AgentCoreProjectSpec, Credential } from '../../../schema'; import { getCredentialProvider } from '../../aws'; import { @@ -323,7 +330,7 @@ export function assertEnvFileExists(projectSpec: AgentCoreProjectSpec, configBas export interface OAuth2ProviderSetupResult { providerName: string; status: 'created' | 'updated' | 'skipped' | 'error'; - error?: string; + error?: Error; credentialProviderArn?: string; clientSecretArn?: string; callbackUrl?: string; @@ -382,7 +389,7 @@ async function setupSingleOAuth2Provider( credentials: SecureCredentials ): Promise { if (credential.authorizerType !== 'OAuthCredentialProvider') { - return { providerName: credential.name, status: 'error', error: 'Invalid credential type' }; + return { providerName: credential.name, status: 'error', error: new ValidationError('Invalid credential type') }; } const nameKey = credential.name.toUpperCase().replace(/-/g, '_'); @@ -396,7 +403,7 @@ async function setupSingleOAuth2Provider( return { providerName: credential.name, status: 'skipped', - error: `Missing ${clientIdEnvVar} or ${clientSecretEnvVar} in agentcore/.env.local`, + error: new MissingCredentialsError(`Missing ${clientIdEnvVar} or ${clientSecretEnvVar} in agentcore/.env.local`), }; } @@ -406,7 +413,9 @@ async function setupSingleOAuth2Provider( return { providerName: credential.name, status: 'skipped', - error: `No discoveryUrl configured for "${credential.name}". Provider already exists in Identity service — credentials in .env.local will be ignored.`, + error: new MissingCredentialsError( + `No discoveryUrl configured for "${credential.name}". Provider already exists in Identity service — credentials in .env.local will be ignored.` + ), }; } @@ -426,10 +435,10 @@ async function setupSingleOAuth2Provider( return { providerName: credential.name, status: updateResult.success ? 'updated' : 'error', - error: updateResult.error, - credentialProviderArn: updateResult.result?.credentialProviderArn, - clientSecretArn: updateResult.result?.clientSecretArn, - callbackUrl: updateResult.result?.callbackUrl, + error: updateResult.success ? undefined : updateResult.error, + credentialProviderArn: updateResult.success ? updateResult.credentialProviderArn : undefined, + clientSecretArn: updateResult.success ? updateResult.clientSecretArn : undefined, + callbackUrl: updateResult.success ? updateResult.callbackUrl : undefined, }; } @@ -437,19 +446,17 @@ async function setupSingleOAuth2Provider( return { providerName: credential.name, status: createResult.success ? 'created' : 'error', - error: createResult.error, - credentialProviderArn: createResult.result?.credentialProviderArn, - clientSecretArn: createResult.result?.clientSecretArn, - callbackUrl: createResult.result?.callbackUrl, + error: createResult.success ? undefined : createResult.error, + credentialProviderArn: createResult.success ? createResult.credentialProviderArn : undefined, + clientSecretArn: createResult.success ? createResult.clientSecretArn : undefined, + callbackUrl: createResult.success ? createResult.callbackUrl : undefined, }; - } catch (error) { - let errorMessage: string; - if (isNoCredentialsError(error)) { - errorMessage = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.'; - } else { - errorMessage = error instanceof Error ? error.message : String(error); + } catch (e) { + const err = toError(e); + if (isNoCredentialsError(e)) { + err.message = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.'; } - return { providerName: credential.name, status: 'error', error: errorMessage }; + return { providerName: credential.name, status: 'error', error: err }; } } diff --git a/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts b/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts index 23523dde8..6b3aa2afe 100644 --- a/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts +++ b/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts @@ -81,11 +81,9 @@ describe('createOAuth2Provider', () => { expect(result).toEqual({ success: true, - result: { - credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', - clientSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret', - callbackUrl: 'https://callback.example.com', - }, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', + clientSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret', + callbackUrl: 'https://callback.example.com', }); }); @@ -104,9 +102,7 @@ describe('createOAuth2Provider', () => { expect(result).toEqual({ success: true, - result: { - credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', - }, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', }); }); @@ -125,9 +121,7 @@ describe('createOAuth2Provider', () => { expect(result).toEqual({ success: true, - result: { - credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', - }, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', }); }); @@ -137,7 +131,7 @@ describe('createOAuth2Provider', () => { const result = await createOAuth2Provider(makeMockClient(), mockParams); expect(result.success).toBe(false); - expect(result.error).toBe('unexpected error'); + expect((result as { success: false; error: Error }).error.message).toBe('unexpected error'); }); it('returns error when no credentialProviderArn in response', async () => { @@ -145,10 +139,8 @@ describe('createOAuth2Provider', () => { const result = await createOAuth2Provider(makeMockClient(), mockParams); - expect(result).toEqual({ - success: false, - error: 'No credential provider ARN in response', - }); + expect(result.success).toBe(false); + expect((result as { success: false; error: Error }).error.message).toBe('No credential provider ARN in response'); }); }); @@ -167,11 +159,9 @@ describe('getOAuth2Provider', () => { expect(result).toEqual({ success: true, - result: { - credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', - clientSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret', - callbackUrl: 'https://callback.example.com', - }, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', + clientSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret', + callbackUrl: 'https://callback.example.com', }); }); @@ -181,7 +171,7 @@ describe('getOAuth2Provider', () => { const result = await getOAuth2Provider(makeMockClient(), 'test-provider'); expect(result.success).toBe(false); - expect(result.error).toBe('get failed'); + expect((result as { success: false; error: Error }).error.message).toBe('get failed'); }); it('returns error when no ARN', async () => { @@ -189,10 +179,8 @@ describe('getOAuth2Provider', () => { const result = await getOAuth2Provider(makeMockClient(), 'test-provider'); - expect(result).toEqual({ - success: false, - error: 'No credential provider ARN in response', - }); + expect(result.success).toBe(false); + expect((result as { success: false; error: Error }).error.message).toBe('No credential provider ARN in response'); }); }); @@ -218,10 +206,8 @@ describe('updateOAuth2Provider', () => { expect(result).toEqual({ success: true, - result: { - credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', - clientSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret', - }, + credentialProviderArn: 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/test-provider', + clientSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret', }); }); @@ -231,6 +217,6 @@ describe('updateOAuth2Provider', () => { const result = await updateOAuth2Provider(makeMockClient(), mockParams); expect(result.success).toBe(false); - expect(result.error).toBe('update failed'); + expect((result as { success: false; error: Error }).error.message).toBe('update failed'); }); }); diff --git a/src/cli/operations/identity/oauth2-credential-provider.ts b/src/cli/operations/identity/oauth2-credential-provider.ts index cd037670b..ef1b6ddea 100644 --- a/src/cli/operations/identity/oauth2-credential-provider.ts +++ b/src/cli/operations/identity/oauth2-credential-provider.ts @@ -5,6 +5,8 @@ * as CDK constructs. These operations run as a pre-deploy step outside the * main CDK synthesis/deploy path. */ +import { type Result, toError } from '@/lib'; +import { err, ok } from '@/lib/result'; import { BedrockAgentCoreControlClient, CreateOauth2CredentialProviderCommand, @@ -14,11 +16,11 @@ import { UpdateOauth2CredentialProviderCommand, } from '@aws-sdk/client-bedrock-agentcore-control'; -export interface OAuth2ProviderResult { +export type OAuth2ProviderResult = Result<{ credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string; -} +}>; export interface OAuth2ProviderParams { name: string; @@ -38,11 +40,11 @@ function extractResult(response: { callbackUrl?: string; }): OAuth2ProviderResult | undefined { if (!response.credentialProviderArn) return undefined; - return { + return ok({ credentialProviderArn: response.credentialProviderArn, clientSecretArn: response.clientSecretArn?.secretArn, callbackUrl: response.callbackUrl, - }; + }); } /** @@ -93,19 +95,19 @@ function buildOAuth2Config(params: OAuth2ProviderParams) { export async function createOAuth2Provider( client: BedrockAgentCoreControlClient, params: OAuth2ProviderParams -): Promise<{ success: boolean; result?: OAuth2ProviderResult; error?: string }> { +): Promise { try { const response = await client.send(new CreateOauth2CredentialProviderCommand(buildOAuth2Config(params))); let result = extractResult(response); if (!result) { // Create response may not include credentialProviderArn — fetch it const getResult = await getOAuth2Provider(client, params.name); - result = getResult.result; + result = getResult; } if (!result) { - return { success: false, error: 'No credential provider ARN in response' }; + return err(new Error('No credential provider ARN in response')); } - return { success: true, result }; + return result; } catch (error) { const errorName = (error as { name?: string }).name; if (errorName === 'ConflictException' || errorName === 'ResourceAlreadyExistsException') { @@ -113,10 +115,7 @@ export async function createOAuth2Provider( // create call. Fall back to update so the user's credentials are always applied. return updateOAuth2Provider(client, params); } - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + return err(toError(error)); } } @@ -126,19 +125,16 @@ export async function createOAuth2Provider( export async function getOAuth2Provider( client: BedrockAgentCoreControlClient, name: string -): Promise<{ success: boolean; result?: OAuth2ProviderResult; error?: string }> { +): Promise { try { const response = await client.send(new GetOauth2CredentialProviderCommand({ name })); const result = extractResult(response); if (!result) { - return { success: false, error: 'No credential provider ARN in response' }; + return err(new Error('No credential provider ARN in response')); } - return { success: true, result }; + return result; } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + return err(toError(error)); } } @@ -148,22 +144,19 @@ export async function getOAuth2Provider( export async function updateOAuth2Provider( client: BedrockAgentCoreControlClient, params: OAuth2ProviderParams -): Promise<{ success: boolean; result?: OAuth2ProviderResult; error?: string }> { +): Promise { try { const response = await client.send(new UpdateOauth2CredentialProviderCommand(buildOAuth2Config(params))); let result = extractResult(response); if (!result) { const getResult = await getOAuth2Provider(client, params.name); - result = getResult.result; + result = getResult; } if (!result) { - return { success: false, error: 'No credential provider ARN in response' }; + return err(new Error('No credential provider ARN in response')); } - return { success: true, result }; + return result; } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + return err(toError(error)); } } From 80b5ef270df4451ad31284f5ff8fe7268a2f1514 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 02:57:28 +0000 Subject: [PATCH 04/16] fix(deploy): surface payment preflight errors --- src/cli/commands/deploy/actions.ts | 4 ++-- .../__tests__/pre-deploy-payments.test.ts | 21 ++++++++++++------ .../operations/deploy/pre-deploy-identity.ts | 22 +++++++++---------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 865cf97bd..6a3860695 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -308,7 +308,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise ({ } }, readEnvFile: mockReadEnvFile, + toError: (e: unknown) => (e instanceof Error ? e : new Error(String(e))), + MissingCredentialsError: class MissingCredentialsError extends Error { + constructor(message: string) { + super(message); + this.name = 'MissingCredentialsError'; + } + }, })); vi.mock('fs', () => ({ @@ -218,9 +225,9 @@ describe('setupPaymentCredentialProviders', () => { expect(result.hasErrors).toBe(true); expect(result.errors).toHaveLength(1); - expect(result.errors[0]).toContain('Missing CDP credentials'); - expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_API_KEY_SECRET'); - expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_WALLET_SECRET'); + expect(result.errors[0]?.message).toContain('Missing CDP credentials'); + expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_API_KEY_SECRET'); + expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_CDP_CRED_WALLET_SECRET'); expect(mockCreatePaymentCredentialProvider).not.toHaveBeenCalled(); }); @@ -240,10 +247,10 @@ describe('setupPaymentCredentialProviders', () => { expect(result.hasErrors).toBe(true); expect(result.errors).toHaveLength(1); - expect(result.errors[0]).toContain('Missing StripePrivy credentials'); - expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_APP_SECRET'); - expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_PRIVATE_KEY'); - expect(result.errors[0]).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_ID'); + expect(result.errors[0]?.message).toContain('Missing StripePrivy credentials'); + expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_APP_SECRET'); + expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_PRIVATE_KEY'); + expect(result.errors[0]?.message).toContain('AGENTCORE_CREDENTIAL_MY_STRIPE_CRED_AUTHORIZATION_ID'); expect(mockCreatePaymentCredentialProvider).not.toHaveBeenCalled(); }); diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index b901a47a6..730657138 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -472,7 +472,7 @@ export interface PaymentCredentialProviderResult { export interface PaymentCredentialProvidersResult { credentialProviders: Record; hasErrors: boolean; - errors: string[]; + errors: Error[]; } export interface SetupPaymentCredentialProvidersOptions { @@ -518,7 +518,9 @@ export async function setupPaymentCredentialProviders( if (!credential) { result.hasErrors = true; result.errors.push( - `Payment manager "${payment.name}" connector "${connector.name}" references credential "${credentialName}" which is not a PaymentCredentialProvider` + new ValidationError( + `Payment manager "${payment.name}" connector "${connector.name}" references credential "${credentialName}" which is not a PaymentCredentialProvider` + ) ); continue; } @@ -534,17 +536,15 @@ export async function setupPaymentCredentialProviders( credentialProviderArn, credentialProviderName: credentialName, }; - } catch (error) { - let errorMessage: string; + } catch (e) { + const error = toError(e); if (isNoCredentialsError(error)) { - errorMessage = `AWS credentials not found. ${await getAwsLoginGuidance()}`; + error.message = `AWS credentials not found. ${await getAwsLoginGuidance()}`; } else if (isQuotaExceededError(error)) { - errorMessage = `Service quota exceeded. Delete unused credential providers, or request a limit increase via the AWS Service Quotas console.`; - } else { - errorMessage = error instanceof Error ? error.message : String(error); + error.message = `Service quota exceeded. Delete unused credential providers, or request a limit increase via the AWS Service Quotas console.`; } result.hasErrors = true; - result.errors.push(`Credential provider for "${connector.name}": ${errorMessage}`); + result.errors.push(error); } } } @@ -608,7 +608,7 @@ async function createOrUpdatePaymentCredentialProvider( !authorizationPrivateKey && envVarNames.authorizationPrivateKey, !authorizationId && envVarNames.authorizationId, ].filter(Boolean); - throw new Error( + throw new MissingCredentialsError( `Missing StripePrivy credentials for connector "${connector.name}" in agentcore/.env.local: ${missing.join(', ')}` ); } @@ -634,7 +634,7 @@ async function createOrUpdatePaymentCredentialProvider( !apiKeySecret && envVarNames.apiKeySecret, !walletSecret && envVarNames.walletSecret, ].filter(Boolean); - throw new Error( + throw new MissingCredentialsError( `Missing CDP credentials for connector "${connector.name}" in agentcore/.env.local: ${missing.join(', ')}` ); } From c9378993e20c215393a8f1699e9363b46616606c Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 12:43:33 +0000 Subject: [PATCH 05/16] fix(telemetry): add missing errors to telemetry error mapping --- src/cli/telemetry/error.ts | 8 ++++++++ src/cli/telemetry/schemas/common-shapes.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/src/cli/telemetry/error.ts b/src/cli/telemetry/error.ts index 91bc83029..fcf612387 100644 --- a/src/cli/telemetry/error.ts +++ b/src/cli/telemetry/error.ts @@ -19,6 +19,14 @@ const SDK_ERROR_MAP: Record Date: Thu, 11 Jun 2026 13:07:20 +0000 Subject: [PATCH 06/16] refactor(telemetry): rename some errors to make their purpose clearer --- src/cli/commands/dev/command.tsx | 12 +++---- .../dev/__tests__/invoke-a2a.test.ts | 8 ++--- .../dev/__tests__/invoke-mcp.test.ts | 6 ++-- src/cli/operations/dev/invoke-a2a.ts | 19 ++++++----- src/cli/operations/dev/invoke-agui.ts | 15 +++++---- src/cli/operations/dev/invoke-mcp.ts | 19 ++++++----- src/cli/operations/dev/invoke.ts | 32 +++++++++++-------- src/cli/telemetry/error.ts | 13 ++++---- src/cli/telemetry/schemas/common-shapes.ts | 5 +-- src/cli/tui/hooks/useDevServer.ts | 6 ++-- src/lib/errors/__tests__/config.test.ts | 10 +++--- src/lib/errors/types.ts | 17 +++++++--- 12 files changed, 93 insertions(+), 69 deletions(-) diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index 7f01a8dfc..b76d9be39 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -1,5 +1,5 @@ import { - ConnectionError, + DevServerConnectionError, NoProjectError, ResourceNotFoundError, ValidationError, @@ -65,7 +65,7 @@ async function invokeDevServer( } } catch (err) { throw isConnectionRefused(err) - ? new ConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, { + ? new DevServerConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, { cause: err, }) : err; @@ -80,7 +80,7 @@ async function invokeA2ADevServer(port: number, prompt: string, headers?: Record process.stdout.write('\n'); } catch (err) { throw isConnectionRefused(err) - ? new ConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, { + ? new DevServerConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, { cause: err, }) : err; @@ -89,8 +89,8 @@ async function invokeA2ADevServer(port: number, prompt: string, headers?: Record function isConnectionRefused(err: unknown): boolean { if (!(err instanceof Error)) return false; - // ConnectionError from invoke.ts wraps fetch failures after retries - if (err.name === 'ConnectionError') return true; + // DevServerConnectionError from invoke.ts wraps fetch failures after retries + if (err.name === 'DevServerConnectionError') return true; const msg = err.message + (err.cause instanceof Error ? err.cause.message : ''); return msg.includes('ECONNREFUSED') || msg.includes('fetch failed'); } @@ -138,7 +138,7 @@ async function handleMcpInvoke( } } catch (err) { throw isConnectionRefused(err) - ? new ConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, { + ? new DevServerConnectionError(`Dev server not running on port ${port}. Start it with: agentcore dev --logs`, { cause: err, }) : err; diff --git a/src/cli/operations/dev/__tests__/invoke-a2a.test.ts b/src/cli/operations/dev/__tests__/invoke-a2a.test.ts index 743c34fc1..e390af6c3 100644 --- a/src/cli/operations/dev/__tests__/invoke-a2a.test.ts +++ b/src/cli/operations/dev/__tests__/invoke-a2a.test.ts @@ -1,4 +1,4 @@ -import { ServerError } from '../../../../lib/errors/types'; +import { DevServerError } from '../../../../lib/errors/types'; import { invokeA2AStreaming } from '../invoke-a2a'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -76,7 +76,7 @@ describe('invokeA2AStreaming', () => { expect(mockFetch).toHaveBeenCalledTimes(2); }); - it('throws ServerError on HTTP error without retrying', async () => { + it('throws DevServerError on HTTP error without retrying', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, @@ -84,7 +84,7 @@ describe('invokeA2AStreaming', () => { }); const gen = invokeA2AStreaming({ port: 8080, message: 'test' }); - await expect(gen.next()).rejects.toThrow(ServerError); + await expect(gen.next()).rejects.toThrow(DevServerError); }); it('handles JSON-RPC error in response', async () => { @@ -101,7 +101,7 @@ describe('invokeA2AStreaming', () => { }); const gen = invokeA2AStreaming({ port: 8080, message: 'test' }); - await expect(gen.next()).rejects.toThrow(ServerError); + await expect(gen.next()).rejects.toThrow(DevServerError); }); it('streams text from status-update events and skips duplicate artifact-update', async () => { diff --git a/src/cli/operations/dev/__tests__/invoke-mcp.test.ts b/src/cli/operations/dev/__tests__/invoke-mcp.test.ts index ccba4559c..021cb1d2d 100644 --- a/src/cli/operations/dev/__tests__/invoke-mcp.test.ts +++ b/src/cli/operations/dev/__tests__/invoke-mcp.test.ts @@ -1,4 +1,4 @@ -import { ServerError } from '../../../../lib/errors/types'; +import { DevServerError } from '../../../../lib/errors/types'; import { callMcpTool, listMcpTools } from '../invoke-mcp'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -79,14 +79,14 @@ describe('listMcpTools', () => { expect(mockFetch).toHaveBeenCalledTimes(4); }); - it('throws ServerError on HTTP error', async () => { + it('throws DevServerError on HTTP error', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, text: () => 'Internal Server Error', }); - await expect(listMcpTools(8080)).rejects.toThrow(ServerError); + await expect(listMcpTools(8080)).rejects.toThrow(DevServerError); }); }); diff --git a/src/cli/operations/dev/invoke-a2a.ts b/src/cli/operations/dev/invoke-a2a.ts index 2b3d28f1d..2491297c1 100644 --- a/src/cli/operations/dev/invoke-a2a.ts +++ b/src/cli/operations/dev/invoke-a2a.ts @@ -1,4 +1,4 @@ -import { ConnectionError, ServerError } from '../../../lib/errors/types'; +import { DevServerConnectionError, DevServerError } from '../../../lib/errors/types'; import { type InvokeStreamingOptions, type SSELogger } from './invoke-types'; import { isConnectionError, sleep } from './utils'; import { randomUUID } from 'crypto'; @@ -91,7 +91,7 @@ export async function* invokeA2AStreaming(options: InvokeStreamingOptions): Asyn if (!res.ok) { const responseBody = await res.text(); - throw new ServerError(res.status, responseBody); + throw new DevServerError(res.status, responseBody); } const contentType = res.headers.get('content-type') ?? ''; @@ -111,7 +111,7 @@ export async function* invokeA2AStreaming(options: InvokeStreamingOptions): Asyn if (json.error) { const rpcError = json.error as { message?: string; code?: number }; - throw new ServerError(rpcError.code ?? 500, rpcError.message ?? 'A2A RPC error'); + throw new DevServerError(rpcError.code ?? 500, rpcError.message ?? 'A2A RPC error'); } const result = json.result as Record | undefined; @@ -126,13 +126,13 @@ export async function* invokeA2AStreaming(options: InvokeStreamingOptions): Asyn yield responseText; } } catch (e) { - if (e instanceof ServerError) throw e; + if (e instanceof DevServerError) throw e; yield responseText; } return; } catch (err) { - if (err instanceof ServerError) { + if (err instanceof DevServerError) { logger?.log?.('error', `Server error (${err.statusCode}): ${err.message}`); throw err; } @@ -154,9 +154,12 @@ export async function* invokeA2AStreaming(options: InvokeStreamingOptions): Asyn } } - const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to A2A server after retries', { - cause: lastError, - }); + const finalError = new DevServerConnectionError( + lastError?.message ?? 'Failed to connect to A2A server after retries', + { + cause: lastError, + } + ); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } diff --git a/src/cli/operations/dev/invoke-agui.ts b/src/cli/operations/dev/invoke-agui.ts index eb11a5129..e7714252f 100644 --- a/src/cli/operations/dev/invoke-agui.ts +++ b/src/cli/operations/dev/invoke-agui.ts @@ -1,4 +1,4 @@ -import { ConnectionError, ServerError } from '../../../lib/errors/types'; +import { DevServerConnectionError, DevServerError } from '../../../lib/errors/types'; import { parseAguiSSEStream } from '../../aws/agui-parser'; import { AguiEventType } from '../../aws/agui-types'; import { type InvokeStreamingOptions } from './invoke-types'; @@ -38,7 +38,7 @@ export async function* invokeAguiStreaming(options: InvokeStreamingOptions): Asy if (!res.ok) { const responseBody = await res.text(); - throw new ServerError(res.status, responseBody); + throw new DevServerError(res.status, responseBody); } if (!res.body) { @@ -122,7 +122,7 @@ export async function* invokeAguiStreaming(options: InvokeStreamingOptions): Asy return; } catch (err) { - if (err instanceof ServerError) { + if (err instanceof DevServerError) { logger?.log?.('error', `Server error (${err.statusCode}): ${err.message}`); throw err; } @@ -148,9 +148,12 @@ export async function* invokeAguiStreaming(options: InvokeStreamingOptions): Asy } } - const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to AGUI server after retries', { - cause: lastError, - }); + const finalError = new DevServerConnectionError( + lastError?.message ?? 'Failed to connect to AGUI server after retries', + { + cause: lastError, + } + ); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } diff --git a/src/cli/operations/dev/invoke-mcp.ts b/src/cli/operations/dev/invoke-mcp.ts index 6e861ef43..2d322f857 100644 --- a/src/cli/operations/dev/invoke-mcp.ts +++ b/src/cli/operations/dev/invoke-mcp.ts @@ -1,4 +1,4 @@ -import { ConnectionError, ServerError } from '../../../lib/errors/types'; +import { DevServerConnectionError, DevServerError } from '../../../lib/errors/types'; import { parseJsonRpcResponse } from '../../../lib/utils/json-rpc'; import { type SSELogger } from './invoke-types'; import { isConnectionError, sleep } from './utils'; @@ -58,7 +58,7 @@ export async function listMcpTools( if (!initRes.ok) { const body = await initRes.text(); - throw new ServerError(initRes.status, body); + throw new DevServerError(initRes.status, body); } // Extract session ID from response header @@ -103,7 +103,7 @@ export async function listMcpTools( if (!listRes.ok) { const body = await listRes.text(); - throw new ServerError(listRes.status, body); + throw new DevServerError(listRes.status, body); } const listResponseText = await listRes.text(); @@ -122,7 +122,7 @@ export async function listMcpTools( sessionId: sessionId ?? undefined, }; } catch (err) { - if (err instanceof ServerError) { + if (err instanceof DevServerError) { logger?.log?.('error', `Server error (${err.statusCode}): ${err.message}`); throw err; } @@ -144,9 +144,12 @@ export async function listMcpTools( } } - const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to MCP server after retries', { - cause: lastError, - }); + const finalError = new DevServerConnectionError( + lastError?.message ?? 'Failed to connect to MCP server after retries', + { + cause: lastError, + } + ); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } @@ -187,7 +190,7 @@ export async function callMcpTool( if (!res.ok) { const responseBody = await res.text(); - throw new ServerError(res.status, responseBody); + throw new DevServerError(res.status, responseBody); } const responseText = await res.text(); diff --git a/src/cli/operations/dev/invoke.ts b/src/cli/operations/dev/invoke.ts index 9a385e574..ad546d366 100644 --- a/src/cli/operations/dev/invoke.ts +++ b/src/cli/operations/dev/invoke.ts @@ -1,4 +1,4 @@ -import { ConnectionError, ServerError } from '../../../lib/errors/types'; +import { DevServerConnectionError, DevServerError } from '../../../lib/errors/types'; import { invokeA2AStreaming } from './invoke-a2a'; import { invokeAguiStreaming } from './invoke-agui'; import { type InvokeStreamingOptions, type SSELogger } from './invoke-types'; @@ -100,7 +100,7 @@ export async function* invokeAgentStreaming( if (!res.ok) { const body = await res.text(); - throw new ServerError(res.status, body); + throw new DevServerError(res.status, body); } if (!res.body) { @@ -171,8 +171,8 @@ export async function* invokeAgentStreaming( return; } catch (err) { - // Re-throw ServerError directly — no retries for HTTP errors - if (err instanceof ServerError) { + // Re-throw DevServerError directly — no retries for HTTP errors + if (err instanceof DevServerError) { logger?.log?.('error', `Server error (${err.statusCode}): ${err.message}`); throw err; } @@ -196,9 +196,12 @@ export async function* invokeAgentStreaming( } // Log final failure after all retries exhausted with full details - const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to dev server after retries', { - cause: lastError, - }); + const finalError = new DevServerConnectionError( + lastError?.message ?? 'Failed to connect to dev server after retries', + { + cause: lastError, + } + ); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } @@ -265,7 +268,7 @@ export async function invokeAgent(portOrOptions: number | InvokeOptions, message if (!res.ok) { const body = await res.text(); - throw new ServerError(res.status, body); + throw new DevServerError(res.status, body); } const text = await res.text(); @@ -281,8 +284,8 @@ export async function invokeAgent(portOrOptions: number | InvokeOptions, message // Handle plain JSON response (non-streaming frameworks) return extractResult(text); } catch (err) { - // Re-throw ServerError directly — no retries for HTTP errors - if (err instanceof ServerError) { + // Re-throw DevServerError directly — no retries for HTTP errors + if (err instanceof DevServerError) { logger?.log?.('error', `Server error (${err.statusCode}): ${err.message}`); throw err; } @@ -306,9 +309,12 @@ export async function invokeAgent(portOrOptions: number | InvokeOptions, message } // Log final failure after all retries exhausted with full details - const finalError = new ConnectionError(lastError?.message ?? 'Failed to connect to dev server after retries', { - cause: lastError, - }); + const finalError = new DevServerConnectionError( + lastError?.message ?? 'Failed to connect to dev server after retries', + { + cause: lastError, + } + ); logger?.log?.('error', `Failed to connect after ${maxRetries} attempts: ${finalError.message}`); throw finalError; } diff --git a/src/cli/telemetry/error.ts b/src/cli/telemetry/error.ts index fcf612387..cffcf1402 100644 --- a/src/cli/telemetry/error.ts +++ b/src/cli/telemetry/error.ts @@ -4,10 +4,11 @@ import type { z } from 'zod'; type ErrorNameValue = z.infer; -// Maps common AWS SDK error names to a category and source. -const SDK_ERROR_MAP: Record = { +// Maps common error names to a category and source. +const COMMON_ERROR_MAP: Record = { AccessDeniedException: { category: 'AccessDeniedError', source: 'user' }, AccessDenied: { category: 'AccessDeniedError', source: 'user' }, + ForbiddenException: { category: 'AccessDeniedError', source: 'user' }, ExpiredToken: { category: 'AwsCredentialsError', source: 'user' }, ExpiredTokenException: { category: 'AwsCredentialsError', source: 'user' }, InvalidClientTokenId: { category: 'AwsCredentialsError', source: 'user' }, @@ -25,8 +26,8 @@ const SDK_ERROR_MAP: Record l.level === 'error') diff --git a/src/lib/errors/__tests__/config.test.ts b/src/lib/errors/__tests__/config.test.ts index 4246d2459..b9b9c280f 100644 --- a/src/lib/errors/__tests__/config.test.ts +++ b/src/lib/errors/__tests__/config.test.ts @@ -1,5 +1,5 @@ import { - ConfigError, + BaseError, ConfigNotFoundError, ConfigParseError, ConfigReadError, @@ -21,9 +21,9 @@ describe('ConfigNotFoundError', () => { expect(err.fileType).toBe('targets'); }); - it('is instance of ConfigError and Error', () => { + it('is instance of BaseError and Error', () => { const err = new ConfigNotFoundError('/path', 'project'); - expect(err).toBeInstanceOf(ConfigError); + expect(err).toBeInstanceOf(BaseError); expect(err).toBeInstanceOf(Error); }); @@ -83,14 +83,14 @@ describe('ConfigParseError', () => { }); describe('ConfigValidationError', () => { - it('stores zodError and is instance of ConfigError', () => { + it('stores zodError and is instance of BaseError', () => { const schema = z.object({ name: z.string() }); const result = schema.safeParse({ name: 123 }); expect(result.success).toBe(false); if (!result.success) { const err = new ConfigValidationError('/path', 'project', result.error); expect(err.zodError).toBe(result.error); - expect(err).toBeInstanceOf(ConfigError); + expect(err).toBeInstanceOf(BaseError); expect(err).toBeInstanceOf(Error); } }); diff --git a/src/lib/errors/types.ts b/src/lib/errors/types.ts index 0aa5b0ef6..67af9c2d8 100644 --- a/src/lib/errors/types.ts +++ b/src/lib/errors/types.ts @@ -159,7 +159,7 @@ export class UnsupportedLanguageError extends PackagingError { /** * Base class for all config-related errors. */ -export abstract class ConfigError extends BaseError { +abstract class ConfigError extends BaseError { protected constructor(message: string, options?: BaseErrorOptions) { super(message, { defaultSource: 'user', ...options }); } @@ -229,8 +229,6 @@ export class ConfigParseError extends ConfigError { } } -// --- Client errors --- - /** * Error indicating git repository initialization failed. */ @@ -263,7 +261,7 @@ export class TimeoutError extends BaseError { /** * Error thrown when the dev server returns a non-OK HTTP response. */ -export class ServerError extends BaseError { +export class DevServerError extends BaseError { constructor( public readonly statusCode: number, body: string, @@ -276,12 +274,21 @@ export class ServerError extends BaseError { /** * Error thrown when the connection to the dev server fails. */ -export class ConnectionError extends BaseError { +export class DevServerConnectionError extends BaseError { constructor(message: string, options?: BaseErrorOptions) { super(message, { defaultSource: 'client', ...options }); } } +/** + * Error indicating an AWS service returned an internal failure. + */ +export class ServiceError extends BaseError { + constructor(message: string, options?: BaseErrorOptions) { + super(message, { defaultSource: 'service', ...options }); + } +} + /** * Error indicating polling timed out. */ From 520a25feebfb125f6d012f83814d90c99e83604c Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 13:22:18 +0000 Subject: [PATCH 07/16] fix(error): narrow down errors further in deploy preflight --- src/cli/commands/deploy/actions.ts | 6 +++--- src/cli/operations/deploy/pre-deploy-identity.ts | 4 ++-- src/cli/operations/identity/oauth2-credential-provider.ts | 8 ++++---- src/lib/errors/types.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 6a3860695..bb727edc6 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -235,7 +235,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise Date: Thu, 11 Jun 2026 13:27:28 +0000 Subject: [PATCH 08/16] fix(deploy): ensure logs include error message instead of object --- src/cli/commands/deploy/actions.ts | 7 +++---- src/cli/tui/hooks/useCdkPreflight.ts | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index bb727edc6..8384995c1 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -228,14 +228,13 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise r.status === 'error' && r.error); - const errorMsg = - errorResult?.error && typeof errorResult.error === 'string' ? errorResult.error : 'Identity setup failed'; + const errorMsg = errorResult?.error?.message ?? 'Identity setup failed'; endStep('error', errorMsg); logger.finalize(false); // return { success: false, - error: errorResult?.error ?? new Error('unknown error ocurred when setting up api key providers'), + error: errorResult?.error ?? new Error('unknown error occurred when setting up api key providers'), logPath: logger.getRelativeLogPath(), }; } @@ -302,7 +301,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise e.message).join('; '); endStep('error', errorMsgs); logger.log(`Payment credential setup errors: ${errorMsgs}`, 'error'); logger.finalize(false); diff --git a/src/cli/tui/hooks/useCdkPreflight.ts b/src/cli/tui/hooks/useCdkPreflight.ts index c6de0ef75..b2462130e 100644 --- a/src/cli/tui/hooks/useCdkPreflight.ts +++ b/src/cli/tui/hooks/useCdkPreflight.ts @@ -81,7 +81,7 @@ async function runPaymentPreDeploy(opts: RunPaymentSetupOptions): Promise e.message).join('; '); logger.endStep('error', errorMsg); updateStepByLabel(LABEL_PAYMENTS, { status: 'error', error: `Payment setup failed: ${errorMsg}` }); setPhase('error'); @@ -800,9 +800,9 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult { } else if (result.status === 'exists') { logger.log(`API key provider exists: ${result.providerName}`); } else if (result.status === 'skipped') { - logger.log(`Skipped ${result.providerName}: ${result.error}`); + logger.log(`Skipped ${result.providerName}: ${result.error?.message}`); } else if (result.status === 'error') { - logger.log(`Error for ${result.providerName}: ${result.error}`); + logger.log(`Error for ${result.providerName}: ${result.error?.message}`); } } @@ -843,9 +843,9 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult { } else if (result.status === 'updated') { logger.log(`Updated OAuth provider: ${result.providerName}`); } else if (result.status === 'skipped') { - logger.log(`Skipped ${result.providerName}: ${result.error}`); + logger.log(`Skipped ${result.providerName}: ${result.error?.message}`); } else if (result.status === 'error') { - logger.log(`Error for ${result.providerName}: ${result.error}`); + logger.log(`Error for ${result.providerName}: ${result.error?.message}`); } } From 70f13155551c098c65700aefef425480f3eabb4c Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 13:31:12 +0000 Subject: [PATCH 09/16] fix(deploy): remove dead comment --- src/cli/commands/deploy/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 8384995c1..1d492bb29 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -231,7 +231,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise Date: Thu, 11 Jun 2026 13:38:20 +0000 Subject: [PATCH 10/16] refactor(telemetry): add first-class service-quota and throttling errors to telemetry classification --- src/cli/telemetry/error.ts | 8 ++++---- src/cli/telemetry/schemas/common-shapes.ts | 2 ++ src/lib/errors/types.ts | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/cli/telemetry/error.ts b/src/cli/telemetry/error.ts index cffcf1402..3fa31385c 100644 --- a/src/cli/telemetry/error.ts +++ b/src/cli/telemetry/error.ts @@ -21,10 +21,10 @@ const COMMON_ERROR_MAP: Record Date: Thu, 11 Jun 2026 14:01:41 +0000 Subject: [PATCH 11/16] fix(deploy): avoid mutating error messages --- .../operations/deploy/pre-deploy-identity.ts | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index da23a769e..f273a3ac0 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -2,6 +2,7 @@ import { AwsCredentialsError, MissingCredentialsError, SecureCredentials, + ServiceQuotaError, ValidationError, readEnvFile, toError, @@ -205,7 +206,9 @@ async function setupApiKeyCredentialProvider( return { providerName: credential.name, status: 'error', - error: new AwsCredentialsError(`AWS credentials not found. ${await getAwsLoginGuidance()}`), + error: new AwsCredentialsError(`AWS credentials not found. ${await getAwsLoginGuidance()}`, undefined, { + cause: error, + }), }; } @@ -452,11 +455,19 @@ async function setupSingleOAuth2Provider( callbackUrl: createResult.success ? createResult.callbackUrl : undefined, }; } catch (e) { - const err = toError(e); - if (isNoCredentialsError(e)) { - err.message = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.'; + const error = toError(e); + if (isNoCredentialsError(error)) { + return { + providerName: credential.name, + status: 'error', + error: new AwsCredentialsError( + 'AWS credentials not found.Run`aws sso login` or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY.', + undefined, + { cause: error } + ), + }; } - return { providerName: credential.name, status: 'error', error: err }; + return { providerName: credential.name, status: 'error', error: error }; } } @@ -537,14 +548,23 @@ export async function setupPaymentCredentialProviders( credentialProviderName: credentialName, }; } catch (e) { + result.hasErrors = true; const error = toError(e); if (isNoCredentialsError(error)) { - error.message = `AWS credentials not found. ${await getAwsLoginGuidance()}`; + result.errors.push( + new AwsCredentialsError(`AWS credentials not found. ${await getAwsLoginGuidance()}`, undefined, { + cause: error, + }) + ); } else if (isQuotaExceededError(error)) { - error.message = `Service quota exceeded. Delete unused credential providers, or request a limit increase via the AWS Service Quotas console.`; + result.errors.push( + new ServiceQuotaError( + `Service quota exceeded. Delete unused credential providers, or request a limit increase via the AWS Service Quotas console.` + ) + ); + } else { + result.errors.push(error); } - result.hasErrors = true; - result.errors.push(error); } } } From e9b35463f29014b64b859bd65772a599c7e27ad7 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 14:12:06 +0000 Subject: [PATCH 12/16] fix(deploy): use result type isntead of result | undefined --- src/cli/commands/deploy/actions.ts | 5 ++++- .../identity/oauth2-credential-provider.ts | 20 +++++-------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 1d492bb29..69a2c3fd9 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -268,9 +268,12 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise { try { const response = await client.send(new GetOauth2CredentialProviderCommand({ name })); - const result = extractResult(response); - if (!result) { - return err(new ServiceError('No credential provider ARN in response')); - } - return result; + return extractResult(response); } catch (error) { return err(toError(error)); } @@ -148,13 +141,10 @@ export async function updateOAuth2Provider( try { const response = await client.send(new UpdateOauth2CredentialProviderCommand(buildOAuth2Config(params))); let result = extractResult(response); - if (!result) { + if (!result.success) { const getResult = await getOAuth2Provider(client, params.name); result = getResult; } - if (!result) { - return err(new ServiceError('No credential provider ARN in response')); - } return result; } catch (error) { return err(toError(error)); From 2820e48de966a83c9fe381667fe827b7e54e650e Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 11 Jun 2026 15:00:58 +0000 Subject: [PATCH 13/16] fix(test): update unit tests with new error message --- .../identity/__tests__/oauth2-credential-provider.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts b/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts index 6b3aa2afe..2de9f32a7 100644 --- a/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts +++ b/src/cli/operations/identity/__tests__/oauth2-credential-provider.test.ts @@ -140,7 +140,9 @@ describe('createOAuth2Provider', () => { const result = await createOAuth2Provider(makeMockClient(), mockParams); expect(result.success).toBe(false); - expect((result as { success: false; error: Error }).error.message).toBe('No credential provider ARN in response'); + expect((result as { success: false; error: Error }).error.message).toBe( + 'missing credentialProviderArn in response' + ); }); }); @@ -180,7 +182,9 @@ describe('getOAuth2Provider', () => { const result = await getOAuth2Provider(makeMockClient(), 'test-provider'); expect(result.success).toBe(false); - expect((result as { success: false; error: Error }).error.message).toBe('No credential provider ARN in response'); + expect((result as { success: false; error: Error }).error.message).toBe( + 'missing credentialProviderArn in response' + ); }); }); From ba30e102ce8238b4061c602cca0ef2ab12ec62da Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 18 Jun 2026 15:29:46 +0000 Subject: [PATCH 14/16] fix: add space back to error message for credentials missing --- src/cli/operations/deploy/pre-deploy-identity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index f273a3ac0..0f44a7e25 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -461,7 +461,7 @@ async function setupSingleOAuth2Provider( providerName: credential.name, status: 'error', error: new AwsCredentialsError( - 'AWS credentials not found.Run`aws sso login` or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY.', + 'AWS credentials not found. Run`aws sso login` or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY.', undefined, { cause: error } ), From 1e6fdda07ce9bd57888080900951f7724400dda4 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 18 Jun 2026 15:47:15 +0000 Subject: [PATCH 15/16] fix: throw original error when oauth setup fails --- src/cli/commands/deploy/actions.ts | 4 +--- src/cli/operations/deploy/pre-deploy-identity.ts | 8 +++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 39d370a2d..5f4f96e93 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -312,11 +312,9 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise Date: Thu, 18 Jun 2026 15:57:49 +0000 Subject: [PATCH 16/16] chore: remove extra space in file --- src/cli/telemetry/schemas/common-shapes.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index fe44d77af..3a11954e4 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -118,7 +118,6 @@ export const ErrorName = z.enum([ 'MissingCredentialsError', 'IngestionError', 'JobNotFoundError', - 'MissingDependencyError', 'MissingProjectFileError', 'NoProjectError',