import type { Context } from 'hono' import type * as z from 'zod/mini' import * as Tidx from './Tidx.js' /** Creates a JSON error response. */ export function error( c: Context, options: error.Options, ) { const { code, details, message, status } = options // Error envelopes embed `requestId` and other per-request state, so they // must never be cached by clients, proxies, or CDNs. c.header('Cache-Control', 'no-store') // Record the stable code so the canonical request log line (`Log.middleware`) // can include it without re-parsing the response body. c.set('errorCode', code) const body: error.Body = { error: { code, ...(details === undefined ? {} : { details }), message, }, requestId: c.get('requestId'), } return c.json(body, status) } /** * Rejects a well-formed but unsupported chain id with a clear 400, listing the * chain ids this deployment serves. Used to short-circuit before an unknown * chain reaches an upstream (which would surface as an opaque `upstream_error`). */ export function unsupportedChainId(c: Context, chainId: number, supported: Iterable) { const ids = [...supported].sort((a, b) => a - b).join(', ') return error(c, { code: 'chain_id_unsupported', message: `Unsupported chain id: ${chainId}. Supported chain ids: ${ids}.`, status: 400, }) } /** * Maps upstream failures to 502, except TIDX query failures after the first positional page return 400. * Preserves `Error` causes for reporting and rethrows non-`Error` causes. */ export function upstream(c: Context, cause: unknown): upstream.Response export function upstream( c: Context, cause: unknown, options: upstream.Options, ): upstream.PageResponse export function upstream(c: Context, cause: unknown, options?: upstream.Options) { if (cause instanceof Error) { // Preserve the original error for error-reporting middleware without // exposing provider URLs, payloads, or credentials in the response. c.error = cause if (options?.page !== undefined && options.page > 1) { if (Tidx.isQueryTimeout(cause)) return error(c, { code: 'query_invalid', message: 'The requested page timed out. Use cursor pagination for deep traversal.', status: 400, }) if (Tidx.isQueryRejection(cause)) return error(c, { code: 'query_invalid', message: 'The requested page was rejected. Use cursor pagination for deep traversal.', status: 400, }) } return error(c, { code: 'upstream_error', message: 'Upstream service could not complete the request', status: 502, }) } throw cause } export declare namespace upstream { /** Pagination context for classifying a deep-page query failure. */ type Options = { /** Validated positional page number, when requested. */ page: number | undefined } /** Upstream response for routes that support positional pagination. */ type PageResponse = | ReturnType> | ReturnType> /** Default upstream error response. */ type Response = ReturnType> } /** * Validates `value` against `schema`. Returns the parsed value on success or * throws a generic upstream error so the public response body never leaks Zod * internals. */ export function validated( schema: schema, value: unknown, ): z.output { const result = schema.safeParse(value) if (result.success) return result.data throw new Error('Upstream returned invalid response data') } /** Converts standard validation issues into API error details. */ export function validationDetails(issues: readonly validationDetails.Issue[]): error.Detail[] { return issues.map((issue) => { const path = issue.path?.map(pathSegment).filter((segment) => segment !== undefined) if (!path?.length) return { message: issue.message } return { message: issue.message, path } }) } export declare namespace error { /** Public API error response body. */ type Body = { /** Public error envelope. */ error: { /** Stable machine-readable error code. */ code: code /** Detailed validation or upstream error information. */ details?: readonly Detail[] | undefined /** Human-readable error message. */ message: string } /** Request id for support/debugging. */ requestId: string } /** One public API error detail. */ type Detail = { /** Human-readable detail message. */ message: string /** Path to the invalid value. */ path?: readonly (number | string)[] | undefined } /** HTTP status code accepted by the public error helper. */ type Status = 400 | 401 | 403 | 404 | 409 | 412 | 413 | 415 | 429 | 500 | 501 | 502 | 503 | 504 /** Options for creating a JSON error response. */ type Options = { /** Stable machine-readable error code. */ code: code /** Detailed validation or upstream error information. */ details?: readonly Detail[] | undefined /** Human-readable error message. */ message: string /** HTTP status code. */ status: status } } export declare namespace validationDetails { /** Standard-schema validation issue shape used by Hono validators. */ type Issue = { /** Human-readable validation message. */ message: string /** Path to the invalid value. */ path?: readonly unknown[] | undefined } } function pathSegment(segment: unknown) { if (typeof segment === 'string' || typeof segment === 'number') return segment if (!segment || typeof segment !== 'object' || !('key' in segment)) return undefined const { key } = segment if (typeof key === 'string' || typeof key === 'number') return key return undefined }