Skip to content
Open
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
100 changes: 99 additions & 1 deletion docs/error-code-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,104 @@ This document describes the canonical error code catalog system used in the Call

The error code catalog provides a single source of truth for all machine-readable error codes emitted by the backend. The system uses a YAML catalog as the authoritative source, with automatic code generation for TypeScript enums, documentation, and OpenAPI schemas.

<!-- BEGIN GENERATED ERROR CODES -->
## Canonical error code catalog

This section is generated from `docs/error-codes.yaml`. Run `npm run error-codes:generate` after changing the catalog.

| Code | Catalog section |
|---|---|
| `BAD_REQUEST` | HTTP status derived / base app codes |
| `UNAUTHORIZED` | HTTP status derived / base app codes |
| `FORBIDDEN` | HTTP status derived / base app codes |
| `NOT_FOUND` | HTTP status derived / base app codes |
| `PAYMENT_REQUIRED` | HTTP status derived / base app codes |
| `TOO_MANY_REQUESTS` | HTTP status derived / base app codes |
| `CONFLICT` | HTTP status derived / base app codes |
| `INTERNAL_SERVER_ERROR` | HTTP status derived / base app codes |
| `BAD_GATEWAY` | HTTP status derived / base app codes |
| `SERVICE_UNAVAILABLE` | HTTP status derived / base app codes |
| `GATEWAY_TIMEOUT` | HTTP status derived / base app codes |
| `VALIDATION_ERROR` | Validation |
| `INVALID_BODY` | Validation |
| `INVALID_QUERY` | Validation |
| `INVALID_PARAMS` | Validation |
| `INVALID_VALUE` | Validation |
| `GATEWAY_AUTH_CONTEXT_MISSING` | Gateway / proxy |
| `UPSTREAM_TARGET_BLOCKED` | Gateway / proxy |
| `INSUFFICIENT_BALANCE` | Billing / Soroban |
| `SOROBAN_RPC_TIMEOUT` | Billing / Soroban |
| `SOROBAN_RPC_ERROR` | Billing / Soroban |
| `BILLING_DEDUCTION_FAILED` | Billing / Soroban |
| `BILLING_REQUEST_NOT_FOUND` | Billing request |
| `DEVELOPER_NOT_FOUND` | Developer / API keys |
| `API_ACCESS_FORBIDDEN` | Developer / API keys |
| `API_KEY_NOT_FOUND` | Developer / API keys |
| `API_KEY_FORBIDDEN` | Developer / API keys |
| `MISSING_REFRESH_TOKEN` | Refresh-token auth |
| `INVALID_REFRESH_TOKEN` | Refresh-token auth |
| `REVOKED_TOKEN` | Refresh-token auth |
| `EXPIRED_TOKEN` | Refresh-token auth |
| `REFRESH_FAILED` | Refresh-token auth |
| `REVOKE_FAILED` | Refresh-token auth |
| `NOT_AUTHENTICATED` | Refresh-token auth |
| `TOKEN_INFO_FAILED` | Refresh-token auth |
| `VAULT_NOT_FOUND` | Vault / deposit |
| `VAULT_BALANCE_RETRIEVAL_FAILED` | Vault / deposit |
| `MISSING_AMOUNT` | Vault / deposit |
| `INVALID_AMOUNT_TYPE` | Vault / deposit |
| `INVALID_AMOUNT_FORMAT` | Vault / deposit |
| `INVALID_NETWORK` | Vault / deposit |
| `NETWORK_MISMATCH` | Vault / deposit |
| `INVALID_SOURCE_ACCOUNT` | Vault / deposit |
| `INVALID_TRANSACTION_INPUT` | Vault / deposit |
| `SOURCE_ACCOUNT_NOT_FOUND` | Vault / deposit |
| `INVALID_CONTRACT_ID` | Vault / deposit |
| `NETWORK_UNAVAILABLE` | Vault / deposit |
| `TRANSACTION_BUILD_FAILED` | Vault / deposit |
| `INTERNAL_ERROR` | Vault / deposit |
| `INVALID_WEBHOOK_REGISTRATION` | Webhooks |
| `INVALID_WEBHOOK_EVENT_TYPES` | Webhooks |
| `WEBHOOK_NOT_FOUND` | Webhooks |
| `INVALID_WEBHOOK_URL` | Webhooks |
| `WEBHOOK_URL_VALIDATION_FAILED` | Webhooks |
| `MISSING_WEBHOOK_SIGNATURE_HEADERS` | Webhooks |
| `INVALID_WEBHOOK_TIMESTAMP` | Webhooks |
| `WEBHOOK_TIMESTAMP_OUT_OF_WINDOW` | Webhooks |
| `MALFORMED_WEBHOOK_SIGNATURE` | Webhooks |
| `INVALID_WEBHOOK_SIGNATURE` | Webhooks |
| `MALFORMED_WEBHOOK_NONCE` | Webhooks |
| `WEBHOOK_NONCE_REPLAYED` | Webhooks |
| `INVALID_DELIVERY_ID` | Webhooks |
| `INVALID_RETRY_POLICY` | Webhooks |
| `DLQ_ENTRY_NOT_FOUND` | Webhooks |
| `INVALID_IP_FORMAT` | IP allowlist |
| `IP_NOT_ALLOWED` | IP allowlist |
| `DATABASE_NOT_AVAILABLE` | DB / infrastructure |
| `IDEMPOTENCY_CONFLICT` | Idempotency |
| `IDEMPOTENCY_IN_PROGRESS` | Idempotency |
| `SIMULATION_FAILED` | Misc / direct middleware responses |
| `INVALID_AUTH_HEADER` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `MISSING_TOKEN` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `INVALID_TOKEN` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `MISSING_CLAIMS` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `TOKEN_EXPIRED` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `TOKEN_NOT_ACTIVE` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `QUOTA_REQUEST_NOT_FOUND` | Quota self-service |
| `QUOTA_REQUEST_ALREADY_RESOLVED` | Quota self-service |
| `INVALID_QUOTA_REQUEST` | Quota self-service |
| `REQUEST_TIMEOUT` | HTTP fallback derived codes referenced by documentation |
| `REQUEST_BODY_TOO_LARGE` | HTTP fallback derived codes referenced by documentation |
| `UNSUPPORTED_MEDIA_TYPE` | HTTP fallback derived codes referenced by documentation |
| `UNPROCESSABLE_ENTITY` | HTTP fallback derived codes referenced by documentation |
| `USAGE_AGGREGATE_NOT_FOUND` | Admin usage management |
| `INVALID_EXPORT_SCHEDULE` | Export schedules |
| `EXPORT_SCHEDULE_NOT_FOUND` | Export schedules |
| `MISSING_AUTH_FIELDS` | Auth |
| `AUTH_NOT_IMPLEMENTED` | Auth |
| `COMPONENT_NOT_CONFIGURED` | Health / dependency probes |
<!-- END GENERATED ERROR CODES -->

## Architecture

### Components
Expand Down Expand Up @@ -85,7 +183,7 @@ Features:
- Type guard function
- Warning header about auto-generation

### 2. Markdown Documentation (`docs/error-codes.md`)
### 2. Markdown Catalog (`docs/error-code-catalog.md`)

The script injects a generated table between markers:

Expand Down
100 changes: 5 additions & 95 deletions docs/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,103 +6,13 @@ It documents the shared `errorHandler` response envelope, every error class in
billing/Soroban error mapping. It is documentation-only and does not describe
any runtime behavior that is not present in the current source.

<!-- BEGIN GENERATED ERROR CODES -->
## Canonical error code catalog

This section is generated from `docs/error-codes.yaml`. Run `npm run error-codes:generate` after changing the catalog.

| Code | Catalog section |
|---|---|
| `BAD_REQUEST` | HTTP status derived / base app codes |
| `UNAUTHORIZED` | HTTP status derived / base app codes |
| `FORBIDDEN` | HTTP status derived / base app codes |
| `NOT_FOUND` | HTTP status derived / base app codes |
| `PAYMENT_REQUIRED` | HTTP status derived / base app codes |
| `TOO_MANY_REQUESTS` | HTTP status derived / base app codes |
| `CONFLICT` | HTTP status derived / base app codes |
| `INTERNAL_SERVER_ERROR` | HTTP status derived / base app codes |
| `BAD_GATEWAY` | HTTP status derived / base app codes |
| `SERVICE_UNAVAILABLE` | HTTP status derived / base app codes |
| `GATEWAY_TIMEOUT` | HTTP status derived / base app codes |
| `VALIDATION_ERROR` | Validation |
| `INVALID_BODY` | Validation |
| `INVALID_QUERY` | Validation |
| `INVALID_PARAMS` | Validation |
| `INVALID_VALUE` | Validation |
| `GATEWAY_AUTH_CONTEXT_MISSING` | Gateway / proxy |
| `UPSTREAM_TARGET_BLOCKED` | Gateway / proxy |
| `INSUFFICIENT_BALANCE` | Billing / Soroban |
| `SOROBAN_RPC_TIMEOUT` | Billing / Soroban |
| `SOROBAN_RPC_ERROR` | Billing / Soroban |
| `BILLING_DEDUCTION_FAILED` | Billing / Soroban |
| `BILLING_REQUEST_NOT_FOUND` | Billing request |
| `DEVELOPER_NOT_FOUND` | Developer / API keys |
| `API_ACCESS_FORBIDDEN` | Developer / API keys |
| `API_KEY_NOT_FOUND` | Developer / API keys |
| `API_KEY_FORBIDDEN` | Developer / API keys |
| `MISSING_REFRESH_TOKEN` | Refresh-token auth |
| `INVALID_REFRESH_TOKEN` | Refresh-token auth |
| `REVOKED_TOKEN` | Refresh-token auth |
| `EXPIRED_TOKEN` | Refresh-token auth |
| `REFRESH_FAILED` | Refresh-token auth |
| `REVOKE_FAILED` | Refresh-token auth |
| `NOT_AUTHENTICATED` | Refresh-token auth |
| `TOKEN_INFO_FAILED` | Refresh-token auth |
| `VAULT_NOT_FOUND` | Vault / deposit |
| `VAULT_BALANCE_RETRIEVAL_FAILED` | Vault / deposit |
| `MISSING_AMOUNT` | Vault / deposit |
| `INVALID_AMOUNT_TYPE` | Vault / deposit |
| `INVALID_AMOUNT_FORMAT` | Vault / deposit |
| `INVALID_NETWORK` | Vault / deposit |
| `NETWORK_MISMATCH` | Vault / deposit |
| `INVALID_SOURCE_ACCOUNT` | Vault / deposit |
| `INVALID_TRANSACTION_INPUT` | Vault / deposit |
| `SOURCE_ACCOUNT_NOT_FOUND` | Vault / deposit |
| `INVALID_CONTRACT_ID` | Vault / deposit |
| `NETWORK_UNAVAILABLE` | Vault / deposit |
| `TRANSACTION_BUILD_FAILED` | Vault / deposit |
| `INTERNAL_ERROR` | Vault / deposit |
| `INVALID_WEBHOOK_REGISTRATION` | Webhooks |
| `INVALID_WEBHOOK_EVENT_TYPES` | Webhooks |
| `WEBHOOK_NOT_FOUND` | Webhooks |
| `INVALID_WEBHOOK_URL` | Webhooks |
| `WEBHOOK_URL_VALIDATION_FAILED` | Webhooks |
| `MISSING_WEBHOOK_SIGNATURE_HEADERS` | Webhooks |
| `INVALID_WEBHOOK_TIMESTAMP` | Webhooks |
| `WEBHOOK_TIMESTAMP_OUT_OF_WINDOW` | Webhooks |
| `MALFORMED_WEBHOOK_SIGNATURE` | Webhooks |
| `INVALID_WEBHOOK_SIGNATURE` | Webhooks |
| `MALFORMED_WEBHOOK_NONCE` | Webhooks |
| `WEBHOOK_NONCE_REPLAYED` | Webhooks |
| `INVALID_DELIVERY_ID` | Webhooks |
| `INVALID_RETRY_POLICY` | Webhooks |
| `DLQ_ENTRY_NOT_FOUND` | Webhooks |
| `INVALID_IP_FORMAT` | IP allowlist |
| `IP_NOT_ALLOWED` | IP allowlist |
| `DATABASE_NOT_AVAILABLE` | DB / infrastructure |
| `IDEMPOTENCY_CONFLICT` | Idempotency |
| `IDEMPOTENCY_IN_PROGRESS` | Idempotency |
| `SIMULATION_FAILED` | Misc / direct middleware responses |
| `INVALID_AUTH_HEADER` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `MISSING_TOKEN` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `INVALID_TOKEN` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `MISSING_CLAIMS` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `TOKEN_EXPIRED` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `TOKEN_NOT_ACTIVE` | Route-specific / auth overrides (documented in docs/error-codes.md) |
| `QUOTA_REQUEST_NOT_FOUND` | Quota self-service |
| `QUOTA_REQUEST_ALREADY_RESOLVED` | Quota self-service |
| `INVALID_QUOTA_REQUEST` | Quota self-service |
| `REQUEST_TIMEOUT` | HTTP fallback derived codes referenced by documentation |
| `REQUEST_BODY_TOO_LARGE` | HTTP fallback derived codes referenced by documentation |
| `UNSUPPORTED_MEDIA_TYPE` | HTTP fallback derived codes referenced by documentation |
| `UNPROCESSABLE_ENTITY` | HTTP fallback derived codes referenced by documentation |
| `USAGE_AGGREGATE_NOT_FOUND` | Admin usage management |
| `INVALID_EXPORT_SCHEDULE` | Export schedules |
| `EXPORT_SCHEDULE_NOT_FOUND` | Export schedules |
| `MISSING_AUTH_FIELDS` | Auth |
| `AUTH_NOT_IMPLEMENTED` | Auth |
| `COMPONENT_NOT_CONFIGURED` | Health / dependency probes |
<!-- END GENERATED ERROR CODES -->
The canonical, generated list of error codes lives in
[`docs/error-code-catalog.md`](./error-code-catalog.md). It is produced from
[`docs/error-codes.yaml`](./error-codes.yaml) by `npm run error-codes:generate`;
`npm run error-codes:check` verifies it in CI. This page keeps the hand-written
reference for the response envelope, error classes, and the gateway/billing mappings.

## Scope and important caveats

Expand Down
16 changes: 8 additions & 8 deletions scripts/generate-error-codes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const root = process.cwd();
const yamlCatalogPath = path.join(root, "docs", "error-codes.yaml");
const legacyCatalogPath = path.join(root, "src", "errors", "errorCatalog.ts");
const generatedCodesPath = path.join(root, "src", "errors", "codes.ts");
const docsPath = path.join(root, "docs", "error-codes.md");
const catalogPath = path.join(root, "docs", "error-code-catalog.md");
const openApiPath = path.join(root, "docs", "openapi.json");
const checkOnly = process.argv.includes("--check");

Expand Down Expand Up @@ -246,30 +246,30 @@ const tsEnumCurrent = fs.existsSync(generatedCodesPath)
: "";
const tsEnumChanged = writeOrCheck(generatedCodesPath, tsEnumCurrent, tsEnum);

// Generate markdown documentation
const docsCurrent = fs.readFileSync(docsPath, "utf8");
const docsNext = updateGeneratedBlock(docsCurrent, buildMarkdownBlock(entries));
const docsChanged = writeOrCheck(docsPath, docsCurrent, docsNext);
// Generate markdown catalog
const catalogCurrent = fs.readFileSync(catalogPath, "utf8");
const catalogNext = updateGeneratedBlock(catalogCurrent, buildMarkdownBlock(entries));
const catalogChanged = writeOrCheck(catalogPath, catalogCurrent, catalogNext);

// Generate OpenAPI schema
const openApiCurrent = fs.readFileSync(openApiPath, "utf8");
const openApiNext = updateOpenApi(JSON.parse(openApiCurrent), entries);
const openApiChanged = writeOrCheck(openApiPath, openApiCurrent, openApiNext);

if (checkOnly && (tsEnumChanged || docsChanged || openApiChanged)) {
if (checkOnly && (tsEnumChanged || catalogChanged || openApiChanged)) {
process.exit(1);
}

if (!checkOnly) {
const changedFiles = [
tsEnumChanged && "src/errors/codes.ts",
docsChanged && "docs/error-codes.md",
catalogChanged && "docs/error-code-catalog.md",
openApiChanged && "docs/openapi.json",
].filter(Boolean);

if (changedFiles.length > 0) {
console.log(`Updated: ${changedFiles.join(", ")}`);
} else {
console.log("Already up to date: src/errors/codes.ts, docs/error-codes.md, docs/openapi.json");
console.log("Already up to date: src/errors/codes.ts, docs/error-code-catalog.md, docs/openapi.json");
}
}
38 changes: 36 additions & 2 deletions scripts/generate-error-codes.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function writeTestYaml(content) {

function writeTestDocs() {
fs.writeFileSync(
path.join(testDir, "docs", "error-codes.md"),
path.join(testDir, "docs", "error-code-catalog.md"),
"# Error Codes\n\nTest doc\n"
);
}
Expand Down Expand Up @@ -242,7 +242,7 @@ error_codes:
assert.strictEqual(result.success, true, "Should succeed");

const docs = fs.readFileSync(
path.join(testDir, "docs", "error-codes.md"),
path.join(testDir, "docs", "error-code-catalog.md"),
"utf8"
);

Expand All @@ -266,6 +266,40 @@ error_codes:
cleanupTestEnv();
});

// Test 5b: leaves the hand-written envelope guide untouched
test("does not rewrite the hand-written docs/error-codes.md", () => {
createTestEnv();
writeTestDocs();
writeTestOpenApi();

const handWritten = "# Error response envelope\n\nHand-written guide.\n";
fs.writeFileSync(path.join(testDir, "docs", "error-codes.md"), handWritten);

const yaml = `
error_codes:
- code: ENVELOPE_DOC_ERROR
section: Docs
description: Should not leak into the envelope guide
`;

writeTestYaml(yaml);
const result = runCodegen();

assert.strictEqual(result.success, true, "Should succeed");

const after = fs.readFileSync(
path.join(testDir, "docs", "error-codes.md"),
"utf8"
);
assert.strictEqual(after, handWritten, "Hand-written guide must be untouched");
assert.ok(
!after.includes("<!-- BEGIN GENERATED ERROR CODES -->"),
"Hand-written guide must not receive generated markers"
);

cleanupTestEnv();
});

// Test 6: Update OpenAPI schema
test("updates OpenAPI schema with error codes", () => {
createTestEnv();
Expand Down