diff --git a/.changeset/vast-parents-roll.md b/.changeset/vast-parents-roll.md new file mode 100644 index 000000000..cd32acf8e --- /dev/null +++ b/.changeset/vast-parents-roll.md @@ -0,0 +1,6 @@ +--- +"@ensnode/ensnode-sdk": patch +"ensapi": patch +--- + +Adds OpenAPI schema endpoint and route descriptions to ENSApi diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index 420b7861f..d421d0f66 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -27,6 +27,7 @@ "@ensnode/ponder-subgraph": "workspace:*", "@hono/node-server": "^1.19.5", "@hono/otel": "^0.2.2", + "@hono/standard-validator": "^0.2.0", "@hono/zod-validator": "^0.7.2", "@namehash/ens-referrals": "workspace:*", "@opentelemetry/api": "^1.9.0", @@ -40,9 +41,11 @@ "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@ponder/utils": "catalog:", + "@standard-schema/utils": "^0.3.0", "date-fns": "catalog:", "drizzle-orm": "catalog:", "hono": "catalog:", + "hono-openapi": "^1.1.1", "p-memoize": "^8.0.0", "p-retry": "^7.1.0", "pg-connection-string": "catalog:", diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index ccb2f40c9..e3e19d779 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -1,5 +1,7 @@ import config from "@/config"; +import { describeRoute, resolver as validationResolver } from "hono-openapi"; + import { IndexingStatusResponseCodes, type IndexingStatusResponseError, @@ -7,6 +9,10 @@ import { serializeENSApiPublicConfig, serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; +import { + makeENSApiPublicConfigSchema, + makeIndexingStatusResponseSchema, +} from "@ensnode/ensnode-sdk/internal"; import { buildEnsApiPublicConfig } from "@/config/config.schema"; import { factory } from "@/lib/hono-factory"; @@ -17,36 +23,76 @@ import resolutionApi from "./resolution-api"; const app = factory.createApp(); -// include ENSApi Public Config endpoint -app.get("/config", async (c) => { - const ensApiPublicConfig = buildEnsApiPublicConfig(config); - return c.json(serializeENSApiPublicConfig(ensApiPublicConfig)); -}); +app.get( + "/config", + describeRoute({ + summary: "Get ENSApi Public Config", + description: "Gets the public config of the ENSApi instance", + responses: { + 200: { + description: "Successfully retrieved ENSApi public config", + content: { + "application/json": { + schema: validationResolver(makeENSApiPublicConfigSchema()), + }, + }, + }, + }, + }), + async (c) => { + const ensApiPublicConfig = buildEnsApiPublicConfig(config); + return c.json(serializeENSApiPublicConfig(ensApiPublicConfig)); + }, +); + +app.get( + "/indexing-status", + describeRoute({ + summary: "Get ENSIndexer Indexing Status", + description: "Returns the indexing status snapshot most recently captured from ENSIndexer", + responses: { + 200: { + description: "Successfully retrieved indexing status", + content: { + "application/json": { + schema: validationResolver(makeIndexingStatusResponseSchema()), + }, + }, + }, + 503: { + description: "Indexing status snapshot unavailable", + content: { + "application/json": { + schema: validationResolver(makeIndexingStatusResponseSchema()), + }, + }, + }, + }, + }), + async (c) => { + // context must be set by the required middleware + if (c.var.indexingStatus === undefined) { + throw new Error(`Invariant(indexing-status): indexingStatusMiddleware required`); + } -// include ENSIndexer Indexing Status endpoint -app.get("/indexing-status", async (c) => { - // context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(ensnode-api): indexingStatusMiddleware required`); - } + if (c.var.indexingStatus instanceof Error) { + return c.json( + serializeIndexingStatusResponse({ + responseCode: IndexingStatusResponseCodes.Error, + } satisfies IndexingStatusResponseError), + 503, + ); + } - if (c.var.indexingStatus instanceof Error) { + // return successful response using the indexing status projection from the middleware context return c.json( serializeIndexingStatusResponse({ - responseCode: IndexingStatusResponseCodes.Error, - } satisfies IndexingStatusResponseError), - 503, + responseCode: IndexingStatusResponseCodes.Ok, + realtimeProjection: c.var.indexingStatus, + } satisfies IndexingStatusResponseOk), ); - } - - // return successful response using the indexing status projection from the context - return c.json( - serializeIndexingStatusResponse({ - responseCode: IndexingStatusResponseCodes.Ok, - realtimeProjection: c.var.indexingStatus, - } satisfies IndexingStatusResponseOk), - ); -}); + }, +); // Name Tokens API app.route("/name-tokens", nameTokensApi); diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 15922625a..e8a81c913 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -1,5 +1,6 @@ import config from "@/config"; +import { describeRoute, resolver as validationResolver } from "hono-openapi"; import { namehash } from "viem"; import z from "zod/v4"; @@ -14,20 +15,21 @@ import { type PluginName, serializeNameTokensResponse, } from "@ensnode/ensnode-sdk"; -import { makeNodeSchema } from "@ensnode/ensnode-sdk/internal"; +import { + ErrorResponseSchema, + makeNameTokensResponseSchema, + makeNodeSchema, +} from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; import { factory } from "@/lib/hono-factory"; -import { makeLogger } from "@/lib/logger"; import { findRegisteredNameTokensForDomain } from "@/lib/name-tokens/find-name-tokens-for-domain"; import { getIndexedSubregistries } from "@/lib/name-tokens/get-indexed-subregistries"; import { nameTokensApiMiddleware } from "@/middleware/name-tokens.middleware"; const app = factory.createApp(); -const logger = makeLogger("name-tokens-api"); - const indexedSubregistries = getIndexedSubregistries( config.namespace, config.ensIndexerPublicConfig.plugins as PluginName[], @@ -44,113 +46,185 @@ app.use(nameTokensApiMiddleware); * Name Tokens API can be requested by either `name` or `domainId`, and * can never be requested by both, or neither. */ -const requestQuerySchema = z.union([ - z.object({ - domainId: makeNodeSchema("request.domainId"), - name: z.undefined(), - }), - z.object({ - domainId: z.undefined(), - name: params.name, - }), -]); - -app.get("/", validate("query", requestQuerySchema), async (c) => { - // Invariant: context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(name-tokens-api): indexingStatusMiddleware required`); - } +const nameTokensQuerySchema = z + .object({ + domainId: makeNodeSchema("request.domainId").optional(), + name: params.name.optional(), + }) + .refine((data) => (data.domainId !== undefined) !== (data.name !== undefined), { + message: "Exactly one of 'domainId' or 'name' must be provided", + }); - // Invariant: Indexing Status has been resolved successfully. - if (c.var.indexingStatus instanceof Error) { - throw new Error(`Invariant(name-tokens-api): Indexing Status has to be resolved successfully`); - } - - const request = c.req.valid("query") satisfies NameTokensRequest; - let domainId: Node | undefined; - - if (request.name !== undefined) { - const { name } = request; +/** + * Factory function for creating a 404 Name Tokens Not Indexed error response + */ +const makeNameTokensNotIndexedResponse = ( + details: string, +): NameTokensResponseErrorNameTokensNotIndexed => ({ + responseCode: NameTokensResponseCodes.Error, + errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, + error: { + message: "No indexed Name Tokens found", + details, + }, +}); - // return 404 when the requested name was the ENS Root - if (name === ENS_ROOT) { +app.get( + "/", + describeRoute({ + summary: "Get Name Tokens", + description: "Returns name tokens for the requested identifier (domainId or name)", + responses: { + 200: { + description: "Name tokens known", + content: { + "application/json": { + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), + }, + }, + }, + 400: { + description: "Invalid input", + content: { + "application/json": { + schema: validationResolver(ErrorResponseSchema), + }, + }, + }, + 404: { + description: "Name tokens not indexed", + content: { + "application/json": { + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), + }, + }, + }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: validationResolver(ErrorResponseSchema), + }, + }, + }, + 503: { + description: + "Service unavailable - Name Tokens API prerequisites not met (indexing status not ready or required plugins not activated)", + content: { + "application/json": { + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), + }, + }, + }, + }, + }), + validate("query", nameTokensQuerySchema), + async (c) => { + // Invariant: context must be set by the required middleware + if (c.var.indexingStatus === undefined) { return c.json( serializeNameTokensResponse({ responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, + errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, error: { - message: "No indexed Name Tokens found", - details: `The 'name' param must not be ENS Root, no tokens exist for it.`, + message: "Name Tokens API is not available yet", + details: "Indexing status middleware is required but not initialized.", }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), - 404, + }), + 503, ); } - const parentNode = namehash(getParentNameFQDN(name)); - const subregistry = indexedSubregistries.find((subregistry) => subregistry.node === parentNode); - - // Return 404 response with error code for Name Tokens Not Indexed when - // the parent name of the requested name was not registered in any of - // the actively indexed subregistries. - if (!subregistry) { - logger.error( - `This ENSNode instance has not been configured to index tokens for the requested name: '${name}'.`, - ); - + // Check if Indexing Status resolution failed. + if (c.var.indexingStatus instanceof Error) { return c.json( serializeNameTokensResponse({ responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, + errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, error: { - message: "No indexed Name Tokens found", - details: `This ENSNode instance has not been configured to index tokens for the requested name: '${name}`, + message: "Name Tokens API is not available yet", + details: + "Indexing status has not yet reached the required state to enable the Name Tokens API.", }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), - 404, + }), + 503, ); } - domainId = namehash(name); - } else { - domainId = request.domainId; - } + const request = c.req.valid("query") satisfies NameTokensRequest; + let domainId: Node; + + if (request.name !== undefined) { + const { name } = request; + + // return 404 when the requested name was the ENS Root + if (name === ENS_ROOT) { + return c.json( + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `The 'name' param must not be ENS Root, no tokens exist for it.`, + ), + ), + 404, + ); + } + + const parentNode = namehash(getParentNameFQDN(name)); + const subregistry = indexedSubregistries.find( + (subregistry) => subregistry.node === parentNode, + ); - const { omnichainSnapshot } = c.var.indexingStatus.snapshot; - const accurateAsOf = omnichainSnapshot.omnichainIndexingCursor; + // Return 404 response with error code for Name Tokens Not Indexed when + // the parent name of the requested name does not match any of the + // actively indexed subregistries. + if (!subregistry) { + return c.json( + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `This ENSNode instance has not been configured to index tokens for the requested name: '${name}`, + ), + ), + 404, + ); + } + + domainId = namehash(name); + } else if (request.domainId !== undefined) { + domainId = request.domainId; + } else { + // This should never happen due to Zod validation, but TypeScript needs this + throw new Error("Invariant(name-tokens-api): Either name or domainId must be provided"); + } - const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf); + const { omnichainSnapshot } = c.var.indexingStatus.snapshot; + const accurateAsOf = omnichainSnapshot.omnichainIndexingCursor; - // Return 404 response with error code for Name Tokens Not Indexed when - // the no name tokens were found for the domain ID associated with - // the requested name. - if (!registeredNameTokens) { - const errorMessageSubject = - request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`; + const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf); - logger.error( - `This ENSNode instance has never indexed tokens for the requested ${errorMessageSubject}.`, - ); + // Return 404 response with error code for Name Tokens Not Indexed when + // no name tokens were found for the domain ID associated with + // the requested name. + if (!registeredNameTokens) { + const errorMessageSubject = + request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`; + + return c.json( + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, + ), + ), + 404, + ); + } return c.json( serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, - error: { - message: "No indexed Name Tokens found", - details: `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, - }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), - 404, + responseCode: NameTokensResponseCodes.Ok, + registeredNameTokens, + }), ); - } - - return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Ok, - registeredNameTokens, - }), - ); -}); + }, +); export default app; diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index e981ca221..42c05558c 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -1,3 +1,4 @@ +import { describeRoute, resolver as validationResolver } from "hono-openapi"; import { z } from "zod/v4"; import type { @@ -6,6 +7,11 @@ import type { ResolvePrimaryNamesResponse, ResolveRecordsResponse, } from "@ensnode/ensnode-sdk"; +import { + makeResolvePrimaryNameResponseSchema, + makeResolvePrimaryNamesResponseSchema, + makeResolveRecordsResponseSchema, +} from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; @@ -44,12 +50,26 @@ app.use(canAccelerateMiddleware); */ app.get( "/records/:name", + describeRoute({ + summary: "Resolve ENS Records", + description: "Resolves ENS records for a given name", + responses: { + 200: { + description: "Successfully resolved records", + content: { + "application/json": { + schema: validationResolver(makeResolveRecordsResponseSchema()), + }, + }, + }, + }, + }), validate("param", z.object({ name: params.name })), validate( "query", z .object({ - ...params.selectionParams.shape, + selection: params.selection, trace: params.trace, accelerate: params.accelerate, }) @@ -99,6 +119,20 @@ app.get( */ app.get( "/primary-name/:address/:chainId", + describeRoute({ + summary: "Resolve Primary Name", + description: "Resolves a primary name for a given `address` and `chainId`", + responses: { + 200: { + description: "Successfully resolved name", + content: { + "application/json": { + schema: validationResolver(makeResolvePrimaryNameResponseSchema()), + }, + }, + }, + }, + }), validate("param", z.object({ address: params.address, chainId: params.defaultableChainId })), validate( "query", @@ -144,6 +178,20 @@ app.get( */ app.get( "/primary-names/:address", + describeRoute({ + summary: "Resolve Primary Names", + description: "Resolves all primary names for a given address across multiple chains", + responses: { + 200: { + description: "Successfully resolved records", + content: { + "application/json": { + schema: validationResolver(makeResolvePrimaryNamesResponseSchema()), + }, + }, + }, + }, + }), validate("param", z.object({ address: params.address })), validate( "query", diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index e29c5b6ec..9c7ca9c53 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -5,6 +5,7 @@ import { serve } from "@hono/node-server"; import { otel } from "@hono/otel"; import { cors } from "hono/cors"; import { html } from "hono/html"; +import { openAPIRouteHandler } from "hono-openapi"; import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; @@ -69,7 +70,23 @@ app.route("/ensanalytics", ensanalyticsApi); // use Am I Realtime API at /amirealtime app.route("/amirealtime", amIRealtimeApi); -// will automatically 500 if config is not available due to ensIndexerPublicConfigMiddleware +// use OpenAPI Schema +app.get( + "/openapi.json", + openAPIRouteHandler(app, { + documentation: { + info: { + title: "ENSApi APIs", + version: packageJson.version, + description: + "APIs for ENS resolution, navigating the ENS nameforest, and metadata about an ENSNode", + }, + servers: [{ url: `http://localhost:${config.port}`, description: "Local Development" }], + }, + }), +); + +// will automatically 503 if config is not available due to ensIndexerPublicConfigMiddleware app.get("/health", async (c) => { return c.json({ message: "fallback ok" }); }); diff --git a/apps/ensapi/src/lib/handlers/error-response.ts b/apps/ensapi/src/lib/handlers/error-response.ts index 8474a1450..d1f489b04 100644 --- a/apps/ensapi/src/lib/handlers/error-response.ts +++ b/apps/ensapi/src/lib/handlers/error-response.ts @@ -1,3 +1,4 @@ +import { SchemaError } from "@standard-schema/utils"; import type { Context } from "hono"; import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status"; import { treeifyError, ZodError } from "zod/v4"; @@ -8,16 +9,17 @@ import type { ErrorResponse } from "@ensnode/ensnode-sdk"; * Creates a standardized error response for the ENSApi. * * Handles different types of errors and converts them to appropriate HTTP responses - * with consistent error formatting. ZodErrors return 400 status codes with validation - * details, while other errors return 500 status codes. + * with consistent error formatting. ZodErrors and Standard Schema validation errors + * return 400 status codes with validation details, while other errors return 500 + * status codes. * * @param c - Hono context object - * @param input - The error input (ZodError, Error, string, or unknown) + * @param input - The error input (ZodError, SchemaError, Error, string, or unknown) * @returns JSON error response with appropriate HTTP status code */ export const errorResponse = ( c: Context, - input: ZodError | Error | string | unknown, + input: ZodError | SchemaError | Error | string | unknown, statusCode: ClientErrorStatusCode | ServerErrorStatusCode = 500, ) => { if (input instanceof ZodError) { @@ -27,6 +29,15 @@ export const errorResponse = ( ); } + if (input instanceof SchemaError) { + // Convert Standard Schema issues to ZodError for consistent formatting + const zodError = new ZodError(input.issues as ZodError["issues"]); + return c.json( + { message: "Invalid Input", details: treeifyError(zodError) } satisfies ErrorResponse, + 400, + ); + } + if (input instanceof Error) { return c.json({ message: input.message } satisfies ErrorResponse, statusCode); } diff --git a/apps/ensapi/src/lib/handlers/validate.ts b/apps/ensapi/src/lib/handlers/validate.ts index 4aa4d33f2..4316e07bb 100644 --- a/apps/ensapi/src/lib/handlers/validate.ts +++ b/apps/ensapi/src/lib/handlers/validate.ts @@ -1,5 +1,6 @@ -import { zValidator } from "@hono/zod-validator"; +import { SchemaError } from "@standard-schema/utils"; import type { ValidationTargets } from "hono"; +import { validator } from "hono-openapi"; import type { ZodType } from "zod/v4"; import { errorResponse } from "./error-response"; @@ -7,7 +8,7 @@ import { errorResponse } from "./error-response"; /** * Creates a Hono validation middleware with custom error formatting. * - * Wraps the Hono zValidator with custom error handling that uses the + * Wraps the Hono validator with custom error handling that uses the * errorResponse function for consistent error formatting across the API. * * @param target - The validation target (param, query, json, etc.) @@ -18,7 +19,11 @@ export const validate = - zValidator(target, schema, (result, c) => { + validator(target, schema, (result, c) => { // if validation failed, return our custom-formatted ErrorResponse instead of default - if (!result.success) return errorResponse(c, result.error); + if (!result.success) { + // Wrap the Standard Schema issues in a SchemaError instance + // for consistent error handling in errorResponse + return errorResponse(c, new SchemaError(result.error)); + } }); diff --git a/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts b/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts index a0fb34ac0..c16c1de0f 100644 --- a/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts +++ b/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts @@ -9,7 +9,9 @@ import { makeNameTokensResponseSchema } from "./zod-schemas"; export function deserializedNameTokensResponse( maybeResponse: SerializedNameTokensResponse, ): NameTokensResponse { - const parsed = makeNameTokensResponseSchema().safeParse(maybeResponse); + const parsed = makeNameTokensResponseSchema("Name Tokens Response", false).safeParse( + maybeResponse, + ); if (parsed.error) { throw new Error(`Cannot deserialize NameTokensResponse:\n${prettifyError(parsed.error)}\n`); diff --git a/packages/ensnode-sdk/src/api/name-tokens/request.ts b/packages/ensnode-sdk/src/api/name-tokens/request.ts index 7aca3cd8f..7a9eac92d 100644 --- a/packages/ensnode-sdk/src/api/name-tokens/request.ts +++ b/packages/ensnode-sdk/src/api/name-tokens/request.ts @@ -2,26 +2,17 @@ import type { Name, Node } from "../../ens"; /** * Represents request to Name Tokens API. + * + * Either `domainId` or `name` must be provided, but not both. */ -export interface NameTokensRequestByDomainId { - domainId: Node; - +export interface NameTokensRequest { /** - * Name for which name tokens were requested. + * Domain ID (namehash) for which name tokens were requested. */ - name?: undefined; -} - -/** - * Represents request to Name Tokens API. - */ -export interface NameTokensRequestByName { - domainId?: undefined; + domainId?: Node; /** * Name for which name tokens were requested. */ - name: Name; + name?: Name; } - -export type NameTokensRequest = NameTokensRequestByDomainId | NameTokensRequestByName; diff --git a/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts b/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts index 6bf23db3e..344f2d108 100644 --- a/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts @@ -1,6 +1,5 @@ import { namehash } from "viem"; import z from "zod/v4"; -import type { ParsePayload } from "zod/v4/core"; import { makeNodeSchema, @@ -22,78 +21,78 @@ import { type RegisteredNameTokens, } from "./response"; -function invariant_nameIsAssociatedWithDomainId(ctx: ParsePayload) { - const { name, domainId } = ctx.value; - - if (namehash(name) !== domainId) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: `'name' must be associated with 'domainId': ${domainId}`, - }); - } -} - -function invariant_nameTokensOwnershipTypeNameWrapperRequiresOwnershipTypeFullyOnchainOrUnknown( - ctx: ParsePayload, -) { - const { tokens } = ctx.value; - const containsOwnershipNameWrapper = tokens.some( - (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.NameWrapper, - ); - const containsOwnershipFullyOnchainOrUnknown = tokens.some( - (t) => - t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain || - t.ownership.ownershipType === NameTokenOwnershipTypes.Unknown, - ); - if (containsOwnershipNameWrapper && !containsOwnershipFullyOnchainOrUnknown) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: `'tokens' must contain name token with ownership type 'fully-onchain' or 'unknown' when name token with ownership type 'namewrapper' in listed`, - }); - } -} - -function invariant_nameTokensContainAtMostOneWithOwnershipTypeEffective( - ctx: ParsePayload, -) { - const { tokens } = ctx.value; - const tokensCountWithOwnershipFullyOnchain = tokens.filter( - (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain, - ).length; - if (tokensCountWithOwnershipFullyOnchain > 1) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: `'tokens' must contain at most one name token with ownership type 'fully-onchain', current count: ${tokensCountWithOwnershipFullyOnchain}`, - }); - } -} - /** * Schema for {@link RegisteredNameTokens}. */ -export const makeRegisteredNameTokenSchema = (valueLabel: string = "Registered Name Token") => +export const makeRegisteredNameTokenSchema = ( + valueLabel: string = "Registered Name Token", + serializable?: SerializableType, +) => z .object({ domainId: makeNodeSchema(`${valueLabel}.domainId`), name: makeReinterpretedNameSchema(valueLabel), - tokens: z.array(makeNameTokenSchema(`${valueLabel}.tokens`)).nonempty(), + tokens: z.array(makeNameTokenSchema(`${valueLabel}.tokens`, serializable)).nonempty(), expiresAt: makeUnixTimestampSchema(`${valueLabel}.expiresAt`), accurateAsOf: makeUnixTimestampSchema(`${valueLabel}.accurateAsOf`), }) - .check(invariant_nameIsAssociatedWithDomainId) - .check(invariant_nameTokensContainAtMostOneWithOwnershipTypeEffective) - .check(invariant_nameTokensOwnershipTypeNameWrapperRequiresOwnershipTypeFullyOnchainOrUnknown); + .check(function invariant_nameIsAssociatedWithDomainId(ctx) { + const { name, domainId } = ctx.value; + + if (namehash(name) !== domainId) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `'name' must be associated with 'domainId': ${domainId}`, + }); + } + }) + .check( + function invariant_nameTokensOwnershipTypeNameWrapperRequiresOwnershipTypeFullyOnchainOrUnknown( + ctx, + ) { + const { tokens } = ctx.value; + const containsOwnershipNameWrapper = tokens.some( + (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.NameWrapper, + ); + const containsOwnershipFullyOnchainOrUnknown = tokens.some( + (t) => + t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain || + t.ownership.ownershipType === NameTokenOwnershipTypes.Unknown, + ); + if (containsOwnershipNameWrapper && !containsOwnershipFullyOnchainOrUnknown) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `'tokens' must contain name token with ownership type 'fully-onchain' or 'unknown' when name token with ownership type 'namewrapper' in listed`, + }); + } + }, + ) + .check(function invariant_nameTokensContainAtMostOneWithOwnershipTypeEffective(ctx) { + const { tokens } = ctx.value; + const tokensCountWithOwnershipFullyOnchain = tokens.filter( + (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain, + ).length; + if (tokensCountWithOwnershipFullyOnchain > 1) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `'tokens' must contain at most one name token with ownership type 'fully-onchain', current count: ${tokensCountWithOwnershipFullyOnchain}`, + }); + } + }); /** * Schema for {@link NameTokensResponseOk} */ -export const makeNameTokensResponseOkSchema = (valueLabel: string = "Name Tokens Response OK") => +export const makeNameTokensResponseOkSchema = ( + valueLabel: string = "Name Tokens Response OK", + serializable?: SerializableType, +) => z.strictObject({ responseCode: z.literal(NameTokensResponseCodes.Ok), - registeredNameTokens: makeRegisteredNameTokenSchema(`${valueLabel}.nameTokens`), + registeredNameTokens: makeRegisteredNameTokenSchema(`${valueLabel}.nameTokens`, serializable), }); /** @@ -145,8 +144,12 @@ export const makeNameTokensResponseErrorSchema = ( /** * Schema for {@link NameTokensResponse} */ -export const makeNameTokensResponseSchema = (valueLabel: string = "Name Tokens Response") => - z.discriminatedUnion("responseCode", [ - makeNameTokensResponseOkSchema(valueLabel), +export const makeNameTokensResponseSchema = ( + valueLabel: string = "Name Tokens Response", + serializable?: SerializableType, +) => { + return z.discriminatedUnion("responseCode", [ + makeNameTokensResponseOkSchema(valueLabel, serializable ?? false), makeNameTokensResponseErrorSchema(valueLabel), ]); +}; diff --git a/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts b/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts new file mode 100644 index 000000000..5beab7a42 --- /dev/null +++ b/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts @@ -0,0 +1,45 @@ +import z from "zod/v4"; + +/** + * Schema for resolver records response (addresses, texts, name) + */ +const makeResolverRecordsResponseSchema = () => + z.object({ + name: z.string().nullable().optional(), + addresses: z.record(z.string(), z.string().nullable()).optional(), + texts: z.record(z.string(), z.string().nullable()).optional(), + }); + +/** + * Schema for {@link ResolveRecordsResponse} + */ +export const makeResolveRecordsResponseSchema = () => + z.object({ + records: makeResolverRecordsResponseSchema(), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + // TODO: Find a better way to handle recursive types, patch solution is .unknown() + trace: z.array(z.unknown()).optional(), + }); + +/** + * Schema for {@link ResolvePrimaryNameResponse} + */ +export const makeResolvePrimaryNameResponseSchema = () => + z.object({ + name: z.string().nullable(), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.array(z.unknown()).optional(), + }); + +/** + * Schema for {@link ResolvePrimaryNamesResponse} + */ +export const makeResolvePrimaryNamesResponseSchema = () => + z.object({ + names: z.record(z.number(), z.string().nullable()), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.array(z.unknown()).optional(), + }); diff --git a/packages/ensnode-sdk/src/client.ts b/packages/ensnode-sdk/src/client.ts index 048f54980..6b3668a4b 100644 --- a/packages/ensnode-sdk/src/client.ts +++ b/packages/ensnode-sdk/src/client.ts @@ -807,7 +807,7 @@ export class ENSNodeClient { if (request.name !== undefined) { url.searchParams.set("name", request.name); - } else { + } else if (request.domainId !== undefined) { url.searchParams.set("domainId", request.domainId); } diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 6e2153f0e..8ed2b332a 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -13,9 +13,12 @@ */ export * from "./api/indexing-status/zod-schemas"; +export * from "./api/name-tokens/zod-schemas"; export * from "./api/registrar-actions/zod-schemas"; +export * from "./api/resolution/zod-schemas"; export * from "./api/shared/errors/zod-schemas"; export * from "./api/shared/pagination/zod-schemas"; +export * from "./ensapi/config/zod-schemas"; export * from "./ensindexer/config/zod-schemas"; export * from "./ensindexer/indexing-status/zod-schemas"; export * from "./registrars/zod-schemas"; diff --git a/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts b/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts index 77bebabb4..8d866e6ab 100644 --- a/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts +++ b/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts @@ -20,15 +20,41 @@ import { type NameTokenOwnershipUnknown, } from "./name-token"; +const tokenIdSchemaSerializable = z.string(); +const tokenIdSchemaNative = z.preprocess( + (v) => (typeof v === "string" ? BigInt(v) : v), + z.bigint().positive(), +); + +export function makeTokenIdSchema( + _valueLabel: string, + serializable: SerializableType, +): SerializableType extends true ? typeof tokenIdSchemaSerializable : typeof tokenIdSchemaNative; +export function makeTokenIdSchema( + _valueLabel: string = "Token ID Schema", + serializable: true | false = false, +): typeof tokenIdSchemaSerializable | typeof tokenIdSchemaNative { + if (serializable) { + return tokenIdSchemaSerializable; + } else { + return tokenIdSchemaNative; + } +} + /** * Make schema for {@link AssetId}. + * */ -export const makeAssetIdSchema = (valueLabel: string = "Asset ID Schema") => - z.object({ +export const makeAssetIdSchema = ( + valueLabel: string = "Asset ID Schema", + serializable?: SerializableType, +) => { + return z.object({ assetNamespace: z.enum(AssetNamespaces), contract: makeAccountIdSchema(valueLabel), - tokenId: z.preprocess((v) => (typeof v === "string" ? BigInt(v) : v), z.bigint().positive()), + tokenId: makeTokenIdSchema(valueLabel, serializable ?? false), }); +}; /** * Make schema for {@link AssetIdString}. @@ -137,9 +163,12 @@ export const makeNameTokenOwnershipSchema = (valueLabel: string = "Name Token Ow /** * Make schema for {@link NameToken}. */ -export const makeNameTokenSchema = (valueLabel: string = "Name Token Schema") => +export const makeNameTokenSchema = ( + valueLabel: string = "Name Token Schema", + serializable?: SerializableType, +) => z.object({ - token: makeAssetIdSchema(`${valueLabel}.token`), + token: makeAssetIdSchema(`${valueLabel}.token`, serializable), ownership: makeNameTokenOwnershipSchema(`${valueLabel}.ownership`), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa4c6187c..a91677811 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,18 +6,69 @@ settings: catalogs: default: + '@astrojs/react': + specifier: ^4.4.1 + version: 4.4.1 + '@astrojs/tailwind': + specifier: ^6.0.2 + version: 6.0.2 + '@namehash/namekit-react': + specifier: 0.12.0 + version: 0.12.0 + '@ponder/utils': + specifier: 0.2.14 + version: 0.2.14 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.0 '@types/node': specifier: 22.18.13 version: 22.18.13 + astro: + specifier: ^5.15.9 + version: 5.16.0 + astro-font: + specifier: ^1.1.0 + version: 1.1.0 + astro-seo: + specifier: ^0.8.4 + version: 0.8.4 + caip: + specifier: 1.1.1 + version: 1.1.1 + date-fns: + specifier: 4.1.0 + version: 4.1.0 + drizzle-orm: + specifier: '=0.41.0' + version: 0.41.0 hono: specifier: ^4.10.2 version: 4.10.3 + pg-connection-string: + specifier: ^2.9.1 + version: 2.9.1 + pino: + specifier: 10.1.0 + version: 10.1.0 + ponder: + specifier: 0.13.16 + version: 0.13.16 + tsup: + specifier: ^8.3.6 + version: 8.5.0 typescript: specifier: ^5.7.3 version: 5.9.3 + viem: + specifier: ^2.22.13 + version: 2.38.5 vitest: specifier: ^4.0.2 version: 4.0.5 + zod: + specifier: ^3.25.7 + version: 3.25.76 overrides: '@adraffy/ens-normalize': 1.11.1 @@ -244,6 +295,9 @@ importers: '@hono/otel': specifier: ^0.2.2 version: 0.2.2(hono@4.10.3) + '@hono/standard-validator': + specifier: ^0.2.0 + version: 0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3) '@hono/zod-validator': specifier: ^0.7.2 version: 0.7.4(hono@4.10.3)(zod@3.25.76) @@ -283,6 +337,9 @@ importers: '@ponder/utils': specifier: 'catalog:' version: 0.2.14(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76)) + '@standard-schema/utils': + specifier: ^0.3.0 + version: 0.3.0 date-fns: specifier: 'catalog:' version: 4.1.0 @@ -292,6 +349,9 @@ importers: hono: specifier: 'catalog:' version: 4.10.3 + hono-openapi: + specifier: ^1.1.1 + version: 1.1.1(@hono/standard-validator@0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(hono@4.10.3)(openapi-types@12.1.3) p-memoize: specifier: ^8.0.0 version: 8.0.0 @@ -1968,6 +2028,12 @@ packages: peerDependencies: hono: '*' + '@hono/standard-validator@0.2.0': + resolution: {integrity: sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ==} + peerDependencies: + '@standard-schema/spec': 1.0.0 + hono: '>=3.9.0' + '@hono/zod-validator@0.7.4': resolution: {integrity: sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q==} peerDependencies: @@ -3541,9 +3607,73 @@ packages: resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} + '@standard-community/standard-json@0.3.5': + resolution: {integrity: sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==} + peerDependencies: + '@standard-schema/spec': ^1.0.0 + '@types/json-schema': ^7.0.15 + '@valibot/to-json-schema': ^1.3.0 + arktype: ^2.1.20 + effect: ^3.16.8 + quansync: ^0.2.11 + sury: ^10.0.0 + typebox: ^1.0.17 + valibot: ^1.1.0 + zod: ^3.25.0 || ^4.0.0 + zod-to-json-schema: ^3.24.5 + peerDependenciesMeta: + '@valibot/to-json-schema': + optional: true + arktype: + optional: true + effect: + optional: true + sury: + optional: true + typebox: + optional: true + valibot: + optional: true + zod: + optional: true + zod-to-json-schema: + optional: true + + '@standard-community/standard-openapi@0.2.9': + resolution: {integrity: sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg==} + peerDependencies: + '@standard-community/standard-json': ^0.3.5 + '@standard-schema/spec': ^1.0.0 + arktype: ^2.1.20 + effect: ^3.17.14 + openapi-types: ^12.1.3 + sury: ^10.0.0 + typebox: ^1.0.0 + valibot: ^1.1.0 + zod: ^3.25.0 || ^4.0.0 + zod-openapi: ^4 + peerDependenciesMeta: + arktype: + optional: true + effect: + optional: true + sury: + optional: true + typebox: + optional: true + valibot: + optional: true + zod: + optional: true + zod-openapi: + optional: true + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -3833,6 +3963,9 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5541,6 +5674,21 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hono-openapi@1.1.1: + resolution: {integrity: sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q==} + peerDependencies: + '@hono/standard-validator': ^0.1.2 + '@standard-community/standard-json': ^0.3.5 + '@standard-community/standard-openapi': ^0.2.8 + '@types/json-schema': ^7.0.15 + hono: ^4.8.3 + openapi-types: ^12.1.3 + peerDependenciesMeta: + '@hono/standard-validator': + optional: true + hono: + optional: true + hono@4.10.3: resolution: {integrity: sha512-2LOYWUbnhdxdL8MNbNg9XZig6k+cZXm5IjHn2Aviv7honhBMOHb+jxrKIeJRZJRmn+htUCKhaicxwXuUDlchRA==} engines: {node: '>=16.9.0'} @@ -6457,6 +6605,9 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -9909,6 +10060,11 @@ snapshots: '@opentelemetry/semantic-conventions': 1.37.0 hono: 4.10.3 + '@hono/standard-validator@0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3)': + dependencies: + '@standard-schema/spec': 1.0.0 + hono: 4.10.3 + '@hono/zod-validator@0.7.4(hono@4.10.3)(zod@3.25.76)': dependencies: hono: 4.10.3 @@ -11682,8 +11838,27 @@ snapshots: dependencies: tslib: 2.8.1 + '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/json-schema': 7.0.15 + quansync: 0.2.11 + optionalDependencies: + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + + '@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76)': + dependencies: + '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76) + '@standard-schema/spec': 1.0.0 + openapi-types: 12.1.3 + optionalDependencies: + zod: 3.25.76 + '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -11991,6 +12166,8 @@ snapshots: '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -12097,22 +12274,6 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 4.0.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) - - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 4.0.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.5 @@ -13999,6 +14160,16 @@ snapshots: help-me@5.0.0: {} + hono-openapi@1.1.1(@hono/standard-validator@0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(hono@4.10.3)(openapi-types@12.1.3): + dependencies: + '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76) + '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76) + '@types/json-schema': 7.0.15 + openapi-types: 12.1.3 + optionalDependencies: + '@hono/standard-validator': 0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3) + hono: 4.10.3 + hono@4.10.3: {} html-encoding-sniffer@4.0.0: @@ -15097,6 +15268,8 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + openapi-types@12.1.3: {} + outdent@0.5.0: {} ox@0.9.6(typescript@5.9.3)(zod@3.25.76): @@ -16955,7 +17128,7 @@ snapshots: vite@5.4.21(@types/node@22.18.13)(lightningcss@1.30.2): dependencies: - esbuild: 0.25.11 + esbuild: 0.27.2 postcss: 8.5.6 rollup: 4.52.5 optionalDependencies: @@ -16965,7 +17138,7 @@ snapshots: vite@5.4.21(@types/node@24.10.4)(lightningcss@1.30.2): dependencies: - esbuild: 0.25.11 + esbuild: 0.27.2 postcss: 8.5.6 rollup: 4.52.5 optionalDependencies: @@ -17060,7 +17233,7 @@ snapshots: vitest@4.0.5(@types/debug@4.1.12)(@types/node@20.19.24)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.5 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.5 '@vitest/runner': 4.0.5 '@vitest/snapshot': 4.0.5 @@ -17100,7 +17273,7 @@ snapshots: vitest@4.0.5(@types/debug@4.1.12)(@types/node@22.18.13)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.5 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.5 '@vitest/runner': 4.0.5 '@vitest/snapshot': 4.0.5