Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/commands/apikeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command } from 'commander'
import chalk from 'chalk'
import { listAccessKeys, getDefaultAccessKey } from '../lib/api.js'
import { isLoggedIn, EXIT_CODES } from '../lib/config.js'
import { extractErrorMessage } from '../lib/errors.js'

export const apikeysCommand = new Command('apikeys')
.description('Manage API keys for a project')
Expand Down Expand Up @@ -103,7 +104,7 @@ async function listApiKeysAction(

console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
} else {
Expand Down Expand Up @@ -172,7 +173,7 @@ async function getDefaultKeyAction(
)
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
} else {
Expand Down
9 changes: 5 additions & 4 deletions src/commands/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SequenceIndexer } from '@0xsequence/indexer'
import { networks, ChainId } from '@0xsequence/network'
import { EXIT_CODES } from '../lib/config.js'
import { ethers } from 'ethers'
import { extractErrorMessage } from '../lib/errors.js'

// Get indexer URL for a chain
function getIndexerUrl(chainId: number): string {
Expand Down Expand Up @@ -77,7 +78,7 @@ indexerCommand
}
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
} else {
Expand Down Expand Up @@ -147,7 +148,7 @@ indexerCommand
console.log(chalk.cyan(` ${symbol}:`), chalk.white(balance))
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
} else {
Expand Down Expand Up @@ -237,7 +238,7 @@ indexerCommand
console.log('')
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
} else {
Expand Down Expand Up @@ -327,7 +328,7 @@ indexerCommand
}
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
} else {
Expand Down
69 changes: 67 additions & 2 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Command } from 'commander'
import chalk from 'chalk'
import { generateEthAuthProof } from '../lib/ethauth.js'
import { getAuthToken } from '../lib/api.js'
import { getAuthToken, isApiError } from '../lib/api.js'
import { extractErrorMessage } from '../lib/errors.js'
import {
updateConfig,
EXIT_CODES,
Expand Down Expand Up @@ -116,7 +117,71 @@ export const loginCommand = new Command('login')
console.log(chalk.gray(' sequence-builder projects create "My Project"'))
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = extractErrorMessage(error)

// Structured API error with rate-limit / permission info
if (isApiError(error) && (error.isRateLimited || error.isPermissionDenied)) {
if (options.json) {
console.log(
JSON.stringify({
error: error.isRateLimited ? 'Rate limited' : 'Permission denied',
statusCode: error.statusCode,
retryAfterSeconds: error.retryAfterSeconds,
detail: error.errorBody,
code: EXIT_CODES.API_ERROR,
})
)
} else {
if (error.isRateLimited) {
console.error(chalk.red('✖ Rate limited by the API'))
if (error.retryAfterSeconds !== null) {
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
}
console.error(
chalk.gray(
' You have made too many login attempts. Please wait before trying again.'
)
)
} else {
console.error(chalk.red('✖ Permission denied (403)'))
console.error(chalk.gray(' This can happen when:'))
console.error(chalk.gray(' - Too many signing/login attempts in a short period'))
console.error(chalk.gray(' - The ETHAuth proof is malformed or expired'))
console.error(chalk.gray(' - Your wallet address is not authorized'))
}
if (error.retryAfterSeconds !== null && !error.isRateLimited) {
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
}
if (error.errorBody) {
console.error(chalk.gray(` Server response: ${error.errorBody}`))
}
}
process.exit(EXIT_CODES.API_ERROR)
}

// Catch 403/rate-limit in generic error strings (e.g. from SDK internals)
if (
errorMessage.includes('403') ||
errorMessage.toLowerCase().includes('permissiondenied') ||
errorMessage.toLowerCase().includes('rate limit')
) {
if (options.json) {
console.log(
JSON.stringify({
error: 'Permission denied or rate limited',
detail: errorMessage,
code: EXIT_CODES.API_ERROR,
})
)
} else {
console.error(chalk.red('✖ Permission denied or rate limited'))
console.error(chalk.gray(' This can happen when:'))
console.error(chalk.gray(' - Too many signing/login attempts in a short period'))
console.error(chalk.gray(' - The ETHAuth proof is malformed or expired'))
console.error(chalk.gray(` Detail: ${errorMessage}`))
}
process.exit(EXIT_CODES.API_ERROR)
}

if (options.json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
Expand Down
80 changes: 76 additions & 4 deletions src/commands/projects.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,73 @@
import { Command } from 'commander'
import chalk from 'chalk'
import { Session } from '@0xsequence/auth'
import { listProjects, createProject, getProject, getDefaultAccessKey } from '../lib/api.js'
import {
listProjects,
createProject,
getProject,
getDefaultAccessKey,
isApiError,
} from '../lib/api.js'
import { extractErrorMessage } from '../lib/errors.js'
import { isLoggedIn, EXIT_CODES, loadConfig } from '../lib/config.js'
import { isValidPrivateKey } from '../lib/wallet.js'

/**
* Handle API errors with rate-limit / permission awareness.
* Returns true if the error was handled; false otherwise.
*/
function handleApiErrorOutput(error: unknown, json: boolean): boolean {
if (
isApiError(error) &&
(error.isRateLimited || error.isPermissionDenied || error.isUnauthorized)
) {
if (json) {
console.log(
JSON.stringify({
error: error.isRateLimited
? 'Rate limited'
: error.isPermissionDenied
? 'Permission denied'
: 'Unauthorized',
statusCode: error.statusCode,
retryAfterSeconds: error.retryAfterSeconds,
detail: error.errorBody,
code: EXIT_CODES.API_ERROR,
})
)
} else {
if (error.isRateLimited) {
console.error(chalk.red('✖ Rate limited by the API'))
if (error.retryAfterSeconds !== null) {
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
}
console.error(
chalk.gray(' You have made too many requests. Please wait before trying again.')
)
} else if (error.isPermissionDenied) {
console.error(chalk.red('✖ Permission denied (403)'))
console.error(chalk.gray(' This can happen when:'))
console.error(chalk.gray(' - Your session token has expired (re-run login)'))
console.error(chalk.gray(' - Too many requests in a short period'))
console.error(chalk.gray(' - The access key is invalid or revoked'))
if (error.retryAfterSeconds !== null) {
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
}
} else {
console.error(chalk.red('✖ Unauthorized (401)'))
console.error(
chalk.gray(' Your JWT token may have expired. Re-run: sequence-builder login')
)
}
if (error.errorBody) {
console.error(chalk.gray(` Server response: ${error.errorBody}`))
}
}
return true
}
return false
}

export const projectsCommand = new Command('projects')
.description('Manage Sequence Builder projects')
.option('--json', 'Output in JSON format')
Expand Down Expand Up @@ -103,7 +166,10 @@ async function listProjectsAction(options: { json?: boolean; env?: string; apiUr
console.log(chalk.gray('Run `sequence-builder apikeys <project-id>` to view API keys'))
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (handleApiErrorOutput(error, !!json)) {
process.exit(EXIT_CODES.API_ERROR)
}
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
} else {
Expand Down Expand Up @@ -233,7 +299,10 @@ async function createProjectAction(
}
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (handleApiErrorOutput(error, !!json)) {
process.exit(EXIT_CODES.API_ERROR)
}
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
} else {
Expand Down Expand Up @@ -296,7 +365,10 @@ async function getProjectAction(
}
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (handleApiErrorOutput(error, !!json)) {
process.exit(EXIT_CODES.API_ERROR)
}
const errorMessage = extractErrorMessage(error)
if (json) {
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
} else {
Expand Down
Loading