diff --git a/docs/error-code-catalog.md b/docs/error-code-catalog.md index f04f1315..8c4f29cc 100644 --- a/docs/error-code-catalog.md +++ b/docs/error-code-catalog.md @@ -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. + +## 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 | + + ## Architecture ### Components @@ -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: diff --git a/docs/error-codes.md b/docs/error-codes.md index 0f27a942..ce056369 100644 --- a/docs/error-codes.md +++ b/docs/error-codes.md @@ -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. - ## 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 | - +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 diff --git a/scripts/generate-error-codes.mjs b/scripts/generate-error-codes.mjs index 94ce41f2..2ff057b4 100644 --- a/scripts/generate-error-codes.mjs +++ b/scripts/generate-error-codes.mjs @@ -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"); @@ -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"); } } diff --git a/scripts/generate-error-codes.test.mjs b/scripts/generate-error-codes.test.mjs index 30e59bd4..03650627 100644 --- a/scripts/generate-error-codes.test.mjs +++ b/scripts/generate-error-codes.test.mjs @@ -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" ); } @@ -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" ); @@ -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(""), + "Hand-written guide must not receive generated markers" + ); + + cleanupTestEnv(); +}); + // Test 6: Update OpenAPI schema test("updates OpenAPI schema with error codes", () => { createTestEnv();