diff --git a/.changeset/omnigraph-mcp.md b/.changeset/omnigraph-mcp.md new file mode 100644 index 000000000..ffba2e154 --- /dev/null +++ b/.changeset/omnigraph-mcp.md @@ -0,0 +1,5 @@ +--- +"ensapi": patch +--- + +Add Omnigraph MCP server at `/api/mcp` (streamable HTTP). MCP clients can run vetted example queries or custom GraphQL via `omnigraph_query`, with schema and example resources. Server name is `ENS`; task-specific MCP prompts removed. Initialize instructions require `exampleId` first and inline the full example catalog. diff --git a/.changeset/omnigraph-schema-reference.md b/.changeset/omnigraph-schema-reference.md new file mode 100644 index 000000000..a9da3852a --- /dev/null +++ b/.changeset/omnigraph-schema-reference.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": patch +--- + +Add offline Omnigraph schema lookup helpers (`lookupOmnigraphSchema`, `buildCondensedSchemaReference`) on `@ensnode/ensnode-sdk/internal` for enscli and ensskills. Bundle Omnigraph SDL in ensnode-sdk (generated from `enssdk/omnigraph/schema.graphql`) so schema loading works in Vite/Rollup builds without Node `createRequire`. diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index b9671aab3..7c1367533 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -91,11 +91,11 @@ jobs: - name: Verify generated files are committed run: | - if ! git diff --quiet packages/enssdk/src/omnigraph/generated/; then + if ! git diff --quiet packages/enssdk/src/omnigraph/generated/ packages/ensnode-sdk/src/omnigraph-api/generated/; then echo "Error: Generated files are out of sync" echo "" echo "The following generated files differ from what is committed:" - git diff --name-status packages/enssdk/src/omnigraph/generated/ + git diff --name-status packages/enssdk/src/omnigraph/generated/ packages/ensnode-sdk/src/omnigraph-api/generated/ echo "" echo "To fix, run:" echo " pnpm generate:gqlschema" diff --git a/.gitignore b/.gitignore index 898696d36..929c4ae5a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ dist # Ponder generated !packages/enssdk/src/omnigraph/generated/ +!packages/ensnode-sdk/src/omnigraph-api/generated/ .ponder #jetbrains elements @@ -46,3 +47,7 @@ apps/ensrainbow/test-* apps/fallback-ensapi/dist + +# Agent skills from npm packages (managed by skills-npm) +**/skills/npm-* +data-* diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index 4a9b71ed7..b6810916c 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -28,9 +28,11 @@ "@ensnode/ensdb-sdk": "workspace:*", "@ensnode/ensnode-sdk": "workspace:*", "@ensnode/ponder-subgraph": "workspace:*", + "@hono/mcp": "^0.3.0", "@hono/node-server": "catalog:", "@hono/otel": "^0.2.2", "@hono/zod-openapi": "^1.4.0", + "@modelcontextprotocol/sdk": "^1.29.0", "@namehash/ens-referrals": "workspace:*", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.7.1", diff --git a/apps/ensapi/src/handlers/api/mcp/mcp-api.integration.test.ts b/apps/ensapi/src/handlers/api/mcp/mcp-api.integration.test.ts new file mode 100644 index 000000000..68b22a009 --- /dev/null +++ b/apps/ensapi/src/handlers/api/mcp/mcp-api.integration.test.ts @@ -0,0 +1,113 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +const ensnodeUrl = process.env.ENSNODE_URL; +if (!ensnodeUrl) { + throw new Error( + "ENSNODE_URL environment variable must be configured for Omnigraph MCP integration tests to run.", + ); +} + +const MCP_URL = new URL("/api/mcp", ensnodeUrl); + +type TextContent = { type: "text"; text: string }; + +/** Connects an MCP client to the live ENSApi `/api/mcp` endpoint over Streamable HTTP. */ +async function connect() { + const client = new Client({ name: "ensapi-integration-test", version: "0.0.0" }); + const transport = new StreamableHTTPClientTransport(MCP_URL); + await client.connect(transport); + return client; +} + +/** Calls `omnigraph_query` and parses the single text content block as GraphQL JSON. */ +async function omnigraphQuery( + client: Client, + query: string, + variables?: Record, +): Promise<{ data?: T; errors?: Array<{ message: string }> }> { + const result = await client.callTool({ + name: "omnigraph_query", + arguments: { query, variables }, + }); + + const content = result.content as TextContent[]; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + return JSON.parse(content[0].text); +} + +describe("Omnigraph MCP API (/api/mcp)", () => { + let client: Client; + + beforeEach(async () => { + client = await connect(); + }); + + afterEach(async () => { + await client.close(); + }); + + it("advertises the omnigraph_query tool", async () => { + const { tools } = await client.listTools(); + expect(tools.map((t) => t.name)).toEqual( + expect.arrayContaining(["omnigraph_query", "omnigraph_schema"]), + ); + }); + + it("executes a trivial query end-to-end through Yoga", async () => { + const { data, errors } = await omnigraphQuery<{ __typename: string }>(client, "{ __typename }"); + + expect(errors).toBeUndefined(); + expect(data).toEqual({ __typename: "Query" }); + }); + + it("resolves a seeded devnet domain via Query.domain", async () => { + const { data, errors } = await omnigraphQuery<{ + domain: { canonical: { name: { interpreted: string } } } | null; + }>( + client, + /* GraphQL */ ` + query DomainByName($name: InterpretedName!) { + domain(by: { name: $name }) { + canonical { name { interpreted } } + } + } + `, + { name: "test.eth" }, + ); + + expect(errors).toBeUndefined(); + expect(data).toMatchObject({ + domain: { canonical: { name: { interpreted: "test.eth" } } }, + }); + }); + + it("surfaces GraphQL errors for invalid queries in the response payload", async () => { + const { errors } = await omnigraphQuery(client, "{ thisFieldDoesNotExist }"); + expect(errors).toBeDefined(); + expect(errors?.length ?? 0).toBeGreaterThan(0); + }); + + it("runs a vetted example query by exampleId", async () => { + const result = await client.callTool({ + name: "omnigraph_query", + arguments: { + exampleId: "domain-by-name", + variables: { name: "test.eth" }, + }, + }); + + const content = result.content as TextContent[]; + const { data, errors } = JSON.parse(content[0].text) as { + data?: { domain: { canonical: { name: { interpreted: string } } } | null }; + errors?: unknown[]; + }; + + expect(errors).toBeUndefined(); + expect(data).toMatchObject({ + domain: { canonical: { name: { interpreted: "test.eth" } } }, + }); + }); +}); diff --git a/apps/ensapi/src/handlers/api/mcp/mcp-api.test.ts b/apps/ensapi/src/handlers/api/mcp/mcp-api.test.ts new file mode 100644 index 000000000..44cb3345a --- /dev/null +++ b/apps/ensapi/src/handlers/api/mcp/mcp-api.test.ts @@ -0,0 +1,436 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the Omnigraph API handler so the tool can be exercised without a database. The tool +// imports it dynamically, so the mock is applied at call time. +const omnigraphApiFetchMock = vi.fn<(request: Request) => Promise>(); +vi.mock("@/handlers/api/omnigraph/omnigraph-api", () => ({ + default: { fetch: omnigraphApiFetchMock }, +})); + +import mcpApi, { createOmnigraphMcpServer } from "./mcp-api"; + +const activeConnections: Array<{ client: Client; server: McpServer }> = []; +const activeHttpSessionIds: string[] = []; + +/** Wires an MCP `Client` directly to a fresh server over an in-memory transport pair. */ +async function connectClient() { + const server = createOmnigraphMcpServer(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: "test-client", version: "0.0.0" }); + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + activeConnections.push({ client, server }); + return { client, server }; +} + +describe("Omnigraph MCP server", () => { + beforeEach(() => { + omnigraphApiFetchMock.mockReset(); + }); + + afterEach(async () => { + for (const { client, server } of activeConnections) { + await client.close(); + await server.close(); + } + activeConnections.length = 0; + + for (const sessionId of activeHttpSessionIds) { + await mcpApi.fetch( + new Request("http://ensapi.internal/", { + method: "DELETE", + headers: { "mcp-session-id": sessionId }, + }), + ); + } + activeHttpSessionIds.length = 0; + + vi.clearAllMocks(); + }); + + it("advertises omnigraph tools", async () => { + const { client } = await connectClient(); + + const { tools } = await client.listTools(); + + expect(tools.map((t) => t.name)).toEqual( + expect.arrayContaining(["omnigraph_query", "omnigraph_schema"]), + ); + }); + + it("advertises schema and example resources", async () => { + const { client } = await connectClient(); + + const { resources } = await client.listResources(); + + expect(resources.map((resource) => resource.uri)).toEqual( + expect.arrayContaining(["omnigraph://schema/condensed", "omnigraph://examples/index"]), + ); + }); + + it("reads the condensed schema resource", async () => { + const { client } = await connectClient(); + + const { contents } = await client.readResource({ uri: "omnigraph://schema/condensed" }); + + expect(contents).toHaveLength(1); + expect(contents[0]).toMatchObject({ mimeType: "text/markdown" }); + if (!("text" in contents[0])) throw new Error("expected text resource"); + expect(contents[0].text).toContain("account(by: AccountByInput!)"); + expect(contents[0].text).toContain("resolve(accelerate: Boolean): ReverseResolve!"); + expect(contents[0].text).toContain("ProfileSocials"); + expect(contents[0].text).toContain("github:"); + expect(contents[0].text).toContain("DomainsNameFilter"); + expect(contents[0].text).toContain("starts_with:"); + }); + + it("includes defaultVariables in the examples index", async () => { + const { client } = await connectClient(); + + const { contents } = await client.readResource({ uri: "omnigraph://examples/index" }); + + if (!("text" in contents[0])) throw new Error("expected text resource"); + const payload = JSON.parse(contents[0].text) as { + examples: Array<{ id: string; defaultVariables?: Record }>; + }; + const registryDomains = payload.examples.find((example) => example.id === "registry-domains"); + expect(registryDomains?.defaultVariables?.registry).toMatchObject({ + chainId: expect.any(Number), + address: expect.any(String), + }); + expect(payload.examples.map((example) => example.id)).toContain("account-profile"); + }); + + it("reads an example resource by id", async () => { + const { client } = await connectClient(); + + const { contents } = await client.readResource({ uri: "omnigraph://examples/hello-world" }); + + expect(contents).toHaveLength(1); + if (!("text" in contents[0])) throw new Error("expected text resource"); + const payload = JSON.parse(contents[0].text) as { id: string; query: string }; + expect(payload.id).toBe("hello-world"); + expect(payload.query).toContain("primaryName(by: { chainName: ETHEREUM })"); + }); + + it("executes omnigraph_query via the omnigraph API handler and returns the raw GraphQL JSON", async () => { + const graphqlResponse = { data: { __typename: "Query" } }; + omnigraphApiFetchMock.mockResolvedValue( + new Response(JSON.stringify(graphqlResponse), { + headers: { "content-type": "application/json" }, + }), + ); + + const { client } = await connectClient(); + + const result = await client.callTool({ + name: "omnigraph_query", + arguments: { query: "{ __typename }" }, + }); + + expect(omnigraphApiFetchMock).toHaveBeenCalledTimes(1); + const [request] = omnigraphApiFetchMock.mock.calls[0]; + expect(request.method).toBe("POST"); + expect(new URL(request.url).pathname).toBe("/api/omnigraph"); + await expect(request.clone().json()).resolves.toEqual({ + query: "{ __typename }", + variables: null, + }); + + const content = result.content as Array<{ type: string; text: string }>; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + expect(JSON.parse(content[0].text)).toEqual(graphqlResponse); + }); + + it("rejects whitespace-only query or exampleId", async () => { + const { client } = await connectClient(); + + for (const arguments_ of [ + { query: " " }, + { exampleId: " " }, + { query: " ", exampleId: " " }, + ]) { + const result = await client.callTool({ name: "omnigraph_query", arguments: arguments_ }); + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toMatch(/Provide exactly one of `query` or `exampleId`/); + } + + expect(omnigraphApiFetchMock).not.toHaveBeenCalled(); + }); + + it("executes omnigraph_query by exampleId", async () => { + omnigraphApiFetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: { account: null } }), { + headers: { "content-type": "application/json" }, + }), + ); + + const { client } = await connectClient(); + + await client.callTool({ + name: "omnigraph_query", + arguments: { + exampleId: "account-migrated-names", + variables: { address: "0x0000000000000000000000000000000000000001" }, + }, + }); + + const [request] = omnigraphApiFetchMock.mock.calls[0]; + const body = (await request.clone().json()) as { + query: string; + variables: Record; + }; + expect(body.query).toContain("v1DomainsCount"); + expect(body.variables).toEqual({ address: "0x0000000000000000000000000000000000000001" }); + }); + + it("forwards GraphQL variables to the omnigraph API handler", async () => { + omnigraphApiFetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: null }), { + headers: { "content-type": "application/json" }, + }), + ); + + const { client } = await connectClient(); + + await client.callTool({ + name: "omnigraph_query", + arguments: { + query: "query($id: ID!) { node(id: $id) { __typename } }", + variables: { id: "0x1234" }, + }, + }); + + const [request] = omnigraphApiFetchMock.mock.calls[0]; + await expect(request.clone().json()).resolves.toMatchObject({ + variables: { id: "0x1234" }, + }); + }); + + it("returns omnigraph_schema lookup for Account", async () => { + const { client } = await connectClient(); + + const result = await client.callTool({ + name: "omnigraph_schema", + arguments: { type: "Account" }, + }); + + const content = result.content as Array<{ type: string; text: string }>; + const parsed = JSON.parse(content[0].text) as { name: string; fields: Array<{ name: string }> }; + expect(parsed.name).toBe("Account"); + expect(parsed.fields.map((field) => field.name)).toContain("resolve"); + }); + + it("appends validation hints for common GraphQL mistakes", async () => { + omnigraphApiFetchMock.mockResolvedValue( + new Response( + JSON.stringify({ + errors: [{ message: 'Unknown argument "id" on field "Query.account".' }], + }), + { headers: { "content-type": "application/json" } }, + ), + ); + + const { client } = await connectClient(); + + const result = await client.callTool({ + name: "omnigraph_query", + arguments: { query: '{ account(id: "0x1") { address } }' }, + }); + + const content = result.content as Array<{ type: string; text: string }>; + const parsed = JSON.parse(content[0].text) as { hints: string[] }; + expect(parsed.hints).toContain( + "Use account(by: { address: $address }) — not account(id: ...).", + ); + }); + + it("trims padded exampleId and query before resolving", async () => { + omnigraphApiFetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: { account: null } }), { + headers: { "content-type": "application/json" }, + }), + ); + + const { client } = await connectClient(); + + await client.callTool({ + name: "omnigraph_query", + arguments: { + exampleId: " account-profile ", + variables: { address: "0x0000000000000000000000000000000000000001" }, + }, + }); + + const [request] = omnigraphApiFetchMock.mock.calls[0]; + const body = (await request.clone().json()) as { query: string }; + expect(body.query).toContain("primaryName(by: { chainName: ETHEREUM })"); + + omnigraphApiFetchMock.mockClear(); + omnigraphApiFetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: { __typename: "Query" } }), { + headers: { "content-type": "application/json" }, + }), + ); + + await client.callTool({ + name: "omnigraph_query", + arguments: { query: " { __typename } " }, + }); + + const [queryRequest] = omnigraphApiFetchMock.mock.calls[0]; + await expect(queryRequest.clone().json()).resolves.toEqual({ + query: "{ __typename }", + variables: null, + }); + }); + + it("executes omnigraph_query by account-profile exampleId alias", async () => { + omnigraphApiFetchMock.mockResolvedValue( + new Response(JSON.stringify({ data: { account: null } }), { + headers: { "content-type": "application/json" }, + }), + ); + + const { client } = await connectClient(); + + await client.callTool({ + name: "omnigraph_query", + arguments: { + exampleId: "account-profile", + variables: { address: "0x0000000000000000000000000000000000000001" }, + }, + }); + + const [request] = omnigraphApiFetchMock.mock.calls[0]; + const body = (await request.clone().json()) as { query: string }; + expect(body.query).toContain("primaryName(by: { chainName: ETHEREUM })"); + expect(body.query).toContain("v1DomainsCount"); + }); + + it("appends validation hints for invalid ProfileSocials fields", async () => { + omnigraphApiFetchMock.mockResolvedValue( + new Response( + JSON.stringify({ + errors: [{ message: 'Cannot query field "discord" on type "ProfileSocials".' }], + }), + { headers: { "content-type": "application/json" } }, + ), + ); + + const { client } = await connectClient(); + + const result = await client.callTool({ + name: "omnigraph_query", + arguments: { + query: + '{ domain(by: { name: "vitalik.eth" }) { resolve { profile { socials { discord { handle } } } } } }', + }, + }); + + const content = result.content as Array<{ type: string; text: string }>; + const parsed = JSON.parse(content[0].text) as { hints: string[] }; + expect(parsed.hints.some((hint) => hint.includes("ProfileSocials"))).toBe(true); + }); + + it("appends validation hints for camelCase filter fields", async () => { + omnigraphApiFetchMock.mockResolvedValue( + new Response( + JSON.stringify({ + errors: [ + { + message: + 'Variable "$name" got invalid value { startsWith: "vit" }; Field "startsWith" is not defined by type "DomainsNameFilter".', + }, + ], + }), + { headers: { "content-type": "application/json" } }, + ), + ); + + const { client } = await connectClient(); + + const result = await client.callTool({ + name: "omnigraph_query", + arguments: { + query: + "query($name: DomainsNameFilter!) { domains(where: { name: $name }, first: 1) { totalCount } }", + variables: { name: { startsWith: "vit" } }, + }, + }); + + const content = result.content as Array<{ type: string; text: string }>; + const parsed = JSON.parse(content[0].text) as { hints: string[] }; + expect(parsed.hints.some((hint) => hint.includes("starts_with"))).toBe(true); + }); + + it("returns a JSON-RPC parse error for invalid JSON on initialize", async () => { + const response = await mcpApi.fetch( + new Request("http://ensapi.internal/", { + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json, text/event-stream", + }, + body: "{not-json", + }), + ); + + expect(response.status).toBe(400); + const payload = (await response.json()) as { + jsonrpc: string; + error: { code: number; message: string }; + id: null; + }; + expect(payload).toMatchObject({ + jsonrpc: "2.0", + error: { code: -32_700, message: "Parse error" }, + id: null, + }); + }); + + it("supports POST initialize followed by GET SSE for the same session", async () => { + const init = await mcpApi.fetch( + new Request("http://ensapi.internal/", { + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json, text/event-stream", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-03-26", + capabilities: {}, + clientInfo: { name: "test-client", version: "0.0.0" }, + }, + }), + }), + ); + + expect(init.status).toBe(200); + const sessionId = init.headers.get("mcp-session-id"); + expect(sessionId).toBeTruthy(); + activeHttpSessionIds.push(sessionId!); + + const sse = await mcpApi.fetch( + new Request("http://ensapi.internal/", { + method: "GET", + headers: { + accept: "text/event-stream", + "mcp-protocol-version": "2025-03-26", + "mcp-session-id": sessionId!, + }, + }), + ); + + expect(sse.status).toBe(200); + expect(sse.headers.get("content-type")).toContain("text/event-stream"); + }); +}); diff --git a/apps/ensapi/src/handlers/api/mcp/mcp-api.ts b/apps/ensapi/src/handlers/api/mcp/mcp-api.ts new file mode 100644 index 000000000..04856c3cd --- /dev/null +++ b/apps/ensapi/src/handlers/api/mcp/mcp-api.ts @@ -0,0 +1,335 @@ +import packageJson from "@/../package.json" with { type: "json" }; + +import { StreamableHTTPTransport } from "@hono/mcp"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { Hono } from "hono"; +import { z } from "zod/v4"; + +import { + buildCondensedSchemaReference, + lookupOmnigraphSchema, + makeOptionalTrimmedNonEmptyStringSchema, + OmnigraphSchemaLookupInputSchema, +} from "@ensnode/ensnode-sdk/internal"; + +import { makeLogger } from "@/lib/logger"; + +import { + buildOmnigraphExamplesIndex, + executeOmnigraphQuery, + listOmnigraphExampleIds, + buildOmnigraphMcpInstructions, + resolveOmnigraphExample, +} from "./omnigraph-mcp-support"; + +const logger = makeLogger("mcp-api"); + +const OmnigraphQueryInputSchema = z + .object({ + query: makeOptionalTrimmedNonEmptyStringSchema( + "GraphQL query document. Mutually exclusive with `exampleId`.", + ), + exampleId: makeOptionalTrimmedNonEmptyStringSchema( + "Run a vetted example query by id (see omnigraph://examples/index). Mutually exclusive with `query`.", + ), + variables: z + .record(z.string(), z.unknown()) + .optional() + .describe("GraphQL variables. With `exampleId`, overrides the example defaults."), + }) + .superRefine((value, ctx) => { + const hasQuery = value.query !== undefined; + const hasExample = value.exampleId !== undefined; + if (hasQuery === hasExample) { + ctx.addIssue({ + code: "custom", + message: "Provide exactly one of `query` or `exampleId`.", + }); + } + }); + +/** + * Builds the ENSNode Omnigraph MCP server and registers its tools. + * + * Exported so tests can drive the server over an in-memory transport without the HTTP layer. + */ +export function createOmnigraphMcpServer(): McpServer { + const server = new McpServer( + { + name: "ENS", + version: packageJson.version, + }, + { + instructions: buildOmnigraphMcpInstructions(), + }, + ); + + server.registerResource( + "omnigraph-schema-condensed", + "omnigraph://schema/condensed", + { + title: "Omnigraph condensed schema", + description: "Core Omnigraph entry points and types for query authoring.", + mimeType: "text/markdown", + }, + async (uri) => ({ + contents: [ + { uri: uri.href, mimeType: "text/markdown", text: buildCondensedSchemaReference() }, + ], + }), + ); + + server.registerResource( + "omnigraph-examples-index", + "omnigraph://examples/index", + { + title: "Omnigraph example query index", + description: + "Vetted example query ids; read omnigraph://examples/{exampleId} for the payload.", + mimeType: "application/json", + }, + async (uri) => ({ + contents: [ + { uri: uri.href, mimeType: "application/json", text: buildOmnigraphExamplesIndex() }, + ], + }), + ); + + server.registerResource( + "omnigraph-example", + new ResourceTemplate("omnigraph://examples/{exampleId}", { + list: undefined, + complete: { + exampleId: async () => listOmnigraphExampleIds(), + }, + }), + { + title: "Omnigraph example query", + description: "A vetted GraphQL query and namespace-aware default variables.", + mimeType: "application/json", + }, + async (uri, { exampleId }) => { + const id = String(exampleId); + const { query, variables } = resolveOmnigraphExample(id); + return { + contents: [ + { + uri: uri.href, + mimeType: "application/json", + text: JSON.stringify({ id, query, variables }, null, 2), + }, + ], + }; + }, + ); + + server.registerTool( + "omnigraph_schema", + { + title: "Look up ENS Omnigraph schema", + description: + "Discover Omnigraph types and fields from the bundled schema (no network). Omit arguments for " + + "root query fields and type names; pass `type` for a type or Type.field; pass `search` to find matches. " + + "Use type DomainsNameFilter or SubdomainsWhereInput before writing custom where filters.", + inputSchema: OmnigraphSchemaLookupInputSchema, + }, + async ({ type, search }) => { + const result = lookupOmnigraphSchema({ type, search }); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + }, + ); + + server.registerTool( + "omnigraph_query", + { + title: "Run an ENS Omnigraph GraphQL query", + description: + "Execute a read-only GraphQL query against this ENSNode instance's ENS Omnigraph API. Returns " + + "`{ data, errors }` JSON (with optional `hints` on common validation mistakes). Prefer `exampleId` " + + "from omnigraph://examples/index; use omnigraph_schema or omnigraph://schema/condensed before writing custom queries.", + inputSchema: OmnigraphQueryInputSchema, + }, + async ({ query, exampleId, variables }) => { + const resolved = exampleId + ? resolveOmnigraphExample(exampleId, variables) + : { query: query ?? "", variables }; + return { + content: [ + { type: "text", text: await executeOmnigraphQuery(resolved.query, resolved.variables) }, + ], + }; + }, + ); + + return server; +} + +type McpSession = { + server: McpServer; + transport: StreamableHTTPTransport; + lastActivityAt: number; +}; + +type McpRequestContext = Parameters[0]; + +/** Active MCP sessions keyed by `mcp-session-id` (one server + transport pair per client). */ +const sessions = new Map(); + +/** Cap stored sessions to limit memory growth from repeated initialize requests. */ +const MAX_MCP_SESSIONS = 200; + +/** Close sessions with no client activity for this long. */ +const MCP_SESSION_IDLE_TTL_MS = 30 * 60 * 1000; + +function jsonRpcErrorResponse( + c: { json: (data: unknown, status: number) => Response }, + code: number, + message: string, + status: number, +) { + return c.json({ jsonrpc: "2.0", error: { code, message }, id: null }, status); +} + +function touchSession(session: McpSession) { + session.lastActivityAt = Date.now(); +} + +function closeSessionFireAndForget(sessionId: string) { + void closeSession(sessionId).catch((err) => { + logger.error({ err, sessionId }, "MCP session cleanup failed"); + }); +} + +function evictIdleSessions(exceptSessionId?: string) { + const now = Date.now(); + for (const [id, session] of sessions) { + if (id !== exceptSessionId && now - session.lastActivityAt > MCP_SESSION_IDLE_TTL_MS) { + closeSessionFireAndForget(id); + } + } +} + +function storeSession(id: string, session: McpSession) { + evictIdleSessions(id); + sessions.set(id, session); + if (sessions.size <= MAX_MCP_SESSIONS) return; + + // Evict the least-recently-active session (LRU), not the oldest-created. + let lruId: string | undefined; + let lruAt = Number.POSITIVE_INFINITY; + for (const [candidateId, candidate] of sessions) { + if (candidateId === id) continue; + if (candidate.lastActivityAt < lruAt) { + lruAt = candidate.lastActivityAt; + lruId = candidateId; + } + } + if (lruId) closeSessionFireAndForget(lruId); +} + +function getSessionId(ctx: { + req: { header: (name: string) => string | undefined }; +}): string | undefined { + const sessionId = ctx.req.header("mcp-session-id"); + return sessionId && sessionId.length > 0 ? sessionId : undefined; +} + +function invalidSessionResponse(c: { json: (data: unknown, status: number) => Response }) { + return jsonRpcErrorResponse(c, -32_000, "Invalid or missing session ID", 400); +} + +async function closeSession(sessionId: string): Promise { + const session = sessions.get(sessionId); + if (!session) return; + + sessions.delete(sessionId); + await session.transport.close(); + await session.server.close(); +} + +const app = new Hono(); + +app.all("/", async (c) => { + const sessionId = getSessionId(c); + const mcpContext = c as unknown as McpRequestContext; + + if (c.req.method === "GET") { + const session = sessionId ? sessions.get(sessionId) : undefined; + if (!session) return invalidSessionResponse(c); + touchSession(session); + evictIdleSessions(sessionId); + const response = await session.transport.handleRequest(mcpContext); + return response ?? c.body(null, 202); + } + + if (c.req.method === "DELETE") { + if (!sessionId) return invalidSessionResponse(c); + const session = sessions.get(sessionId); + if (!session) return invalidSessionResponse(c); + + const response = await session.transport.handleRequest(mcpContext); + await closeSession(sessionId); + return response ?? c.body(null, 202); + } + + if (c.req.method === "POST") { + if (sessionId) { + const session = sessions.get(sessionId); + if (!session) { + return jsonRpcErrorResponse(c, -32_000, "Session not found", 404); + } + touchSession(session); + evictIdleSessions(sessionId); + const response = await session.transport.handleRequest(mcpContext); + return response ?? c.body(null, 202); + } + + let body: unknown; + try { + body = await c.req.json(); + } catch { + return jsonRpcErrorResponse(c, -32_700, "Parse error", 400); + } + if (!isInitializeRequest(body)) { + return jsonRpcErrorResponse(c, -32_000, "Bad Request: No valid session ID provided", 400); + } + + const server = createOmnigraphMcpServer(); + let initializedSessionId: string | undefined; + const session: McpSession = { + server, + lastActivityAt: Date.now(), + transport: new StreamableHTTPTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + onsessioninitialized: (id) => { + initializedSessionId = id; + storeSession(id, session); + }, + onsessionclosed: (id) => { + closeSessionFireAndForget(id); + }, + }), + }; + + try { + await server.connect(session.transport); + const response = await session.transport.handleRequest(mcpContext, body); + return response ?? c.body(null, 202); + } catch { + if (initializedSessionId) { + await closeSession(initializedSessionId); + } else { + await session.transport.close(); + await session.server.close(); + } + return jsonRpcErrorResponse(c, -32_603, "Internal error", 500); + } + } + + return jsonRpcErrorResponse(c, -32_000, "Method not allowed.", 405); +}); + +export default app; diff --git a/apps/ensapi/src/handlers/api/mcp/omnigraph-mcp-support.test.ts b/apps/ensapi/src/handlers/api/mcp/omnigraph-mcp-support.test.ts new file mode 100644 index 000000000..c3a229fc6 --- /dev/null +++ b/apps/ensapi/src/handlers/api/mcp/omnigraph-mcp-support.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { buildOmnigraphMcpInstructions } from "./omnigraph-mcp-support"; + +describe("omnigraph MCP support", () => { + it("requires exampleId before custom GraphQL and inlines the catalog", () => { + const instructions = buildOmnigraphMcpInstructions(); + + expect(instructions).toContain( + "Before ANY query: call omnigraph_query with exampleId from the list below. Only write custom GraphQL if no example fits.", + ); + expect(instructions).toContain("Vetted exampleId values:"); + expect(instructions).toContain("- account-profile —"); + expect(instructions).toContain("- domain-profile —"); + expect(instructions).toContain("- account-migrated-names —"); + expect(instructions).toContain("account(id:"); + expect(instructions).toContain("starts_with"); + }); +}); diff --git a/apps/ensapi/src/handlers/api/mcp/omnigraph-mcp-support.ts b/apps/ensapi/src/handlers/api/mcp/omnigraph-mcp-support.ts new file mode 100644 index 000000000..75dacab7a --- /dev/null +++ b/apps/ensapi/src/handlers/api/mcp/omnigraph-mcp-support.ts @@ -0,0 +1,176 @@ +import { + getGraphqlApiExampleQueryById, + listGraphqlApiExampleQueryIds, + resolveGraphqlApiExampleQuery, +} from "@ensnode/ensnode-sdk/internal"; + +import di from "@/di"; + +/** MCP resource URI for a given example query id. */ +export function omnigraphExampleUri(exampleId: string): string { + return `omnigraph://examples/${exampleId}`; +} + +const VALIDATION_HINTS: Array<{ match: RegExp; hint: string }> = [ + { + match: /Unknown argument "id" on field "Query\.account"/, + hint: "Use account(by: { address: $address }) — not account(id: ...).", + }, + { + match: /Cannot query field "primaryName" on type "Account"/, + hint: "Primary names live under account.resolve.primaryName(by: { chainName: ETHEREUM }).", + }, + { + match: /Cannot query field "items"/, + hint: "Relay connections use edges { node }, pageInfo, and totalCount — not items.", + }, + { + match: /Field "account" argument "by" .* is required/, + hint: "Pass account(by: { address: $address }) or account(by: { id: $address }).", + }, + { + match: /Cannot query field "[^"]+" on type "ProfileSocials"/, + hint: + "ProfileSocials fields: github, keybase, linkedin, telegram, twitter only. " + + "Call omnigraph_schema({ type: 'ProfileSocials' }) or use exampleId domain-profile.", + }, + { + match: /Field "startsWith" is not defined by type "DomainsNameFilter"/, + hint: + "Omnigraph filter inputs use snake_case: starts_with, not startsWith. " + + "Call omnigraph_schema({ type: 'DomainsNameFilter' }).", + }, + { + match: /Cannot query field "startsWith"/, + hint: + "Omnigraph filter inputs use snake_case: starts_with, not startsWith. " + + "Call omnigraph_schema({ type: 'DomainsNameFilter' }).", + }, + { + match: + /Field "[^"]*[A-Z][^"]*" is not defined by type "(DomainsNameFilter|SubdomainsWhereInput)"/, + hint: + "Omnigraph filter inputs use snake_case field names (e.g. starts_with). " + + "Call omnigraph_schema({ type: 'DomainsNameFilter' }) before custom where filters.", + }, +]; + +function getMcpNamespace() { + try { + return di.context.namespace; + } catch { + return undefined; + } +} + +export function listOmnigraphExampleIds(): string[] { + return listGraphqlApiExampleQueryIds(); +} + +export function buildOmnigraphExamplesIndex(): string { + const namespace = getMcpNamespace(); + return JSON.stringify( + { + examples: listGraphqlApiExampleQueryIds().map((id) => { + const { title, description } = getGraphqlApiExampleQueryById(id); + const { variables: defaultVariables } = resolveGraphqlApiExampleQuery(id, { namespace }); + return { + id, + uri: omnigraphExampleUri(id), + title, + description, + defaultVariables, + }; + }), + }, + null, + 2, + ); +} + +export function resolveOmnigraphExample( + exampleId: string, + variablesOverride?: Record, +): { query: string; variables: Record } { + return resolveGraphqlApiExampleQuery(exampleId, { + namespace: getMcpNamespace(), + variables: variablesOverride, + }); +} + +function appendValidationHints(responseText: string): string { + let parsed: { data?: unknown; errors?: Array<{ message: string }> }; + try { + parsed = JSON.parse(responseText) as { data?: unknown; errors?: Array<{ message: string }> }; + } catch { + return responseText; + } + + if (!parsed.errors?.length) return responseText; + + const hints = [ + ...new Set( + parsed.errors.flatMap((error) => + VALIDATION_HINTS.filter(({ match }) => match.test(error.message)).map(({ hint }) => hint), + ), + ), + ]; + if (hints.length === 0) return responseText; + + return JSON.stringify({ ...parsed, hints }, null, 2); +} + +/** Executes a read-only Omnigraph query via the /api/omnigraph handler (middleware + Yoga). */ +export async function executeOmnigraphQuery( + query: string, + variables?: Record, +): Promise { + const omnigraphApi = await import("@/handlers/api/omnigraph/omnigraph-api"); + const response = await omnigraphApi.default.fetch( + new Request("http://ensapi.internal/api/omnigraph", { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify({ query, variables: variables ?? null }), + }), + ); + return appendValidationHints(await response.text()); +} + +/** One-line catalog of vetted exampleId values for MCP server instructions. */ +function buildInlinedExamplesCatalog(): string { + return listGraphqlApiExampleQueryIds() + .map((id) => { + const { description } = getGraphqlApiExampleQueryById(id); + return `- ${id} — ${description}`; + }) + .join("\n"); +} + +/** MCP server instructions sent to clients at initialize (system prompt). */ +export function buildOmnigraphMcpInstructions(): string { + return [ + "ENS Omnigraph MCP — read-only GraphQL over indexed ENSv1 + ENSv2 state.", + "", + "Before ANY query: call omnigraph_query with exampleId from the list below. Only write custom GraphQL if no example fits.", + "", + "Vetted exampleId values:", + buildInlinedExamplesCatalog(), + "", + "When no example fits, discover the schema before writing custom GraphQL:", + "- Read omnigraph://schema/condensed or call omnigraph_schema.", + "- Use omnigraph_schema({ type: 'DomainsNameFilter' }) before custom where filters.", + "", + "Entry points:", + "- account(by: { address }) — address-owned domains, permissions, reverse resolution", + "- domain(by: { name }) — a single name", + "- domains(where: { … }, first: N) — search canonical domains", + "", + "Anti-patterns (will fail validation):", + "- account(id: …), Account.primaryName, connection.items", + "- Filter fields use snake_case: starts_with, not startsWith", + "- ProfileSocials: github, keybase, linkedin, telegram, twitter only — do not guess (no discord/instagram)", + "- Pagination: edges { node }, pageInfo { hasNextPage endCursor }, totalCount — not items", + "", + "Interactive schema browser: /api/omnigraph (GraphiQL).", + ].join("\n"); +} diff --git a/apps/ensapi/src/handlers/api/router.ts b/apps/ensapi/src/handlers/api/router.ts index 1b15e11f1..ac796be8e 100644 --- a/apps/ensapi/src/handlers/api/router.ts +++ b/apps/ensapi/src/handlers/api/router.ts @@ -2,6 +2,7 @@ import { createApp } from "@/lib/hono-factory"; import nameTokensApi from "./explore/name-tokens-api"; import registrarActionsApi from "./explore/registrar-actions-api"; +import mcpApi from "./mcp/mcp-api"; import realtimeApi from "./meta/realtime-api"; import statusApi from "./meta/status-api"; import omnigraphApi from "./omnigraph/omnigraph-api"; @@ -15,5 +16,6 @@ app.route("/resolve", resolutionApi); app.route("/name-tokens", nameTokensApi); app.route("/registrar-actions", registrarActionsApi); app.route("/omnigraph", omnigraphApi); +app.route("/mcp", mcpApi); export default app; diff --git a/apps/ensapi/src/openapi-document.ts b/apps/ensapi/src/openapi-document.ts index 2e7053f14..fe1618033 100644 --- a/apps/ensapi/src/openapi-document.ts +++ b/apps/ensapi/src/openapi-document.ts @@ -6,6 +6,8 @@ import { openapiMeta } from "@/openapi-meta"; * Deprecated endpoints to exclude from the generated OpenAPI document. */ const HIDE_OPENAPI_ENDPOINTS: string[] = [ + // MCP (Model Context Protocol) over streamable HTTP — not a REST/OpenAPI surface. + "/api/mcp", // TODO: remove /amirealtime once the legacy endpoint is deleted. "/amirealtime", // TODO: remove all other endpoints from this list once the legacy endpoints are deleted. diff --git a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts index 9d084e8e0..55755ff8d 100644 --- a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts +++ b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts @@ -167,6 +167,10 @@ export const integrateSidebarTopic = { label: "ENS Omnigraph (GraphQL)", link: "/docs/integrate/integration-options/omnigraph-graphql-api", }, + { + label: "ENS Omnigraph MCP (AI agents)", + link: "/docs/integrate/integration-options/omnigraph-mcp", + }, { label: "ENSDb (SQL)", link: "/docs/integrate/integration-options/ensdb", diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/ai-llm.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/ai-llm.mdx index 89d36c78c..e066af855 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/ai-llm.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/ai-llm.mdx @@ -61,6 +61,12 @@ and how many other domains do they own? href="/docs/integrate/integration-options/enscli" /> + + + +## Tools and resources + +The server exposes two read-only tools: + +- **`omnigraph_query`** — accepts a GraphQL `query` or vetted `exampleId` (and optional `variables`) and returns the raw `{ data, errors }` JSON, exactly as the [HTTP endpoint](/docs/integrate/integration-options/omnigraph-graphql-api#1-the-endpoint) does. Because it's the full Omnigraph behind one tool, an agent can answer any question the Omnigraph can — resolve records, search Domains, list a user's Domains, and much more — without a fixed, hand-written tool per use case. +- **`omnigraph_schema`** — looks up Omnigraph types and fields from the bundled schema (no network). Use it before writing custom queries, especially for filter inputs like `DomainsNameFilter`. + +It also ships **resources**: condensed schema at `omnigraph://schema/condensed`, example query index at `omnigraph://examples/index`, and per-example payloads at `omnigraph://examples/{exampleId}`. Pick a vetted `exampleId` from the index when you want a starting point (e.g. `account-profile`, `domain-profile`) — the agent chooses the right one for your question. + +## Cursor + +Cursor supports streamable-HTTP MCP servers natively via a `url` in `mcp.json`. + +**Project-scoped** (recommended when working in one repo) — create `.cursor/mcp.json` in the project root: + +```json title=".cursor/mcp.json" +{ + "mcpServers": { + "ENS": { + "url": "https://api.v2-sepolia.ensnode.io/api/mcp" + } + } +} +``` + +**User-scoped** (all projects) — use `~/.cursor/mcp.json` with the same shape. + +For a local ENSApi instance: + +```json title=".cursor/mcp.json" +{ + "mcpServers": { + "ENS": { + "url": "http://localhost:4334/api/mcp" + } + } +} +``` + +Then: + +1. Open **Cursor Settings → Tools & MCP** (or **Features → MCP**). +2. Confirm **ENS** is listed with a green status and the `omnigraph_query` and `omnigraph_schema` tools. +3. In chat, ask ENS questions in plain language — e.g. _"Who owns vitalik.eth?"_ or _"List domains owned by 0x…"_ — and let the agent call `omnigraph_query`. Use the server key `"ENS"` in your config so the MCP entry is easy to spot; the agent picks the right `exampleId` or writes a custom query for your question. + + + +## Claude Desktop + +Claude Desktop configures MCP servers in a JSON file (restart Claude after editing): + +| OS | Config path | +| ------- | ----------------------------------------------------------------- | +| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| Windows | `%APPDATA%\Claude\claude_desktop_config.json` | + +Many Claude Desktop builds expect **stdio** MCP servers rather than a remote URL. Bridge to ENSNode's streamable-HTTP endpoint with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote): + +```json title="claude_desktop_config.json" +{ + "mcpServers": { + "ENS": { + "command": "npx", + "args": [ + "-y", + "mcp-remote", + "https://api.v2-sepolia.ensnode.io/api/mcp" + ] + } + } +} +``` + +For local development, swap the URL for `http://localhost:4334/api/mcp`. + +After restart, start a new conversation and ask ENS questions — Claude should invoke `omnigraph_query` when it needs indexed ENS data. + + + +## Verify the connection + +To smoke-test without an editor, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): + +```bash +npx @modelcontextprotocol/inspector +``` + +Set **Transport** to **Streamable HTTP**, **URL** to your `/api/mcp` endpoint, connect, then **List Tools** — you should see `omnigraph_query` and `omnigraph_schema`. Call `omnigraph_query` with: + +```json +{ + "query": "{ __typename }" +} +``` + +## Authoring queries + +The MCP endpoint runs whatever GraphQL you hand it — it does not author queries for you. Use `omnigraph_schema` or read `omnigraph://schema/condensed` to discover the schema, then write queries (or pick a vetted `exampleId` from `omnigraph://examples/index`). You can also: + +- Explore interactively in the **GraphiQL playground** at `/api/omnigraph`. +- Browse the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) and [Omnigraph examples](/docs/integrate/omnigraph/examples). +- Give your agent the [`ensskills`](/docs/integrate/integration-options/ensskills) **`omnigraph`** skill, which teaches the unified ENSv1 + ENSv2 datamodel, resolution, pagination, and vetted example queries — so it writes queries that work the first time. + + + +## Where to go next + +- Want to call the same API over plain HTTP, `curl`, or your own GraphQL client? See the [ENS Omnigraph API (GraphQL)](/docs/integrate/integration-options/omnigraph-graphql-api) guide. +- Building integration code rather than driving an agent? Use [`enssdk`](/docs/integrate/integration-options/enssdk) (typed client) or [`enskit`](/docs/integrate/integration-options/enskit) (React). + + + + + + diff --git a/docs/ensnode.io/src/data/omnigraph-examples/config.test.ts b/docs/ensnode.io/src/data/omnigraph-examples/config.test.ts index a80a34f90..ae4590e5b 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/config.test.ts +++ b/docs/ensnode.io/src/data/omnigraph-examples/config.test.ts @@ -3,6 +3,8 @@ import { describe, expect, it } from "vitest"; import { DOCS_OMNIGRAPH_NAMESPACE_CONFIG } from "@lib/examples/omnigraph/constants"; +import { getGraphqlApiExampleQueryById } from "@ensnode/ensnode-sdk/internal"; + import { getOmnigraphExamplePageHref, OMNIGRAPH_EXAMPLES_CONFIG, @@ -56,7 +58,7 @@ describe("OMNIGRAPH_EXAMPLES_CONFIG", () => { }); expect(OMNIGRAPH_EXAMPLES_SIDEBAR_ITEMS.slice(1)).toEqual( pageConfigs.map((config) => ({ - label: config.title, + label: getGraphqlApiExampleQueryById(config.id).title, link: getOmnigraphExamplePageHref(config), })), ); diff --git a/docs/ensnode.io/src/data/omnigraph-examples/config.ts b/docs/ensnode.io/src/data/omnigraph-examples/config.ts index 11fea63f7..37480adc0 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/config.ts +++ b/docs/ensnode.io/src/data/omnigraph-examples/config.ts @@ -1,10 +1,9 @@ import type { DocsOmnigraphExampleNamespace } from "@lib/examples/omnigraph/constants"; import { ENSNamespaceIds } from "@ensnode/ensnode-sdk"; +import { getGraphqlApiExampleQueryById } from "@ensnode/ensnode-sdk/internal"; export type OmnigraphExampleConfig = { id: string; - title: string; - description: string; category: string; namespace: DocsOmnigraphExampleNamespace; hostSeparatePage: boolean; @@ -14,164 +13,120 @@ export type OmnigraphExampleConfig = { export const OMNIGRAPH_EXAMPLES_CONFIG: OmnigraphExampleConfig[] = [ { id: "hello-world", - title: "Hello World", - description: - "From a wallet address: Ethereum primary name and interpreted profile, plus ENSv1 and ENSv2 ownership counts.", category: "Introduction", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: false, }, { id: "domain-profile", - title: "Domain Profile", - description: "Load a domain's high-level profile (avatar, socials, addresses, and more).", category: "Resolution", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "domain-records", - title: "Domain Records", - description: "For given name resolve raw records like `addresses`, `texts`, `contenthash` etc.", category: "Resolution", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "domain-by-name", - title: "Domain By Name", - description: "Load a domain by interpreted name, including profile information.", category: "Resolution", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "find-domains", - title: "Find Domains", - description: "List domains matching a name prefix with ordering and registration metadata.", category: "Search", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "domain-subdomains", - title: "Domain Subdomains", - description: "Paginate direct child names under a parent domain.", category: "Resolution", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "domain-subdomains-recently-registered", - title: "Recently Registered Subdomains", - description: "List a parent domain's subdomains ordered by most recent registration first.", category: "Resolution", namespace: ENSNamespaceIds.SepoliaV2, hostSeparatePage: true, }, { id: "domain-events", - title: "Domain Events", - description: "Raw contract events associated with a domain's registry records.", category: "History", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "domains-by-address", - title: "Account Domains", - description: "Load domains owned by an address via the Omnigraph `account` root field.", category: "Accounts", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "account-primary-name", - title: "Account Primary Name", - description: "Load a primary name for an account on Ethereum, including profile information.", category: "Accounts", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "account-events", - title: "Account Events", - description: "Events touching an account across indexed ENS contracts.", category: "History", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "registry-domains", - title: "Registry Domains", - description: "Enumerate domains under a specific v2 ETH registry contract.", category: "Registry", namespace: ENSNamespaceIds.SepoliaV2, hostSeparatePage: true, }, { id: "permissions-by-contract", - title: "Permissions By Contract", - description: "Roles and users granted on resources for a registrar or registry contract.", category: "Permissions", namespace: ENSNamespaceIds.SepoliaV2, hostSeparatePage: true, }, { id: "permissions-by-user", - title: "Permissions By User", - description: "Resources and roles for an address in the permissions graph.", category: "Permissions", namespace: ENSNamespaceIds.SepoliaV2, hostSeparatePage: true, }, { id: "account-resolver-permissions", - title: "Account Resolver Permissions", - description: "Resolver contracts where an account has been granted resolver ACLs.", category: "Permissions", namespace: ENSNamespaceIds.SepoliaV2, hostSeparatePage: true, }, { id: "domain-resolver", - title: "Domain Resolver", - description: "Assigned resolver contract address and recent resolver events.", category: "Resolution", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "namegraph", - title: "Namegraph", - description: - "Walk a domain's registry, parent, subregistry, and direct subdomains (as in Core Concepts).", category: "Exploration", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: true, }, { id: "account-migrated-names", - title: "Account Migration Counts", - description: "Count an account's ENSv1 vs ENSv2 domains to gauge its migration progress.", category: "Migration", namespace: ENSNamespaceIds.SepoliaV2, hostSeparatePage: true, }, { id: "eth-by-version", - title: "ETH TLD By Version", - description: - "Load the .eth TLD across protocol versions: one Domain per version, discriminated by `__typename` (ENSv1Domain / ENSv2Domain).", category: "Migration", namespace: ENSNamespaceIds.SepoliaV2, hostSeparatePage: true, }, { id: "accelerate-resolve", - title: "Resolve primary name and records, and track protocol acceleration", - description: - "Resolve primary name and records, and track protocol acceleration with `trace` and `accelerate` arguments.", category: "Resolution", namespace: ENSNamespaceIds.Mainnet, hostSeparatePage: false, @@ -199,7 +154,7 @@ export function getOmnigraphExampleConfigById(id: string): OmnigraphExampleConfi export const OMNIGRAPH_EXAMPLES_SIDEBAR_ITEMS: { label: string; link: string }[] = [ { label: "Overview", link: OMNIGRAPH_EXAMPLES_INDEX_PATH }, ...OMNIGRAPH_EXAMPLES_CONFIG.filter((config) => config.hostSeparatePage).map((config) => ({ - label: config.title, + label: getGraphqlApiExampleQueryById(config.id).title, link: getOmnigraphExamplePageHref(config)!, })), ]; diff --git a/docs/ensnode.io/src/data/omnigraph-examples/examples.ts b/docs/ensnode.io/src/data/omnigraph-examples/examples.ts index 38046e13f..cdf47edd4 100644 --- a/docs/ensnode.io/src/data/omnigraph-examples/examples.ts +++ b/docs/ensnode.io/src/data/omnigraph-examples/examples.ts @@ -5,6 +5,7 @@ import { } from "@lib/examples/omnigraph/example-query"; import { getOmnigraphExamplePageHref, OMNIGRAPH_EXAMPLES_CONFIG } from "./config"; +import { getGraphqlApiExampleQueryById } from "@ensnode/ensnode-sdk/internal"; import exampleSnapshots from "./examples.json"; import responses from "./responses.json"; import type { SnapshotExample } from "./types"; @@ -33,8 +34,8 @@ export const omnigraphExamples: OmnigraphExampleQuery[] = OMNIGRAPH_EXAMPLES_CON const href = getOmnigraphExamplePageHref(config); return OmnigraphExampleQuerySchema.parse({ id: config.id, - title: config.title, - description: config.description, + title: getGraphqlApiExampleQueryById(config.id).title, + description: getGraphqlApiExampleQueryById(config.id).description, category: config.category, namespace: config.namespace, query: example.query.trim(), diff --git a/packages/enscli/src/commands/ensnode/omnigraph-schema.ts b/packages/enscli/src/commands/ensnode/omnigraph-schema.ts index cb4dd4f7d..099c53b48 100644 --- a/packages/enscli/src/commands/ensnode/omnigraph-schema.ts +++ b/packages/enscli/src/commands/ensnode/omnigraph-schema.ts @@ -1,131 +1,12 @@ -import schemaSDL from "enssdk/omnigraph/schema.graphql"; import { - buildSchema, - type GraphQLArgument, - type GraphQLField, - type GraphQLInputField, - type GraphQLNamedType, - type GraphQLSchema, - isEnumType, - isInputObjectType, - isInterfaceType, - isObjectType, - isUnionType, -} from "graphql"; + getOmnigraphSchema, + lookupOmnigraphSchema, + type OmnigraphSchemaFieldInfo, +} from "@ensnode/ensnode-sdk/internal"; import { printResult } from "../../lib/output"; -/** Builds the Omnigraph schema from the SDL bundled with enssdk (no network, always matches the SDK). */ -function loadSchema(): GraphQLSchema { - return buildSchema(schemaSDL); -} - -interface ArgInfo { - name: string; - type: string; - description: string | null; -} - -interface FieldInfo { - name: string; - type: string; - description: string | null; - args?: ArgInfo[]; -} - -function argInfo(arg: GraphQLArgument): ArgInfo { - return { name: arg.name, type: arg.type.toString(), description: arg.description ?? null }; -} - -function fieldInfo(field: GraphQLField | GraphQLInputField): FieldInfo { - const args = "args" in field && field.args.length > 0 ? field.args.map(argInfo) : undefined; - return { - name: field.name, - type: field.type.toString(), - description: field.description ?? null, - ...(args ? { args } : {}), - }; -} - -/** Field/value listing for a single named type, abstracting over object/interface/input/enum/union. */ -function describeType(type: GraphQLNamedType) { - const base = { name: type.name, description: type.description ?? null }; - if (isObjectType(type) || isInterfaceType(type)) { - return { - ...base, - kind: isObjectType(type) ? "object" : "interface", - fields: Object.values(type.getFields()).map(fieldInfo), - }; - } - if (isInputObjectType(type)) { - return { ...base, kind: "input", fields: Object.values(type.getFields()).map(fieldInfo) }; - } - if (isEnumType(type)) { - return { - ...base, - kind: "enum", - values: type.getValues().map((value) => ({ - name: value.name, - description: value.description ?? null, - })), - }; - } - if (isUnionType(type)) { - return { ...base, kind: "union", types: type.getTypes().map((member) => member.name) }; - } - return { ...base, kind: "scalar" }; -} - -/** Root listing: query entrypoints plus the major (non-connection) types, abstracting Relay plumbing. */ -function describeRoot(schema: GraphQLSchema) { - const queryType = schema.getQueryType(); - const queryFields = queryType ? Object.values(queryType.getFields()).map(fieldInfo) : []; - const types = Object.values(schema.getTypeMap()) - .filter((type) => isObjectType(type) && !type.name.startsWith("__")) - .map((type) => type.name) - .filter( - (name) => - name !== queryType?.name && - !name.endsWith("Connection") && - !name.endsWith("ConnectionEdge") && - !name.endsWith("Edge") && - !name.endsWith("Payload"), - ) - .sort(); - return { query: queryFields, types }; -} - -function describeFieldPath(schema: GraphQLSchema, typeName: string, fieldName: string) { - const type = schema.getType(typeName); - if (!type || !(isObjectType(type) || isInterfaceType(type) || isInputObjectType(type))) { - throw new Error( - `Type "${typeName}" has no fields. Run "enscli ensnode omnigraph schema" to list types.`, - ); - } - const field = type.getFields()[fieldName]; - if (!field) { - throw new Error(`Type "${typeName}" has no field "${fieldName}".`); - } - return { parent: typeName, ...fieldInfo(field) }; -} - -function searchSchema(schema: GraphQLSchema, keyword: string) { - const query = keyword.toLowerCase(); - const types: string[] = []; - const fields: string[] = []; - for (const type of Object.values(schema.getTypeMap())) { - if (type.name.startsWith("__")) continue; - if (type.name.toLowerCase().includes(query)) types.push(type.name); - if (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type)) { - for (const field of Object.values(type.getFields())) { - if (field.name.toLowerCase().includes(query)) fields.push(`${type.name}.${field.name}`); - } - } - } - return { types: types.sort(), fields: fields.sort() }; -} - -function renderField(field: FieldInfo, indent: string): string { +function renderField(field: OmnigraphSchemaFieldInfo, indent: string): string { const args = field.args ? `(${field.args.map((a) => `${a.name}: ${a.type}`).join(", ")})` : ""; const description = field.description ? `\n${indent} # ${field.description.replace(/\s+/g, " ").trim()}` @@ -133,18 +14,25 @@ function renderField(field: FieldInfo, indent: string): string { return `${indent}${field.name}${args}: ${field.type}${description}`; } -function renderTypePretty(type: ReturnType): string { +function renderTypePretty(type: { + description: string | null; + kind: string; + name: string; + fields?: OmnigraphSchemaFieldInfo[]; + values?: Array<{ name: string }>; + types?: string[]; +}): string { const lines: string[] = []; if (type.description) lines.push(`# ${type.description.replace(/\s+/g, " ").trim()}`); lines.push(`${type.kind} ${type.name} {`); - if ("fields" in type) for (const field of type.fields) lines.push(renderField(field, " ")); - else if ("values" in type) for (const value of type.values) lines.push(` ${value.name}`); - else if ("types" in type) lines.push(` ${type.types.join(" | ")}`); + if (type.fields) for (const field of type.fields) lines.push(renderField(field, " ")); + else if (type.values) for (const value of type.values) lines.push(` ${value.name}`); + else if (type.types) lines.push(` ${type.types.join(" | ")}`); lines.push("}"); return lines.join("\n"); } -function renderRootPretty(root: ReturnType): string { +function renderRootPretty(root: { query: OmnigraphSchemaFieldInfo[]; types: string[] }): string { return [ "# Root query fields", ...root.query.map((field) => renderField(field, " ")), @@ -154,11 +42,11 @@ function renderRootPretty(root: ReturnType): string { ].join("\n"); } -function renderFieldPathPretty(field: ReturnType): string { +function renderFieldPathPretty(field: OmnigraphSchemaFieldInfo & { parent: string }): string { return `${field.parent}.${renderField(field, "")}`; } -function renderSearchPretty(result: ReturnType): string { +function renderSearchPretty(result: { types: string[]; fields: string[] }): string { return [ "# Matching types", ...result.types.map((name) => ` ${name}`), @@ -176,39 +64,49 @@ export function runOmnigraphSchema( args: Record, target: string | undefined, ): void { - const omnigraphSchema = loadSchema(); - if (typeof args.search === "string") { - printResult(searchSchema(omnigraphSchema, args.search), args, renderSearchPretty); + const result = lookupOmnigraphSchema({ search: args.search }) as { + types: string[]; + fields: string[]; + }; + printResult(result, args, renderSearchPretty); return; } if (!target) { - printResult(describeRoot(omnigraphSchema), args, renderRootPretty); + const result = lookupOmnigraphSchema({}) as { + query: OmnigraphSchemaFieldInfo[]; + types: string[]; + }; + printResult(result, args, renderRootPretty); return; } if (target.includes(".")) { - const segments = target.split("."); - if (segments.length !== 2 || segments.some((segment) => segment.length === 0)) { - throw new Error( - `Invalid target "${target}". Expected "Type" or "Type.field" (e.g. Domain or Domain.canonical).`, - ); - } - const [typeName, fieldName] = segments; printResult( - describeFieldPath(omnigraphSchema, typeName, fieldName), + lookupOmnigraphSchema({ type: target }) as OmnigraphSchemaFieldInfo & { parent: string }, args, renderFieldPathPretty, ); return; } - const type = omnigraphSchema.getType(target); + const type = getOmnigraphSchema().getType(target); if (!type) { throw new Error( `Unknown type "${target}". Run "enscli ensnode omnigraph schema" to list types, or use --search.`, ); } - printResult(describeType(type), args, renderTypePretty); + printResult( + lookupOmnigraphSchema({ type: target }) as { + description: string | null; + kind: string; + name: string; + fields?: OmnigraphSchemaFieldInfo[]; + values?: Array<{ name: string }>; + types?: string[]; + }, + args, + renderTypePretty, + ); } diff --git a/packages/ensnode-sdk/package.json b/packages/ensnode-sdk/package.json index 98ff74b15..42d3d8e65 100644 --- a/packages/ensnode-sdk/package.json +++ b/packages/ensnode-sdk/package.json @@ -55,7 +55,8 @@ "lint": "biome check --write .", "lint:ci": "biome ci", "test": "vitest", - "typecheck": "tsgo --noEmit" + "typecheck": "tsgo --noEmit", + "generate:gqlschema": "node scripts/inline-schema-sdl.mjs" }, "peerDependencies": { "viem": "catalog:" @@ -75,6 +76,7 @@ "caip": "catalog:", "date-fns": "catalog:", "enssdk": "workspace:*", + "graphql": "^16.11.0", "zod": "catalog:" } } diff --git a/packages/ensnode-sdk/scripts/inline-schema-sdl.mjs b/packages/ensnode-sdk/scripts/inline-schema-sdl.mjs new file mode 100644 index 000000000..0d61c331d --- /dev/null +++ b/packages/ensnode-sdk/scripts/inline-schema-sdl.mjs @@ -0,0 +1,15 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const SCHEMA_PATH = resolve(SCRIPT_DIR, "../../enssdk/src/omnigraph/generated/schema.graphql"); +const OUT_PATH = resolve(SCRIPT_DIR, "../src/omnigraph-api/generated/schema-sdl.ts"); + +const sdl = readFileSync(SCHEMA_PATH, "utf8"); +const content = `/** Auto-generated from enssdk schema.graphql — run \`pnpm generate:gqlschema\` to refresh. */ +export const omnigraphSchemaSdl = ${JSON.stringify(sdl)}; +`; + +mkdirSync(dirname(OUT_PATH), { recursive: true }); +writeFileSync(OUT_PATH, content); diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 7d553054a..455b68f9a 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -26,6 +26,7 @@ export * from "./ensnode/api/shared/errors/examples"; export * from "./ensnode/api/shared/errors/zod-schemas"; export * from "./ensnode/api/shared/pagination/zod-schemas"; export * from "./omnigraph-api/example-queries"; +export * from "./omnigraph-api/schema-reference"; export * from "./registrars/zod-schemas"; export * from "./rpc"; export * from "./shared/config/build-rpc-urls"; diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.test.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.test.ts new file mode 100644 index 000000000..f73b2b72a --- /dev/null +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { listGraphqlApiExampleQueryIds, resolveGraphqlApiExampleQuery } from "./example-queries"; + +describe("resolveGraphqlApiExampleQuery", () => { + it("uses example defaults when variables are omitted", () => { + const { variables } = resolveGraphqlApiExampleQuery("registry-domains"); + expect(variables.registry).toBeDefined(); + expect(variables.registry).toMatchObject({ + chainId: expect.any(Number), + address: expect.any(String), + }); + }); + + it("replaces example defaults when variables are provided", () => { + const { variables } = resolveGraphqlApiExampleQuery("account-migrated-names", { + variables: { address: "0x0000000000000000000000000000000000000001" }, + }); + expect(variables).toEqual({ address: "0x0000000000000000000000000000000000000001" }); + }); + + it("resolves account-profile alias to hello-world defaults", () => { + const { query, variables } = resolveGraphqlApiExampleQuery("account-profile"); + expect(query).toContain("primaryName(by: { chainName: ETHEREUM })"); + expect(variables.address).toMatch(/^0x[a-f0-9]{40}$/); + }); +}); + +describe("listGraphqlApiExampleQueryIds", () => { + it("includes alias ids alongside canonical ids", () => { + const ids = listGraphqlApiExampleQueryIds(); + expect(ids).toContain("hello-world"); + expect(ids).toContain("account-profile"); + }); +}); diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index 072c3e20a..025f621e0 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -1,10 +1,13 @@ import { asInterpretedName, toNormalizedAddress } from "enssdk"; -import { DatasourceNames, ENSNamespaceIds } from "@ensnode/datasources"; +import { DatasourceNames, type ENSNamespaceId, ENSNamespaceIds } from "@ensnode/datasources"; import { accounts } from "@ensnode/integration-test-env/devnet"; import { getDatasourceContract } from "../shared/datasource-contract"; -import type { NamespaceSpecificValue } from "../shared/namespace-specific-value"; +import { + getNamespaceSpecificValue, + type NamespaceSpecificValue, +} from "../shared/namespace-specific-value"; const SEPOLIA_V2_V2_ETH_REGISTRY = getDatasourceContract( ENSNamespaceIds.SepoliaV2, @@ -58,24 +61,58 @@ const SEPOLIA_V2_RESOLVER_WITH_RECORDS = { export type GraphqlApiExampleQuery = { id: string; + title: string; + description: string; query: string; variables: NamespaceSpecificValue>; }; +/** Alternate example ids that resolve to a canonical entry in {@link GRAPHQL_API_EXAMPLE_QUERIES}. */ +export const GRAPHQL_API_EXAMPLE_ID_ALIASES: Record = { + "account-profile": "hello-world", +}; + export function getGraphqlApiExampleQueryById(id: string): GraphqlApiExampleQuery { - const found = graphqlApiExampleQueryById.get(id); + const resolvedId = GRAPHQL_API_EXAMPLE_ID_ALIASES[id] ?? id; + const found = graphqlApiExampleQueryById.get(resolvedId); if (!found) { throw new Error(`Unknown GraphQL API example query id: ${id}`); } return found; } +export function listGraphqlApiExampleQueryIds(): string[] { + return [ + ...GRAPHQL_API_EXAMPLE_QUERIES.map((example) => example.id), + ...Object.keys(GRAPHQL_API_EXAMPLE_ID_ALIASES), + ]; +} + +export function resolveGraphqlApiExampleQuery( + id: string, + options?: { namespace?: ENSNamespaceId; variables?: Record }, +): { query: string; variables: Record } { + const example = getGraphqlApiExampleQueryById(id); + const { namespace, variables: variablesOverride } = options ?? {}; + return { + query: example.query, + variables: + variablesOverride ?? + (namespace + ? getNamespaceSpecificValue(namespace, example.variables) + : example.variables.default), + }; +} + export const GRAPHQL_API_EXAMPLE_QUERIES: GraphqlApiExampleQuery[] = [ //////////////// // Hello World //////////////// { id: "hello-world", + title: "Hello World", + description: + "From a wallet address: Ethereum primary name and interpreted profile, plus ENSv1 and ENSv2 ownership counts.", query: `query HelloWorld($address: Address!) { # Lookup an Account by address. account(by: { address: $address }) { @@ -121,6 +158,8 @@ export const GRAPHQL_API_EXAMPLE_QUERIES: GraphqlApiExampleQuery[] = [ ///////////////// { id: "find-domains", + title: "Find Domains", + description: "List domains matching a name prefix with ordering and registration metadata.", query: ` query FindDomains( $name: DomainsNameFilter! @@ -158,6 +197,8 @@ query FindDomains( { id: "domain-by-name", + title: "Domain By Name", + description: "Load a domain by interpreted name, including profile information.", query: ` query DomainByName($name: InterpretedName!) { domain(by: { name: $name }) { @@ -185,6 +226,8 @@ query DomainByName($name: InterpretedName!) { //////////////////////////////// { id: "domain-by-name-type-condition", + title: "Domain By Name Type Condition", + description: "Load a domain by interpreted name with type condition.", query: ` query DomainByName($name: InterpretedName!) { domain(by: {name: $name}) { @@ -211,6 +254,8 @@ query DomainByName($name: InterpretedName!) { /////////////////////// { id: "domain-registration", + title: "Domain Registration", + description: "Load domain registration details.", query: ` query DomainRegistration($name: InterpretedName!) { domain(by: { name: $name }) { @@ -257,6 +302,8 @@ query DomainRegistration($name: InterpretedName!) { //////////////////// { id: "domain-records", + title: "Domain Records", + description: "For given name resolve raw records like `addresses`, `texts`, `contenthash` etc.", query: ` query DomainRecords($name: InterpretedName!) { domain(by: {name: $name}) { @@ -293,6 +340,8 @@ query DomainRecords($name: InterpretedName!) { { id: "domain-profile", + title: "Domain Profile", + description: "Load a domain's high-level profile (avatar, socials, addresses, and more).", query: ` query DomainProfile($name: InterpretedName!) { domain(by: {name: $name}) { @@ -337,6 +386,8 @@ query DomainProfile($name: InterpretedName!) { ////////////////////// { id: "domain-subdomains", + title: "Domain Subdomains", + description: "Paginate direct child names under a parent domain.", query: ` query DomainSubdomains($name: InterpretedName!) { domain(by: {name: $name}) { @@ -365,6 +416,8 @@ query DomainSubdomains($name: InterpretedName!) { //////////////////////////////////// { id: "domain-subdomains-recently-registered", + title: "Recently Registered Subdomains", + description: "List a parent domain's subdomains ordered by most recent registration first.", query: ` query RecentlyRegisteredSubdomains($name: InterpretedName!) { domain(by: {name: $name}) { @@ -386,6 +439,8 @@ query RecentlyRegisteredSubdomains($name: InterpretedName!) { //////////////////////// { id: "subdomains-pagination", + title: "Subdomains Pagination", + description: "Paginate through all subdomains of a parent domain.", query: ` query SubdomainsPagination($first: Int!, $after: String) { domain(by: { name: "eth" }) { @@ -412,6 +467,8 @@ query SubdomainsPagination($first: Int!, $after: String) { ///////////////// { id: "domain-events", + title: "Domain Events", + description: "Raw contract events associated with a domain's registry records.", query: ` query DomainEvents($name: InterpretedName!) { domain(by: {name: $name}) { @@ -441,6 +498,8 @@ query DomainEvents($name: InterpretedName!) { //////////////////// { id: "domains-by-address", + title: "Account Domains", + description: "Load domains owned by an address via the Omnigraph `account` root field.", query: ` query AccountDomains( $address: Address! @@ -468,6 +527,8 @@ query AccountDomains( ///////////////////////// { id: "account-primary-name", + title: "Account Primary Name", + description: "Load a primary name for an account on Ethereum, including profile information.", query: ` query AccountPrimaryName($address: Address!) { account(by: { address: $address }) { @@ -500,6 +561,8 @@ query AccountPrimaryName($address: Address!) { //////////////////// { id: "account-events", + title: "Account Events", + description: "Events touching an account across indexed ENS contracts.", query: ` query AccountEvents( $address: Address! @@ -520,6 +583,8 @@ query AccountEvents( ///////////////////// { id: "registry-domains", + title: "Registry Domains", + description: "Enumerate domains under a specific v2 ETH registry contract.", query: ` query RegistryDomains( $registry: AccountIdInput! @@ -547,6 +612,8 @@ query RegistryDomains( //////////////////////////// { id: "permissions-by-contract", + title: "Permissions By Contract", + description: "Roles and users granted on resources for a registrar or registry contract.", query: ` query PermissionsByContract( $contract: AccountIdInput! @@ -584,6 +651,8 @@ query PermissionsByContract( //////////////////////// { id: "permissions-by-user", + title: "Permissions By User", + description: "Resources and roles for an address in the permissions graph.", query: ` query PermissionsByUser($address: Address!) { account(by: { address: $address }) { @@ -608,6 +677,8 @@ query PermissionsByUser($address: Address!) { ////////////////////////////////// { id: "account-resolver-permissions", + title: "Account Resolver Permissions", + description: "Resolver contracts where an account has been granted resolver ACLs.", query: ` query AccountResolverPermissions($address: Address!) { account(by: { address: $address }) { @@ -635,6 +706,8 @@ query AccountResolverPermissions($address: Address!) { ////////////////////////////// { id: "domain-resolver", + title: "Domain Resolver", + description: "Assigned resolver contract address and recent resolver events.", query: ` query DomainResolver($name: InterpretedName!) { domain(by: { name: $name }) { @@ -662,6 +735,8 @@ query DomainResolver($name: InterpretedName!) { //////////////////////// { id: "resolver-by-address", + title: "Resolver By Address", + description: "Load a resolver by its contract address.", query: ` query ResolverByAddress($contract: AccountIdInput!) { resolver(by: { contract: $contract }) { @@ -695,6 +770,9 @@ query ResolverByAddress($contract: AccountIdInput!) { ////////////// { id: "namegraph", + title: "Namegraph", + description: + "Walk a domain's registry, parent, subregistry, and direct subdomains (as in Core Concepts).", query: ` query Namegraph { domain(by: { name: "eth" }) { @@ -720,6 +798,8 @@ query Namegraph { ///////////////////////////// { id: "account-migrated-names", + title: "Account Migration Counts", + description: "Count an account's ENSv1 vs ENSv2 domains to gauge its migration progress.", query: ` query AccountMigratedNames($address: Address!) { account(by: { address: $address }) { @@ -734,6 +814,9 @@ query AccountMigratedNames($address: Address!) { }, { id: "eth-by-version", + title: "ETH TLD By Version", + description: + "Load the .eth TLD across protocol versions: one Domain per version, discriminated by `__typename` (ENSv1Domain / ENSv2Domain).", query: ` query GetEthDomains { domains(where: { name: { eq: "eth" } }) { @@ -749,6 +832,9 @@ query GetEthDomains { }, { id: "accelerate-resolve", + title: "Resolve primary name and records, and track protocol acceleration", + description: + "Resolve primary name and records, and track protocol acceleration with `trace` and `accelerate` arguments.", query: ` query AccelerateResolve($address: Address!) { account(by: { address: $address }) { diff --git a/packages/ensnode-sdk/src/omnigraph-api/generated/schema-sdl.ts b/packages/ensnode-sdk/src/omnigraph-api/generated/schema-sdl.ts new file mode 100644 index 000000000..73046cf82 --- /dev/null +++ b/packages/ensnode-sdk/src/omnigraph-api/generated/schema-sdl.ts @@ -0,0 +1,2 @@ +/** Auto-generated from enssdk schema.graphql — run `pnpm generate:gqlschema` to refresh. */ +export const omnigraphSchemaSdl = "\"\"\"Execution status metadata for a resolver strategy.\"\"\"\ntype AccelerationStatus {\n \"\"\"Whether protocol acceleration was attempted at runtime.\"\"\"\n attempted: Boolean!\n\n \"\"\"Whether protocol acceleration was requested by the caller.\"\"\"\n requested: Boolean!\n}\n\n\"\"\"Represents an individual Account, keyed by its Address.\"\"\"\ntype Account {\n \"\"\"An EVM Address that uniquely identifies this Account on-chain.\"\"\"\n address: Address!\n\n \"\"\"\n The Domains that are owned by the Account. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: AccountDomainsWhereInput): AccountDomainsConnection\n\n \"\"\"\n All Events for which this Account is the HCA-aware `sender` (i.e. `Event.sender`).\n \"\"\"\n events(after: String, before: String, first: Int, last: Int, where: AccountEventsWhereInput): AccountEventsConnection\n\n \"\"\"A unique reference to this Account.\"\"\"\n id: Address!\n\n \"\"\"\n The Permissions granted to this Account, optionally filtered to Permissions in a specific contract.\n \"\"\"\n permissions(after: String, before: String, first: Int, last: Int, where: AccountPermissionsWhereInput): AccountPermissionsConnection\n\n \"\"\"The Permissions on Registries granted to this Account.\"\"\"\n registryPermissions(after: String, before: String, first: Int, last: Int): AccountRegistryPermissionsConnection\n\n \"\"\"Resolve primary names for this Account.\"\"\"\n resolve(\n \"\"\"\n When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.\n @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration\n \"\"\"\n accelerate: Boolean = true\n ): ReverseResolve!\n\n \"\"\"The Permissions on Resolvers granted to this Account.\"\"\"\n resolverPermissions(after: String, before: String, first: Int, last: Int): AccountResolverPermissionsConnection\n}\n\n\"\"\"Address an Account by ID or Address.\"\"\"\ninput AccountByInput @oneOf {\n address: Address\n id: Address\n}\n\ntype AccountDomainsConnection {\n edges: [AccountDomainsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype AccountDomainsConnectionEdge {\n cursor: String!\n node: Domain!\n}\n\n\"\"\"Filter for Account.domains query.\"\"\"\ninput AccountDomainsWhereInput {\n \"\"\"\n If set, filters the set of Domains by canonicality (i.e. reachability by ENS Forward Resolution): `true` for Canonical only, `false` for non-Canonical only. If omitted, returns all Domains owned by the Account regardless of canonicality.\n \"\"\"\n canonical: Boolean\n\n \"\"\"If set, filters the set of Domains by name.\"\"\"\n name: DomainsNameFilter\n\n \"\"\"\n If set, filters the set of Domains to only those of the specified ENS protocol version.\n \"\"\"\n version: ENSProtocolVersion\n}\n\ntype AccountEventsConnection {\n edges: [AccountEventsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype AccountEventsConnectionEdge {\n cursor: String!\n node: Event!\n}\n\n\"\"\"\nFilter conditions for Account.events (where `sender` is implied by the Account).\n\"\"\"\ninput AccountEventsWhereInput {\n \"\"\"\n Filter to events whose `tx.from` matches the provided filter. Not HCA-aware — the Account's HCA-aware filter is applied via `sender = Account.id`.\n \"\"\"\n from: EventsFromFilter\n\n \"\"\"\n Filter to events whose selector (event signature) matches the provided filter.\n \"\"\"\n selector: EventsSelectorFilter\n\n \"\"\"Filter to events whose UnixTimestamp falls within the provided range.\"\"\"\n timestamp: EventsTimestampFilter\n}\n\n\"\"\"A CAIP-10 Account ID including chainId and address.\"\"\"\ntype AccountId {\n address: Address!\n chainId: ChainId!\n}\n\n\"\"\"A CAIP-10 Account ID including chainId and address.\"\"\"\ninput AccountIdInput {\n address: Address!\n chainId: ChainId!\n}\n\ntype AccountPermissionsConnection {\n edges: [AccountPermissionsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype AccountPermissionsConnectionEdge {\n cursor: String!\n node: PermissionsUser!\n}\n\n\"\"\"Filter for Account.permissions.\"\"\"\ninput AccountPermissionsWhereInput {\n \"\"\"\n If set, filters this Account's Permissions to those granted in this contract.\n \"\"\"\n contract: AccountIdInput\n}\n\ntype AccountRegistryPermissionsConnection {\n edges: [AccountRegistryPermissionsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype AccountRegistryPermissionsConnectionEdge {\n cursor: String!\n node: RegistryPermissionsUser!\n}\n\ntype AccountResolverPermissionsConnection {\n edges: [AccountResolverPermissionsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype AccountResolverPermissionsConnectionEdge {\n cursor: String!\n node: ResolverPermissionsUser!\n}\n\n\"\"\"Address represents an EVM Address in all lowercase.\"\"\"\nscalar Address\n\n\"\"\"\nA BaseRegistrarRegistration represents a Registration within an ENSv1 BaseRegistrar contract, including those deployed by Basenames and Lineanames.\n\"\"\"\ntype BaseRegistrarRegistration implements Registration {\n \"\"\"The `baseCost` for registering this Domain, in wei.\"\"\"\n baseCost: BigInt\n\n \"\"\"The Domain for which this Registration exists.\"\"\"\n domain: Domain!\n\n \"\"\"The Event for which this Registration was created.\"\"\"\n event: Event!\n\n \"\"\"\n Indicates whether this Registration is expired. If the Registration is for an ENSv1Domain, a Registration is only considered `expired` after the Grace Period has elapsed.\n \"\"\"\n expired: Boolean!\n\n \"\"\"A UnixTimestamp indicating the Registration's expiry, if exists.\"\"\"\n expiry: BigInt\n\n \"\"\"A unique reference to this Registration.\"\"\"\n id: RegistrationId!\n\n \"\"\"\n Whether this Registration is in the Grace Period (90 days) and can be renewed by the current owner.\n \"\"\"\n isInGracePeriod: Boolean!\n\n \"\"\"The `premium` for registering this Domain, in wei.\"\"\"\n premium: BigInt\n\n \"\"\"The extra `referrer` data provided with a Registration, if exists.\"\"\"\n referrer: Hex\n\n \"\"\"\n The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used).\n \"\"\"\n registrant: Account\n\n \"\"\"The Registrar contract under which this Registration is managed.\"\"\"\n registrar: AccountId!\n\n \"\"\"\n Renewals that have occurred within this Registration's lifespan to extend its expiration.\n \"\"\"\n renewals(after: String, before: String, first: Int, last: Int): RegistrationRenewalsConnection\n\n \"\"\"A UnixTimestamp indicating when this Registration was created.\"\"\"\n start: BigInt!\n\n \"\"\"\n The TokenId for this Domain in the BaseRegistrar. This is the bigint encoding of the Domain's LabelHash.\n \"\"\"\n tokenId: BigInt!\n\n \"\"\"\n The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).\n \"\"\"\n unregistrant: Account\n\n \"\"\"\n Additional metadata if this BaseRegistrarRegistration is wrapped by the NameWrapper (i.e. in the case of wrapped .eth names).\n \"\"\"\n wrapped: WrappedBaseRegistrarRegistration\n}\n\n\"\"\"\nBeautifiedLabel represents an enssdk#BeautifiedLabel: an InterpretedLabel beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. It is display-only and MUST NOT be used as a lookup key.\n\"\"\"\nscalar BeautifiedLabel\n\n\"\"\"\nBeautifiedName represents an enssdk#BeautifiedName: an InterpretedName whose normalized labels have been beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. It is display-only and MUST NOT be used as a navigation target or lookup key.\n\"\"\"\nscalar BeautifiedName\n\n\"\"\"BigInt represents non-fractional signed whole numeric values.\"\"\"\nscalar BigInt\n\n\"\"\"\nBinanceAddress represents a Bech32-encoded Binance Chain (BNB) address (coin type 714).\n\"\"\"\nscalar BinanceAddress\n\n\"\"\"\nBitcoinAddress represents a Base58Check-encoded Bitcoin address (coin type 0).\n\"\"\"\nscalar BitcoinAddress\n\n\"\"\"\nBitcoinCashAddress represents a CashAddr-encoded Bitcoin Cash address (coin type 145).\n\"\"\"\nscalar BitcoinCashAddress\n\n\"\"\"A Canonical Name, exposed in each representation we support.\"\"\"\ntype CanonicalName {\n \"\"\"\n The Canonical Name as a BeautifiedName: the InterpretedName with its normalized labels beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. Encoded LabelHash labels are preserved verbatim. Display-only; use `interpreted` for navigation targets and lookup keys.\n \"\"\"\n beautified: BeautifiedName!\n\n \"\"\"\n The Canonical Name as an InterpretedName: each label is either a normalized literal Label or an Encoded LabelHash.\n \"\"\"\n interpreted: InterpretedName!\n}\n\n\"\"\"ChainId represents an enssdk#ChainId.\"\"\"\nscalar ChainId\n\n\"\"\"\nThe names of chains that the Omnigraph API supports identifying by name as a syntactic convenience. The Omnigraph API supports identification of additional chains beyond this list, but those chains must be identified through other mechanisms such as `coinType` or `chainId`.\n\"\"\"\nenum ChainName {\n ARBITRUM_ONE\n BASE\n ETHEREUM\n LINEA\n OPTIMISM\n SCROLL\n}\n\n\"\"\"CoinType represents an enssdk#CoinType.\"\"\"\nscalar CoinType\n\n\"\"\"\nDogecoinAddress represents a Base58Check-encoded Dogecoin address (coin type 3).\n\"\"\"\nscalar DogecoinAddress\n\n\"\"\"\nRepresents a Domain, i.e. an individual Label within the ENS namegraph. It may or may not be Canonical. It may be an ENSv1Domain or an ENSv2Domain.\n\"\"\"\ninterface Domain {\n \"\"\"\n Metadata (name, path, and node) related to the Domain's canonicality, if known. Null when the Domain is not in the canonical nametree.\n \"\"\"\n canonical: DomainCanonical\n\n \"\"\"All Events associated with this Domain.\"\"\"\n events(after: String, before: String, first: Int, last: Int, where: EventsWhereInput): DomainEventsConnection\n\n \"\"\"A unique and stable reference to this Domain.\"\"\"\n id: DomainId!\n\n \"\"\"The Label associated with this Domain in the ENS Namegraph.\"\"\"\n label: Label!\n\n \"\"\"\n If this is an ENSv1Domain, this is the effective owner of the Domain (derived from the Registry, the Registrar, or the NameWrapper, in that order). If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).\n \"\"\"\n owner: Account\n\n \"\"\"\n The Domain that this Domain's parent Registry declares as its Canonical Domain, if any. Follows a single unidirectional pointer (`Registry.canonicalDomainId`) and does NOT enforce bidirectional canonical-edge agreement: a non-canonical Domain may have a non-null `parent`, and a canonical Domain's `parent` may itself be non-canonical. Null when the parent Registry does not declare a Canonical Domain. For an UnindexedDomain (which has no Registry of its own), this reflects the wildcard-bearing ancestor's Registry — see `Domain.registry`.\n \"\"\"\n parent: Domain\n\n \"\"\"The latest Registration for this Domain, if exists.\"\"\"\n registration: Registration\n\n \"\"\"All Registrations for a Domain, including the latest Registration.\"\"\"\n registrations(after: String, before: String, first: Int, last: Int): DomainRegistrationsConnection\n\n \"\"\"\n The Registry under which this Domain exists. For an UnindexedDomain — a resolvable-but-unindexed Domain that has no Registry of its own — this is instead the Registry that manages the ancestor Domain bearing the wildcard Resolver (the same Registry encoded in its `id`).\n \"\"\"\n registry: Registry!\n\n \"\"\"Resolve protocol-level data for this Domain.\"\"\"\n resolve(\n \"\"\"\n When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.\n @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration\n \"\"\"\n accelerate: Boolean = true\n ): ForwardResolve!\n\n \"\"\"Resolver relationship metadata for this Domain.\"\"\"\n resolver: DomainResolver!\n\n \"\"\"\n All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection\n\n \"\"\"The Registry this Domain declares as its Subregistry, if exists.\"\"\"\n subregistry: Registry\n}\n\n\"\"\"\nCanonicality metadata for a Domain, including its name, depth, path, and node (namehash).\n\"\"\"\ntype DomainCanonical {\n \"\"\"\n The depth of this Domain, i.e. the number of labels in this Domain's Canonical Name (e.g. 2 for `vitalik.eth`).\n \"\"\"\n depth: Int!\n\n \"\"\"The Canonical Name for this Domain.\"\"\"\n name: CanonicalName!\n\n \"\"\"\n The namehash of this Domain's Canonical Name. Note that this is NOT a stable reference to this Domain; use `Domain.id`.\n \"\"\"\n node: Node!\n\n \"\"\"\n The Canonical Path from this Domain to the ENS Root, root→leaf inclusive of this Domain.\n \"\"\"\n path: [Domain!]!\n}\n\ntype DomainEventsConnection {\n edges: [DomainEventsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype DomainEventsConnectionEdge {\n cursor: String!\n node: Event!\n}\n\n\"\"\"DomainId represents an enssdk#DomainId.\"\"\"\nscalar DomainId\n\n\"\"\"Reference a specific Domain.\"\"\"\ninput DomainIdInput @oneOf {\n id: DomainId\n name: InterpretedName\n}\n\n\"\"\"\nFilter Permissions by user address. Exactly one of `eq` or `in` must be provided.\n\"\"\"\ninput DomainPermissionsUserFilter @oneOf {\n \"\"\"Exact user address match.\"\"\"\n eq: Address\n\n \"\"\"\n User address matches any value in the set. Max 10 items. An empty set matches nothing.\n \"\"\"\n in: [Address!]\n}\n\n\"\"\"Filter Permissions over this Domain by user.\"\"\"\ninput DomainPermissionsWhereInput {\n \"\"\"Filter Permissions to those whose user matches the provided filter.\"\"\"\n user: DomainPermissionsUserFilter\n}\n\n\"\"\"The interpreted profile of an ENS name.\"\"\"\ntype DomainProfile {\n \"\"\"The interpreted addresses on the profile of an ENS name.\"\"\"\n addresses: ProfileAddresses\n\n \"\"\"\n Interpreted avatar metadata. Returns null when the raw avatar record is unset or empty.\n \"\"\"\n avatar: ProfileAvatar\n\n \"\"\"\n The interpreted description on the profile of an ENS name, or null when unset.\n \"\"\"\n description: String\n\n \"\"\"\n The interpreted email address on the profile of an ENS name, or null when unset or invalid.\n \"\"\"\n email: Email\n\n \"\"\"\n Interpreted header metadata. Returns null when the raw header record is unset or empty.\n \"\"\"\n header: ProfileHeader\n\n \"\"\"The interpreted social accounts on the profile of an ENS name.\"\"\"\n socials: ProfileSocials\n\n \"\"\"The interpreted website on the profile of an ENS name.\"\"\"\n website: ProfileWebsite\n}\n\ntype DomainRegistrationsConnection {\n edges: [DomainRegistrationsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype DomainRegistrationsConnectionEdge {\n cursor: String!\n node: Registration!\n}\n\n\"\"\"Metadata describing this Domain's relationship to its Resolver(s).\"\"\"\ntype DomainResolver {\n \"\"\"\n The Resolver that this Domain has assigned, if any. NOTE that this is the Domain's _assigned_ Resolver, _not_ its _effective_ Resolver, which can only be determined by following ENS Forward Resolution and ENSIP-10. Do NOT use this Domain-Resolver relationship in isolation to resolve records, that operation is NOT ENS Forward Resolution.\n \"\"\"\n assigned: Resolver\n\n \"\"\"\n The Resolver that ENS Forward Resolution (ENSIP-10) lands on for this Domain — i.e. its _effective_ Resolver. Null when no active Resolver exists or the Domain is not in the Canonical Nametree.\n \"\"\"\n effective: Resolver\n}\n\ntype DomainSubdomainsConnection {\n edges: [DomainSubdomainsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype DomainSubdomainsConnectionEdge {\n cursor: String!\n node: Domain!\n}\n\n\"\"\"\nFilter Domains by name. Exactly one of `starts_with`, `eq`, or `in` must be provided.\n\"\"\"\ninput DomainsNameFilter @oneOf {\n \"\"\"\n Exact InterpretedName match. Sugar for `in: [eq]`. Combine with `version` to disambiguate across ENS protocol versions.\n \"\"\"\n eq: InterpretedName\n\n \"\"\"\n Exact InterpretedName match against any name in the set. Max 100 items.\n \"\"\"\n in: [InterpretedName!]\n\n \"\"\"\n Prefix-match on Interpreted Name for typeahead. ex: 'vit', 'vitalik.et'. Case-insensitive (InterpretedName labels are normalized). Matched against the first 64 code points of the name; prefixes longer than 64 code points never match.\n \"\"\"\n starts_with: String\n}\n\n\"\"\"Fields by which domains can be ordered.\"\"\"\nenum DomainsOrderBy {\n \"\"\"\n Order by Canonical Name depth (number of labels); e.g. `eth` < `vitalik.eth`.\n \"\"\"\n DEPTH\n\n \"\"\"Order by the Domain's Canonical Name, alphabetically.\"\"\"\n NAME\n\n \"\"\"\n Order by the expiry of the Domain's latest Registration. A Domain that never expires (or has no Registration) is treated as +∞: it sorts last when `dir: ASC` (“expiring soonest first”) and first when `dir: DESC` (“expiring latest first”).\n \"\"\"\n REGISTRATION_EXPIRY\n\n \"\"\"\n Order by the start time of the Domain's latest Registration. A Domain with no Registration has no timestamp and sorts last when `dir: ASC` (“oldest registered first”) and first when `dir: DESC` (“most recently registered first”).\n \"\"\"\n REGISTRATION_TIMESTAMP\n}\n\n\"\"\"\nOrdering options for domains query. If no order is provided, the default is ASC.\n\"\"\"\ninput DomainsOrderInput {\n by: DomainsOrderBy!\n dir: OrderDirection = ASC\n}\n\n\"\"\"Filter for the top-level domains query.\"\"\"\ninput DomainsWhereInput {\n \"\"\"Filter the set of Domains by name.\"\"\"\n name: DomainsNameFilter!\n\n \"\"\"\n If set, filters the set of Domains to only those of the specified ENS protocol version.\n \"\"\"\n version: ENSProtocolVersion\n}\n\n\"\"\"An ENS protocol version.\"\"\"\nenum ENSProtocolVersion {\n ENSv1\n ENSv2\n}\n\n\"\"\"An ENSv1Domain represents an ENSv1 Domain.\"\"\"\ntype ENSv1Domain implements Domain {\n \"\"\"\n Metadata (name, path, and node) related to the Domain's canonicality, if known. Null when the Domain is not in the canonical nametree.\n \"\"\"\n canonical: DomainCanonical\n\n \"\"\"All Events associated with this Domain.\"\"\"\n events(after: String, before: String, first: Int, last: Int, where: EventsWhereInput): DomainEventsConnection\n\n \"\"\"A unique and stable reference to this Domain.\"\"\"\n id: DomainId!\n\n \"\"\"The Label associated with this Domain in the ENS Namegraph.\"\"\"\n label: Label!\n\n \"\"\"The namehash of this ENSv1 Domain.\"\"\"\n node: Node!\n\n \"\"\"\n If this is an ENSv1Domain, this is the effective owner of the Domain (derived from the Registry, the Registrar, or the NameWrapper, in that order). If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).\n \"\"\"\n owner: Account\n\n \"\"\"\n The Domain that this Domain's parent Registry declares as its Canonical Domain, if any. Follows a single unidirectional pointer (`Registry.canonicalDomainId`) and does NOT enforce bidirectional canonical-edge agreement: a non-canonical Domain may have a non-null `parent`, and a canonical Domain's `parent` may itself be non-canonical. Null when the parent Registry does not declare a Canonical Domain. For an UnindexedDomain (which has no Registry of its own), this reflects the wildcard-bearing ancestor's Registry — see `Domain.registry`.\n \"\"\"\n parent: Domain\n\n \"\"\"The latest Registration for this Domain, if exists.\"\"\"\n registration: Registration\n\n \"\"\"All Registrations for a Domain, including the latest Registration.\"\"\"\n registrations(after: String, before: String, first: Int, last: Int): DomainRegistrationsConnection\n\n \"\"\"\n The Registry under which this Domain exists. For an UnindexedDomain — a resolvable-but-unindexed Domain that has no Registry of its own — this is instead the Registry that manages the ancestor Domain bearing the wildcard Resolver (the same Registry encoded in its `id`).\n \"\"\"\n registry: Registry!\n\n \"\"\"Resolve protocol-level data for this Domain.\"\"\"\n resolve(\n \"\"\"\n When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.\n @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration\n \"\"\"\n accelerate: Boolean = true\n ): ForwardResolve!\n\n \"\"\"Resolver relationship metadata for this Domain.\"\"\"\n resolver: DomainResolver!\n\n \"\"\"\n The rootRegistryOwner of this Domain, i.e. the owner() of this Domain within the ENSv1 Registry.\n \"\"\"\n rootRegistryOwner: Account\n\n \"\"\"\n All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection\n\n \"\"\"The Registry this Domain declares as its Subregistry, if exists.\"\"\"\n subregistry: Registry\n}\n\n\"\"\"\nAn ENSv1Registry is a concrete ENSv1 Registry contract (the mainnet ENS Registry, the Basenames shadow Registry, or the Lineanames shadow Registry).\n\"\"\"\ntype ENSv1Registry implements Registry {\n \"\"\"Whether the Registry is Canonical.\"\"\"\n canonical: Boolean!\n\n \"\"\"\n Contract metadata for this Registry. If this is an ENSv1VirtualRegistry, this will reference the concrete Registry contract under which the parent Domain exists.\n \"\"\"\n contract: AccountId!\n\n \"\"\"\n The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection\n\n \"\"\"A unique reference to this Registry.\"\"\"\n id: RegistryId!\n\n \"\"\"The Domains for which this Registry is a Subregistry.\"\"\"\n parents(after: String, before: String, first: Int, last: Int): RegistryParentsConnection\n\n \"\"\"The Permissions managed by this Registry.\"\"\"\n permissions: Permissions\n}\n\n\"\"\"\nAn ENSv1VirtualRegistry is the virtual Registry managed by an ENSv1 Domain that has children. It is keyed by `(chainId, address, node)` where `(chainId, address)` identify the concrete Registry that houses the parent Domain, and `node` is the parent Domain's namehash.\n\"\"\"\ntype ENSv1VirtualRegistry implements Registry {\n \"\"\"Whether the Registry is Canonical.\"\"\"\n canonical: Boolean!\n\n \"\"\"\n Contract metadata for this Registry. If this is an ENSv1VirtualRegistry, this will reference the concrete Registry contract under which the parent Domain exists.\n \"\"\"\n contract: AccountId!\n\n \"\"\"\n The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection\n\n \"\"\"A unique reference to this Registry.\"\"\"\n id: RegistryId!\n\n \"\"\"\n The namehash of the parent ENSv1 Domain that owns this virtual Registry.\n \"\"\"\n node: Node!\n\n \"\"\"The Domains for which this Registry is a Subregistry.\"\"\"\n parents(after: String, before: String, first: Int, last: Int): RegistryParentsConnection\n\n \"\"\"The Permissions managed by this Registry.\"\"\"\n permissions: Permissions\n}\n\n\"\"\"An ENSv2Domain represents an ENSv2 Domain.\"\"\"\ntype ENSv2Domain implements Domain {\n \"\"\"\n Metadata (name, path, and node) related to the Domain's canonicality, if known. Null when the Domain is not in the canonical nametree.\n \"\"\"\n canonical: DomainCanonical\n\n \"\"\"All Events associated with this Domain.\"\"\"\n events(after: String, before: String, first: Int, last: Int, where: EventsWhereInput): DomainEventsConnection\n\n \"\"\"A unique and stable reference to this Domain.\"\"\"\n id: DomainId!\n\n \"\"\"The Label associated with this Domain in the ENS Namegraph.\"\"\"\n label: Label!\n\n \"\"\"\n If this is an ENSv1Domain, this is the effective owner of the Domain (derived from the Registry, the Registrar, or the NameWrapper, in that order). If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).\n \"\"\"\n owner: Account\n\n \"\"\"\n The Domain that this Domain's parent Registry declares as its Canonical Domain, if any. Follows a single unidirectional pointer (`Registry.canonicalDomainId`) and does NOT enforce bidirectional canonical-edge agreement: a non-canonical Domain may have a non-null `parent`, and a canonical Domain's `parent` may itself be non-canonical. Null when the parent Registry does not declare a Canonical Domain. For an UnindexedDomain (which has no Registry of its own), this reflects the wildcard-bearing ancestor's Registry — see `Domain.registry`.\n \"\"\"\n parent: Domain\n\n \"\"\"\n Permissions for this Domain within its Registry, representing the roles granted to users for this Domain's token.\n \"\"\"\n permissions(after: String, before: String, first: Int, last: Int, where: DomainPermissionsWhereInput): ENSv2DomainPermissionsConnection\n\n \"\"\"The latest Registration for this Domain, if exists.\"\"\"\n registration: Registration\n\n \"\"\"All Registrations for a Domain, including the latest Registration.\"\"\"\n registrations(after: String, before: String, first: Int, last: Int): DomainRegistrationsConnection\n\n \"\"\"\n The Registry under which this Domain exists. For an UnindexedDomain — a resolvable-but-unindexed Domain that has no Registry of its own — this is instead the Registry that manages the ancestor Domain bearing the wildcard Resolver (the same Registry encoded in its `id`).\n \"\"\"\n registry: Registry!\n\n \"\"\"Resolve protocol-level data for this Domain.\"\"\"\n resolve(\n \"\"\"\n When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.\n @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration\n \"\"\"\n accelerate: Boolean = true\n ): ForwardResolve!\n\n \"\"\"Resolver relationship metadata for this Domain.\"\"\"\n resolver: DomainResolver!\n\n \"\"\"\n All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection\n\n \"\"\"The Registry this Domain declares as its Subregistry, if exists.\"\"\"\n subregistry: Registry\n\n \"\"\"The ENSv2Domain's current Token Id.\"\"\"\n tokenId: BigInt!\n}\n\ntype ENSv2DomainPermissionsConnection {\n edges: [ENSv2DomainPermissionsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype ENSv2DomainPermissionsConnectionEdge {\n cursor: String!\n node: PermissionsUser!\n}\n\n\"\"\"An ENSv2Registry represents an ENSv2 Registry contract.\"\"\"\ntype ENSv2Registry implements Registry {\n \"\"\"Whether the Registry is Canonical.\"\"\"\n canonical: Boolean!\n\n \"\"\"\n Contract metadata for this Registry. If this is an ENSv1VirtualRegistry, this will reference the concrete Registry contract under which the parent Domain exists.\n \"\"\"\n contract: AccountId!\n\n \"\"\"\n The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection\n\n \"\"\"A unique reference to this Registry.\"\"\"\n id: RegistryId!\n\n \"\"\"The Domains for which this Registry is a Subregistry.\"\"\"\n parents(after: String, before: String, first: Int, last: Int): RegistryParentsConnection\n\n \"\"\"The Permissions managed by this Registry.\"\"\"\n permissions: Permissions\n}\n\n\"\"\"\nENSv2RegistryRegistration represents a Registration within an ENSv2 Registry.\n\"\"\"\ntype ENSv2RegistryRegistration implements Registration {\n \"\"\"The Domain for which this Registration exists.\"\"\"\n domain: Domain!\n\n \"\"\"The Event for which this Registration was created.\"\"\"\n event: Event!\n\n \"\"\"\n Indicates whether this Registration is expired. If the Registration is for an ENSv1Domain, a Registration is only considered `expired` after the Grace Period has elapsed.\n \"\"\"\n expired: Boolean!\n\n \"\"\"A UnixTimestamp indicating the Registration's expiry, if exists.\"\"\"\n expiry: BigInt\n\n \"\"\"A unique reference to this Registration.\"\"\"\n id: RegistrationId!\n\n \"\"\"The extra `referrer` data provided with a Registration, if exists.\"\"\"\n referrer: Hex\n\n \"\"\"\n The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used).\n \"\"\"\n registrant: Account\n\n \"\"\"The Registrar contract under which this Registration is managed.\"\"\"\n registrar: AccountId!\n\n \"\"\"\n Renewals that have occurred within this Registration's lifespan to extend its expiration.\n \"\"\"\n renewals(after: String, before: String, first: Int, last: Int): RegistrationRenewalsConnection\n\n \"\"\"A UnixTimestamp indicating when this Registration was created.\"\"\"\n start: BigInt!\n\n \"\"\"\n The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).\n \"\"\"\n unregistrant: Account\n}\n\n\"\"\"\nENSv2RegistryReservation represents a Reservation within an ENSv2 Registry.\n\"\"\"\ntype ENSv2RegistryReservation implements Registration {\n \"\"\"The Domain for which this Registration exists.\"\"\"\n domain: Domain!\n\n \"\"\"The Event for which this Registration was created.\"\"\"\n event: Event!\n\n \"\"\"\n Indicates whether this Registration is expired. If the Registration is for an ENSv1Domain, a Registration is only considered `expired` after the Grace Period has elapsed.\n \"\"\"\n expired: Boolean!\n\n \"\"\"A UnixTimestamp indicating the Registration's expiry, if exists.\"\"\"\n expiry: BigInt\n\n \"\"\"A unique reference to this Registration.\"\"\"\n id: RegistrationId!\n\n \"\"\"The extra `referrer` data provided with a Registration, if exists.\"\"\"\n referrer: Hex\n\n \"\"\"\n The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used).\n \"\"\"\n registrant: Account\n\n \"\"\"The Registrar contract under which this Registration is managed.\"\"\"\n registrar: AccountId!\n\n \"\"\"\n Renewals that have occurred within this Registration's lifespan to extend its expiration.\n \"\"\"\n renewals(after: String, before: String, first: Int, last: Int): RegistrationRenewalsConnection\n\n \"\"\"A UnixTimestamp indicating when this Registration was created.\"\"\"\n start: BigInt!\n\n \"\"\"\n The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).\n \"\"\"\n unregistrant: Account\n}\n\n\"\"\"Email represents a validated contact email address.\"\"\"\nscalar Email\n\n\"\"\"\nAn Event represents a discrete Log Event that was emitted on an EVM chain, including associated metadata.\n\"\"\"\ntype Event {\n \"\"\"Identifies the contract by which this Event was emitted.\"\"\"\n address: Address!\n\n \"\"\"Identifies the Block within which this Event was emitted.\"\"\"\n blockHash: Hex!\n\n \"\"\"The block number within which this Event was emitted.\"\"\"\n blockNumber: BigInt!\n\n \"\"\"The ChainId upon which this Event was emitted.\"\"\"\n chainId: ChainId!\n\n \"\"\"The non-indexed data of this Event's log.\"\"\"\n data: Hex!\n\n \"\"\"\n Identifies the sender of the Transaction within which this Event was emitted (`tx.from`). Never HCA-aware — always the EOA/relayer that submitted the transaction. Use `Event.sender` for the HCA-aware actor.\n \"\"\"\n from: Address!\n\n \"\"\"A unique reference to this Event.\"\"\"\n id: ID!\n\n \"\"\"The index of this Event's log within the Block.\"\"\"\n logIndex: Int!\n\n \"\"\"The HCA account address if used, otherwise Transaction.from.\"\"\"\n sender: Address!\n\n \"\"\"\n The UnixTimestamp indicating the moment in which this Event was emitted.\n \"\"\"\n timestamp: BigInt!\n\n \"\"\"\n Identifies the recipient of the Transaction within which this Event was emitted. Null if the transaction deployed a contract.\n \"\"\"\n to: Address\n\n \"\"\"The indexed topics of this Event's log.\"\"\"\n topics: [Hex!]!\n\n \"\"\"Identifies the Transaction within which this Event was emitted.\"\"\"\n transactionHash: Hex!\n\n \"\"\"The index of the Transaction within the Block.\"\"\"\n transactionIndex: Int!\n}\n\n\"\"\"\nFilter Events by `tx.from`. Not HCA-aware — use `sender` to filter by the HCA-aware actor. Exactly one of `eq` or `in` must be provided.\n\"\"\"\ninput EventsFromFilter @oneOf {\n \"\"\"Exact `tx.from` match.\"\"\"\n eq: Address\n\n \"\"\"\n `tx.from` matches any address in the set. Max 10 items. An empty set matches nothing.\n \"\"\"\n in: [Address!]\n}\n\n\"\"\"\nFilter Events by selector (event signature). Exactly one of `eq` or `in` must be provided.\n\"\"\"\ninput EventsSelectorFilter @oneOf {\n \"\"\"Exact selector match.\"\"\"\n eq: Hex\n\n \"\"\"\n Selector matches any value in the set. Max 10 items. An empty set matches nothing.\n \"\"\"\n in: [Hex!]\n}\n\n\"\"\"\nFilter Events by HCA-aware `sender` (the HCA account address if used, otherwise Transaction.from). Exactly one of `eq` or `in` must be provided.\n\"\"\"\ninput EventsSenderFilter @oneOf {\n \"\"\"Exact `sender` match.\"\"\"\n eq: Address\n\n \"\"\"\n `sender` matches any address in the set. Max 10 items. An empty set matches nothing.\n \"\"\"\n in: [Address!]\n}\n\n\"\"\"\nFilter Events by timestamp range. At least one bound must be provided. `gt`/`gte` are mutually exclusive; `lt`/`lte` are mutually exclusive.\n\"\"\"\ninput EventsTimestampFilter {\n \"\"\"Filter to events strictly after this UnixTimestamp.\"\"\"\n gt: BigInt\n\n \"\"\"Filter to events at or after this UnixTimestamp.\"\"\"\n gte: BigInt\n\n \"\"\"Filter to events strictly before this UnixTimestamp.\"\"\"\n lt: BigInt\n\n \"\"\"Filter to events at or before this UnixTimestamp.\"\"\"\n lte: BigInt\n}\n\n\"\"\"Filter conditions for an events connection.\"\"\"\ninput EventsWhereInput {\n \"\"\"\n Filter to events whose `tx.from` matches the provided filter. Not HCA-aware — use `sender` to filter by the HCA-aware actor.\n \"\"\"\n from: EventsFromFilter\n\n \"\"\"\n Filter to events whose selector (event signature) matches the provided filter.\n \"\"\"\n selector: EventsSelectorFilter\n\n \"\"\"\n Filter to events whose HCA-aware `sender` matches the provided filter (the HCA account address if used, otherwise Transaction.from).\n \"\"\"\n sender: EventsSenderFilter\n\n \"\"\"Filter to events whose UnixTimestamp falls within the provided range.\"\"\"\n timestamp: EventsTimestampFilter\n}\n\n\"\"\"\nNested domain resolution container exposing resolved data for the domain.\n\"\"\"\ntype ForwardResolve {\n \"\"\"\n Whether protocol acceleration was requested and attempted for this resolution.\n \"\"\"\n acceleration: AccelerationStatus!\n\n \"\"\"\n The interpreted profile of an ENS name. Returns null when the name is not resolvable (non-canonical, unnormalized, or no profile records were selected).\n \"\"\"\n profile: DomainProfile\n\n \"\"\"\n Resolved ENS records via the ENS protocol. Null when the name is not resolvable (non-canonical, unnormalized, or no records field was selected).\n \"\"\"\n records: ResolvedRecords\n\n \"\"\"\n Protocol trace tree emitted by resolution, represented as untyped JSON for schema stability. This data model should be expected to experience breaking changes.\n \"\"\"\n trace: JSON\n}\n\n\"\"\"Hex represents viem#Hex.\"\"\"\nscalar Hex\n\n\"\"\"InterfaceId represents an ERC-165 interface id (4-byte hex selector).\"\"\"\nscalar InterfaceId\n\n\"\"\"InterpretedLabel represents an enssdk#InterpretedLabel.\"\"\"\nscalar InterpretedLabel\n\n\"\"\"InterpretedName represents an enssdk#InterpretedName.\"\"\"\nscalar InterpretedName\n\n\"\"\"JSON represents arbitrary JSON-serializable data.\"\"\"\nscalar JSON\n\n\"\"\"\nRepresents a Label within ENS, providing its hash and interpreted representation.\n\"\"\"\ntype Label {\n \"\"\"\n The Label as a BeautifiedLabel: the Interpreted Label beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. An Encoded LabelHash is preserved verbatim. Display-only; use `interpreted` for lookup keys. \n (@see https://ensnode.io/docs/reference/terminology#interpreted-label)\n \"\"\"\n beautified: BeautifiedLabel!\n\n \"\"\"\n The Label's LabelHash\n (@see https://ensnode.io/docs/reference/terminology#labels-labelhashes-labelhash-function)\n \"\"\"\n hash: Hex!\n\n \"\"\"\n The Label represented as an Interpreted Label. This is either a normalized Literal Label or an Encoded LabelHash. \n (@see https://ensnode.io/docs/reference/terminology#interpreted-label)\n \"\"\"\n interpreted: InterpretedLabel!\n}\n\n\"\"\"\nLitecoinAddress represents a Base58Check-encoded Litecoin address (coin type 2).\n\"\"\"\nscalar LitecoinAddress\n\n\"\"\"\nMonacoinAddress represents a Base58Check-encoded Monacoin address (coin type 22).\n\"\"\"\nscalar MonacoinAddress\n\n\"\"\"Constructs a reference to a specific Node via one of `name` or `node`.\"\"\"\ninput NameOrNodeInput @oneOf {\n name: InterpretedName\n node: Node\n}\n\n\"\"\"\nA NameWrapperRegistration represents a Registration initiated by the ENSv1 NameWrapper.\n\"\"\"\ntype NameWrapperRegistration implements Registration {\n \"\"\"The Domain for which this Registration exists.\"\"\"\n domain: Domain!\n\n \"\"\"The Event for which this Registration was created.\"\"\"\n event: Event!\n\n \"\"\"\n Indicates whether this Registration is expired. If the Registration is for an ENSv1Domain, a Registration is only considered `expired` after the Grace Period has elapsed.\n \"\"\"\n expired: Boolean!\n\n \"\"\"A UnixTimestamp indicating the Registration's expiry, if exists.\"\"\"\n expiry: BigInt\n\n \"\"\"The Fuses for this Registration's Domain in the NameWrapper.\"\"\"\n fuses: Int!\n\n \"\"\"A unique reference to this Registration.\"\"\"\n id: RegistrationId!\n\n \"\"\"The extra `referrer` data provided with a Registration, if exists.\"\"\"\n referrer: Hex\n\n \"\"\"\n The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used).\n \"\"\"\n registrant: Account\n\n \"\"\"The Registrar contract under which this Registration is managed.\"\"\"\n registrar: AccountId!\n\n \"\"\"\n Renewals that have occurred within this Registration's lifespan to extend its expiration.\n \"\"\"\n renewals(after: String, before: String, first: Int, last: Int): RegistrationRenewalsConnection\n\n \"\"\"A UnixTimestamp indicating when this Registration was created.\"\"\"\n start: BigInt!\n\n \"\"\"\n The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).\n \"\"\"\n unregistrant: Account\n}\n\n\"\"\"Node represents an enssdk#Node.\"\"\"\nscalar Node\n\n\"\"\"Sort direction\"\"\"\nenum OrderDirection {\n ASC\n DESC\n}\n\ntype PageInfo {\n endCursor: String\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n startCursor: String\n}\n\n\"\"\"Permissions\"\"\"\ntype Permissions {\n \"\"\"The contract within which these Permissions are granted.\"\"\"\n contract: AccountId!\n\n \"\"\"All Events associated with these Permissions.\"\"\"\n events(after: String, before: String, first: Int, last: Int, where: EventsWhereInput): PermissionsEventsConnection\n\n \"\"\"A unique reference to this Permission.\"\"\"\n id: PermissionsId!\n\n \"\"\"All PermissionResources managed by this contract.\"\"\"\n resources(after: String, before: String, first: Int, last: Int): PermissionsResourcesConnection\n\n \"\"\"The Root Resource.\"\"\"\n root: PermissionsResource!\n}\n\ntype PermissionsEventsConnection {\n edges: [PermissionsEventsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype PermissionsEventsConnectionEdge {\n cursor: String!\n node: Event!\n}\n\n\"\"\"PermissionsId represents an enssdk#PermissionsId.\"\"\"\nscalar PermissionsId\n\n\"\"\"Address Permissions by ID or AccountId.\"\"\"\ninput PermissionsIdInput @oneOf {\n contract: AccountIdInput\n id: PermissionsId\n}\n\n\"\"\"PermissionsResource\"\"\"\ntype PermissionsResource {\n \"\"\"The contract within which these Permissions are granted.\"\"\"\n contract: AccountId!\n\n \"\"\"A unique reference to this PermissionsResource.\"\"\"\n id: PermissionsResourceId!\n\n \"\"\"The Permissions within which this Resource is managed.\"\"\"\n permissions: Permissions!\n\n \"\"\"Identifies the Resource that this PermissionsResource represents.\"\"\"\n resource: BigInt!\n\n \"\"\"The PermissionUsers who have Roles within this Resource.\"\"\"\n users(after: String, before: String, first: Int, last: Int): PermissionsResourceUsersConnection\n}\n\n\"\"\"PermissionsResourceId represents an enssdk#PermissionsResourceId.\"\"\"\nscalar PermissionsResourceId\n\ntype PermissionsResourceUsersConnection {\n edges: [PermissionsResourceUsersConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype PermissionsResourceUsersConnectionEdge {\n cursor: String!\n node: PermissionsUser!\n}\n\ntype PermissionsResourcesConnection {\n edges: [PermissionsResourcesConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype PermissionsResourcesConnectionEdge {\n cursor: String!\n node: PermissionsResource!\n}\n\n\"\"\"PermissionsUser\"\"\"\ntype PermissionsUser {\n \"\"\"The contract within which these Permissions are granted.\"\"\"\n contract: AccountId!\n\n \"\"\"All Events associated with this PermissionsUser.\"\"\"\n events(after: String, before: String, first: Int, last: Int, where: EventsWhereInput): PermissionsUserEventsConnection\n\n \"\"\"A unique reference to this PermissionsUser.\"\"\"\n id: PermissionsUserId!\n\n \"\"\"The Resource that this user has Roles within.\"\"\"\n resource: BigInt!\n\n \"\"\"The Roles this User has been granted within this Resource.\"\"\"\n roles: BigInt!\n\n \"\"\"\n The user/grantee address this Permission is granted to (the HCA account address if used).\n \"\"\"\n user: Account!\n}\n\ntype PermissionsUserEventsConnection {\n edges: [PermissionsUserEventsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype PermissionsUserEventsConnectionEdge {\n cursor: String!\n node: Event!\n}\n\n\"\"\"PermissionsUserId represents an enssdk#PermissionsUserId.\"\"\"\nscalar PermissionsUserId\n\n\"\"\"\nSelect a primary name lookup target. Exactly one of `coinType` or `chainName` must be provided.\n\"\"\"\ninput PrimaryNameByInput @oneOf {\n \"\"\"A `ChainName` to resolve the primary name for.\"\"\"\n chainName: ChainName\n\n \"\"\"The ENSIP-9 coin type to resolve the primary name for.\"\"\"\n coinType: CoinType\n}\n\n\"\"\"An ENSIP-19 primary name for an Account on a specific coin type.\"\"\"\ntype PrimaryNameRecord {\n \"\"\"\n The chain corresponding to `coinType`, or null when `coinType` is not represented in `ChainName`.\n \"\"\"\n chainName: ChainName\n\n \"\"\"The canonical ENSIP-9 coin type for this primary name lookup.\"\"\"\n coinType: CoinType!\n\n \"\"\"\n The validated primary name for this Account on this coin type, or null if none is set.\n \"\"\"\n name: CanonicalName\n\n \"\"\"Forward resolve data for this primary name.\"\"\"\n resolve: ForwardResolve!\n}\n\n\"\"\"\nFilter primary name lookups. Exactly one of `coinTypes` or `chainNames` must be provided.\n\"\"\"\ninput PrimaryNamesWhereInput @oneOf {\n \"\"\"`ChainName` values to resolve primary names for.\"\"\"\n chainNames: [ChainName!]\n\n \"\"\"Coin types to resolve primary names for.\"\"\"\n coinTypes: [CoinType!]\n}\n\n\"\"\"The interpreted addresses on the profile of an ENS name.\"\"\"\ntype ProfileAddresses {\n \"\"\"\n The interpreted Base address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n base: Address\n\n \"\"\"\n The interpreted Binance Chain (BNB) address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n binance: BinanceAddress\n\n \"\"\"\n The interpreted Bitcoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n bitcoin: BitcoinAddress\n\n \"\"\"\n The interpreted Bitcoin Cash address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n bitcoincash: BitcoinCashAddress\n\n \"\"\"\n The interpreted Dogecoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n dogecoin: DogecoinAddress\n\n \"\"\"\n The interpreted Ethereum address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n ethereum: Address\n\n \"\"\"\n The interpreted Litecoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n litecoin: LitecoinAddress\n\n \"\"\"\n The interpreted Monacoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n monacoin: MonacoinAddress\n\n \"\"\"\n The interpreted Ripple (XRP) address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n ripple: RippleAddress\n\n \"\"\"\n The interpreted Rootstock (RBTC) address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n rootstock: RootstockAddress\n\n \"\"\"\n The interpreted Solana address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.\n \"\"\"\n solana: SolanaAddress\n}\n\n\"\"\"The interpreted avatar image on the profile of an ENS name.\"\"\"\ntype ProfileAvatar {\n \"\"\"\n HTTP-compatible URL for fetching the avatar image in web browsers. Abstraction over the raw avatar record (IPFS, CAIP NFT references, etc.). Returns null when the raw value is not a direct http(s) URL and no fallback URL can be derived (including when the ENS Metadata Service is unavailable for this namespace). See https://docs.ens.domains/ensip/12.\n \"\"\"\n httpUrl: String\n}\n\n\"\"\"The interpreted header image on the profile of an ENS name.\"\"\"\ntype ProfileHeader {\n \"\"\"\n HTTP-compatible URL for fetching the header image in web browsers. Abstraction over the raw header record (IPFS, CAIP NFT references, etc.). Returns null when the raw value is not a direct http(s) URL and no fallback URL can be derived (including when the ENS Metadata Service is unavailable for this namespace). See https://docs.ens.domains/ensip/12.\n \"\"\"\n httpUrl: String\n}\n\n\"\"\"An interpreted social account on the profile of an ENS name.\"\"\"\ntype ProfileSocialAccount {\n \"\"\"The handle of the social account.\"\"\"\n handle: String!\n\n \"\"\"The HTTP-compatible url to the social account.\"\"\"\n httpUrl: String!\n}\n\n\"\"\"The interpreted social accounts on the profile of an ENS name.\"\"\"\ntype ProfileSocials {\n \"\"\"\n The interpreted GitHub account. Returns null when the raw record is unset, empty, or cannot be parsed as a GitHub handle or profile URL.\n \"\"\"\n github: ProfileSocialAccount\n\n \"\"\"\n The interpreted Keybase account. Returns null when the raw record is unset, empty, or cannot be parsed as a Keybase handle or profile URL.\n \"\"\"\n keybase: ProfileSocialAccount\n\n \"\"\"\n The interpreted LinkedIn account. Returns null when the raw record is unset, empty, or cannot be parsed as a LinkedIn handle or profile URL.\n \"\"\"\n linkedin: ProfileSocialAccount\n\n \"\"\"\n The interpreted Telegram account. Returns null when the raw record is unset, empty, or cannot be parsed as a Telegram handle or profile URL.\n \"\"\"\n telegram: ProfileSocialAccount\n\n \"\"\"\n The interpreted X (Twitter) account. Returns null when the raw record is unset, empty, or cannot be parsed as a X (Twitter) handle or profile URL.\n \"\"\"\n twitter: ProfileSocialAccount\n}\n\n\"\"\"The interpreted website on the profile of an ENS name.\"\"\"\ntype ProfileWebsite {\n \"\"\"\n The HTTP-compatible website URL. Returns null when the raw url record is unset, empty, not an http(s) URL, or cannot be parsed as a valid URL.\n \"\"\"\n httpUrl: String\n}\n\ntype Query {\n \"\"\"Identify an Account by ID or Address.\"\"\"\n account(by: AccountByInput!): Account\n\n \"\"\"Identify a Domain by Name or DomainId\"\"\"\n domain(by: DomainIdInput!): Domain\n\n \"\"\"\n Find Canonical Domains by Name. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: DomainsWhereInput!): QueryDomainsConnection\n\n \"\"\"Identify Permissions by ID or AccountId.\"\"\"\n permissions(by: PermissionsIdInput!): Permissions\n\n \"\"\"\n Identify a Registry by ID or AccountId. If querying by `contract`, only concrete Registries will be returned.\n \"\"\"\n registry(by: RegistryIdInput!): Registry\n\n \"\"\"Identify a Resolver by ID or AccountId.\"\"\"\n resolver(by: ResolverIdInput!): Resolver\n\n \"\"\"\n The Root Registry for this namespace. It will be the ENSv2 Root Registry when defined, otherwise the ENSv1 Root Registry.\n \"\"\"\n root: Registry!\n}\n\ntype QueryDomainsConnection {\n edges: [QueryDomainsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype QueryDomainsConnectionEdge {\n cursor: String!\n node: Domain!\n}\n\n\"\"\"\nA Registration represents a Domain's registration status within the various registries.\n\"\"\"\ninterface Registration {\n \"\"\"The Domain for which this Registration exists.\"\"\"\n domain: Domain!\n\n \"\"\"The Event for which this Registration was created.\"\"\"\n event: Event!\n\n \"\"\"\n Indicates whether this Registration is expired. If the Registration is for an ENSv1Domain, a Registration is only considered `expired` after the Grace Period has elapsed.\n \"\"\"\n expired: Boolean!\n\n \"\"\"A UnixTimestamp indicating the Registration's expiry, if exists.\"\"\"\n expiry: BigInt\n\n \"\"\"A unique reference to this Registration.\"\"\"\n id: RegistrationId!\n\n \"\"\"The extra `referrer` data provided with a Registration, if exists.\"\"\"\n referrer: Hex\n\n \"\"\"\n The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used).\n \"\"\"\n registrant: Account\n\n \"\"\"The Registrar contract under which this Registration is managed.\"\"\"\n registrar: AccountId!\n\n \"\"\"\n Renewals that have occurred within this Registration's lifespan to extend its expiration.\n \"\"\"\n renewals(after: String, before: String, first: Int, last: Int): RegistrationRenewalsConnection\n\n \"\"\"A UnixTimestamp indicating when this Registration was created.\"\"\"\n start: BigInt!\n\n \"\"\"\n The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).\n \"\"\"\n unregistrant: Account\n}\n\n\"\"\"RegistrationId represents an enssdk#RegistrationId.\"\"\"\nscalar RegistrationId\n\ntype RegistrationRenewalsConnection {\n edges: [RegistrationRenewalsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype RegistrationRenewalsConnectionEdge {\n cursor: String!\n node: Renewal!\n}\n\n\"\"\"\nA Registry represents a Registry contract in the ENS namegraph. It may be an ENSv1Registry (a concrete ENSv1 Registry contract), an ENSv1VirtualRegistry (the virtual Registry managed by an ENSv1 domain that has children), or an ENSv2Registry.\n\"\"\"\ninterface Registry {\n \"\"\"Whether the Registry is Canonical.\"\"\"\n canonical: Boolean!\n\n \"\"\"\n Contract metadata for this Registry. If this is an ENSv1VirtualRegistry, this will reference the concrete Registry contract under which the parent Domain exists.\n \"\"\"\n contract: AccountId!\n\n \"\"\"\n The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection\n\n \"\"\"A unique reference to this Registry.\"\"\"\n id: RegistryId!\n\n \"\"\"The Domains for which this Registry is a Subregistry.\"\"\"\n parents(after: String, before: String, first: Int, last: Int): RegistryParentsConnection\n\n \"\"\"The Permissions managed by this Registry.\"\"\"\n permissions: Permissions\n}\n\ntype RegistryDomainsConnection {\n edges: [RegistryDomainsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype RegistryDomainsConnectionEdge {\n cursor: String!\n node: Domain!\n}\n\n\"\"\"Filter for Registry.domains query.\"\"\"\ninput RegistryDomainsWhereInput {\n \"\"\"If set, filters the set of Domains in this Registry by name.\"\"\"\n name: DomainsNameFilter\n}\n\n\"\"\"RegistryId represents an enssdk#RegistryId.\"\"\"\nscalar RegistryId\n\n\"\"\"Address a Registry by ID or AccountId.\"\"\"\ninput RegistryIdInput @oneOf {\n contract: AccountIdInput\n id: RegistryId\n}\n\ntype RegistryParentsConnection {\n edges: [RegistryParentsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype RegistryParentsConnectionEdge {\n cursor: String!\n node: Domain!\n}\n\ntype RegistryPermissionsUser {\n \"\"\"A unique reference to this RegistryPermissionsUser.\"\"\"\n id: PermissionsUserId!\n\n \"\"\"The Registry in which this Permission is granted.\"\"\"\n registry: Registry!\n\n \"\"\"The Resource for which this Permission is granted.\"\"\"\n resource: BigInt!\n\n \"\"\"The Roles that this Permission grants.\"\"\"\n roles: BigInt!\n\n \"\"\"\n The user/grantee address this Permission is granted to (the HCA account address if used).\n \"\"\"\n user: Account!\n}\n\n\"\"\"A Renewal represents an extension of a Registration's expiry.\"\"\"\ntype Renewal {\n \"\"\"The `base` cost of a Renewal, in wei, if exists.\"\"\"\n base: BigInt\n\n \"\"\"The duration for which a Registration was extended.\"\"\"\n duration: BigInt!\n\n \"\"\"The Event for which this Renewal was created.\"\"\"\n event: Event!\n\n \"\"\"A unique reference to this Renewal.\"\"\"\n id: RenewalId!\n\n \"\"\"The `premium` cost of a Renewal, in wei, if exists.\"\"\"\n premium: BigInt\n\n \"\"\"The extra `referrer` data provided with a Renewal, if exists.\"\"\"\n referrer: Hex\n}\n\n\"\"\"RenewalId represents an enssdk#RenewalId.\"\"\"\nscalar RenewalId\n\n\"\"\"A resolved ABI record for an ENS name.\"\"\"\ntype ResolvedAbiRecord {\n contentType: BigInt!\n data: Hex!\n}\n\n\"\"\"A resolved address record for an ENS name.\"\"\"\ntype ResolvedAddressRecord {\n \"\"\"\n The \"raw\" resolved address record as hex, or null if not set, empty (\"0x\"), or zeroAddress. Decode with ENSIP-9 (https://docs.ens.domains/ensip/9) and address-encoder (https://github.com/ensdomains/address-encoder) for the associated coin type. Guaranteed to be at least one byte of hex data. There is no guarantee that an EVM CoinType returns an address value of any particular byte length.\n \"\"\"\n address: Hex\n\n \"\"\"The coin type for this address record.\"\"\"\n coinType: CoinType!\n}\n\n\"\"\"A resolved ERC-165 interface implementer record for an ENS name.\"\"\"\ntype ResolvedInterfaceRecord {\n implementer: Address\n interfaceId: InterfaceId!\n}\n\n\"\"\"A resolved PubkeyResolver (x, y) pair for an ENS name.\"\"\"\ntype ResolvedPubkeyRecord {\n x: Hex!\n y: Hex!\n}\n\n\"\"\"\nA resolved 'raw' text record for an ENS name. Value is any possible string and may require additional validation or preprocessing before use.\n\"\"\"\ntype ResolvedRawTextRecord {\n \"\"\"The text record key.\"\"\"\n key: String!\n\n \"\"\"\n The 'raw' text record value, or null if not set. Value is any possible string and may require additional validation or preprocessing before use.\n \"\"\"\n value: String\n}\n\n\"\"\"Records resolved for a specific ENS name via the ENS protocol.\"\"\"\ntype ResolvedRecords {\n \"\"\"\n The first stored ABI matching the requested content-type bitmask, or null if not set.\n \"\"\"\n abi(\n \"\"\"\n Content-type bitmask; the resolver returns the first stored ABI whose bit is set (lowest bit first).\n \"\"\"\n contentTypeMask: BigInt!\n ): ResolvedAbiRecord\n\n \"\"\"Resolved address records for the requested coin types.\"\"\"\n addresses(\n \"\"\"Coin types to resolve (e.g. `60` for ETH).\"\"\"\n coinTypes: [CoinType!]!\n ): [ResolvedAddressRecord!]!\n\n \"\"\"The ENSIP-7 contenthash record raw bytes, or null if not set.\"\"\"\n contenthash: Hex\n\n \"\"\"The IDNSZoneResolver zonehash raw bytes, or null if not set.\"\"\"\n dnszonehash: Hex\n\n \"\"\"Resolved ERC-165 interface implementer records for the requested ids.\"\"\"\n interfaces(\n \"\"\"ERC-165 interface ids to resolve (4-byte hex selectors).\"\"\"\n ids: [InterfaceId!]!\n ): [ResolvedInterfaceRecord!]!\n\n \"\"\"The PubkeyResolver (x, y) pair, or null if not set.\"\"\"\n pubkey: ResolvedPubkeyRecord\n\n \"\"\"\n The `name` record value used in Reverse Resolution (ENSIP-19), or null if not set. To reduce a common point of developer confusion the Omnigraph API represents this as the `reverseName` rather than the `name` record which is what this field actually resolves to onchain.\n \"\"\"\n reverseName: String\n\n \"\"\"Resolved text records for the requested keys.\"\"\"\n texts(\n \"\"\"Text record keys to resolve (e.g. `avatar`, `description`).\"\"\"\n keys: [String!]!\n ): [ResolvedRawTextRecord!]!\n\n \"\"\"The IVersionableResolver version, or null if not set or unavailable.\"\"\"\n version: BigInt\n}\n\n\"\"\"A Resolver represents a Resolver contract on-chain.\"\"\"\ntype Resolver {\n \"\"\"\n If Resolver is a Bridged Resolver, the Registry to which it Bridges resolution.\n \"\"\"\n bridged: Registry\n\n \"\"\"Contract metadata for this Resolver.\"\"\"\n contract: AccountId!\n\n \"\"\"All Events associated with this Resolver.\"\"\"\n events(after: String, before: String, first: Int, last: Int, where: EventsWhereInput): ResolverEventsConnection\n\n \"\"\"\n Whether this Resolver implements ENSIP-10 wildcard resolution (`IExtendedResolver`, interfaceId `0x9061b923`), determined via a single cached `supportsInterface` RPC the first time the Resolver is observed.\n \"\"\"\n extended: Boolean!\n\n \"\"\"A unique reference to this Resolver.\"\"\"\n id: ResolverId!\n\n \"\"\"Permissions granted by this Resolver.\"\"\"\n permissions: Permissions\n\n \"\"\"ResolverRecords issued by this Resolver.\"\"\"\n records(after: String, before: String, first: Int, last: Int): ResolverRecordsConnection\n\n \"\"\"Identify a ResolverRecord by `name` or `node`.\"\"\"\n records_(by: NameOrNodeInput!): ResolverRecords\n}\n\ntype ResolverEventsConnection {\n edges: [ResolverEventsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype ResolverEventsConnectionEdge {\n cursor: String!\n node: Event!\n}\n\n\"\"\"ResolverId represents an enssdk#ResolverId.\"\"\"\nscalar ResolverId\n\n\"\"\"Address a Resolver by ID or AccountId.\"\"\"\ninput ResolverIdInput @oneOf {\n contract: AccountIdInput\n id: ResolverId\n}\n\ntype ResolverPermissionsUser {\n \"\"\"A unique reference to this ResolverPermissionsUser.\"\"\"\n id: PermissionsUserId!\n\n \"\"\"The Resolver in which this Permission is granted.\"\"\"\n resolver: Resolver!\n\n \"\"\"The Resource for which this Permission is granted.\"\"\"\n resource: BigInt!\n\n \"\"\"The Roles that this Permission grants.\"\"\"\n roles: BigInt!\n\n \"\"\"\n The user/grantee address this Permission is granted to (the HCA account address if used).\n \"\"\"\n user: Account!\n}\n\n\"\"\"ResolverRecords represents the _indexed_ records within a Resolver.\"\"\"\ntype ResolverRecords {\n \"\"\"Unique CoinTypes of `addr` records for this `node`.\"\"\"\n coinTypes: [CoinType!]!\n\n \"\"\"A unique reference to these ResolverRecords.\"\"\"\n id: ResolverRecordsId!\n\n \"\"\"Unique keys of `text` records for this `node`.\"\"\"\n keys: [String!]!\n\n \"\"\"The `name` record for this `node`, if any.\"\"\"\n name: String\n\n \"\"\"The Node for which these ResolverRecords are issued.\"\"\"\n node: Node!\n}\n\ntype ResolverRecordsConnection {\n edges: [ResolverRecordsConnectionEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype ResolverRecordsConnectionEdge {\n cursor: String!\n node: ResolverRecords!\n}\n\n\"\"\"ResolverRecordsId represents an enssdk#ResolverRecordsId.\"\"\"\nscalar ResolverRecordsId\n\n\"\"\"Nested account resolution container exposing primary name resolution.\"\"\"\ntype ReverseResolve {\n \"\"\"\n Whether protocol acceleration was requested and attempted for this reverse resolution.\n \"\"\"\n acceleration: AccelerationStatus!\n\n \"\"\"\n The primary name for this Account on a specific coin type or chain name.\n \"\"\"\n primaryName(\n \"\"\"Select a coin type or chain name to resolve a primary name for.\"\"\"\n by: PrimaryNameByInput!\n ): PrimaryNameRecord!\n\n \"\"\"\n Primary names for this Account on the requested coin types or chain names.\n \"\"\"\n primaryNames(\n \"\"\"Select coin types or chain names to resolve primary names for.\"\"\"\n where: PrimaryNamesWhereInput!\n ): [PrimaryNameRecord!]!\n\n \"\"\"\n Protocol trace tree emitted by reverse resolution, represented as JSON for schema stability. This data model should be expected to experience breaking changes.\n \"\"\"\n trace: JSON!\n}\n\n\"\"\"\nRippleAddress represents a Base58Check-encoded Ripple (XRP) address (coin type 144).\n\"\"\"\nscalar RippleAddress\n\n\"\"\"\nRootstockAddress represents an EIP-55 checksummed Rootstock (RBTC) address (coin type 137).\n\"\"\"\nscalar RootstockAddress\n\n\"\"\"\nSolanaAddress represents a Base58-encoded Solana address (coin type 501).\n\"\"\"\nscalar SolanaAddress\n\n\"\"\"Filter for Domain.subdomains query.\"\"\"\ninput SubdomainsWhereInput {\n \"\"\"If set, filters the set of subdomains by name.\"\"\"\n name: DomainsNameFilter\n}\n\n\"\"\"ThreeDNSRegistration represents a Registration within ThreeDNSToken.\"\"\"\ntype ThreeDNSRegistration implements Registration {\n \"\"\"The Domain for which this Registration exists.\"\"\"\n domain: Domain!\n\n \"\"\"The Event for which this Registration was created.\"\"\"\n event: Event!\n\n \"\"\"\n Indicates whether this Registration is expired. If the Registration is for an ENSv1Domain, a Registration is only considered `expired` after the Grace Period has elapsed.\n \"\"\"\n expired: Boolean!\n\n \"\"\"A UnixTimestamp indicating the Registration's expiry, if exists.\"\"\"\n expiry: BigInt\n\n \"\"\"A unique reference to this Registration.\"\"\"\n id: RegistrationId!\n\n \"\"\"The extra `referrer` data provided with a Registration, if exists.\"\"\"\n referrer: Hex\n\n \"\"\"\n The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used).\n \"\"\"\n registrant: Account\n\n \"\"\"The Registrar contract under which this Registration is managed.\"\"\"\n registrar: AccountId!\n\n \"\"\"\n Renewals that have occurred within this Registration's lifespan to extend its expiration.\n \"\"\"\n renewals(after: String, before: String, first: Int, last: Int): RegistrationRenewalsConnection\n\n \"\"\"A UnixTimestamp indicating when this Registration was created.\"\"\"\n start: BigInt!\n\n \"\"\"\n The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).\n \"\"\"\n unregistrant: Account\n}\n\n\"\"\"\nA resolvable-but-unindexed Domain: not present in the index, but resolvable because an ancestor in its namegraph path has an ENSIP-10 wildcard Resolver (e.g. off-chain / CCIP-Read names, unindexed 3DNS names, wildcard subnames).\n\"\"\"\ntype UnindexedDomain implements Domain {\n \"\"\"\n Metadata (name, path, and node) related to the Domain's canonicality, if known. Null when the Domain is not in the canonical nametree.\n \"\"\"\n canonical: DomainCanonical\n\n \"\"\"All Events associated with this Domain.\"\"\"\n events(after: String, before: String, first: Int, last: Int, where: EventsWhereInput): DomainEventsConnection\n\n \"\"\"A unique and stable reference to this Domain.\"\"\"\n id: DomainId!\n\n \"\"\"The Label associated with this Domain in the ENS Namegraph.\"\"\"\n label: Label!\n\n \"\"\"\n If this is an ENSv1Domain, this is the effective owner of the Domain (derived from the Registry, the Registrar, or the NameWrapper, in that order). If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).\n \"\"\"\n owner: Account\n\n \"\"\"\n The Domain that this Domain's parent Registry declares as its Canonical Domain, if any. Follows a single unidirectional pointer (`Registry.canonicalDomainId`) and does NOT enforce bidirectional canonical-edge agreement: a non-canonical Domain may have a non-null `parent`, and a canonical Domain's `parent` may itself be non-canonical. Null when the parent Registry does not declare a Canonical Domain. For an UnindexedDomain (which has no Registry of its own), this reflects the wildcard-bearing ancestor's Registry — see `Domain.registry`.\n \"\"\"\n parent: Domain\n\n \"\"\"The latest Registration for this Domain, if exists.\"\"\"\n registration: Registration\n\n \"\"\"All Registrations for a Domain, including the latest Registration.\"\"\"\n registrations(after: String, before: String, first: Int, last: Int): DomainRegistrationsConnection\n\n \"\"\"\n The Registry under which this Domain exists. For an UnindexedDomain — a resolvable-but-unindexed Domain that has no Registry of its own — this is instead the Registry that manages the ancestor Domain bearing the wildcard Resolver (the same Registry encoded in its `id`).\n \"\"\"\n registry: Registry!\n\n \"\"\"Resolve protocol-level data for this Domain.\"\"\"\n resolve(\n \"\"\"\n When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.\n @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration\n \"\"\"\n accelerate: Boolean = true\n ): ForwardResolve!\n\n \"\"\"Resolver relationship metadata for this Domain.\"\"\"\n resolver: DomainResolver!\n\n \"\"\"\n All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.\n \"\"\"\n subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection\n\n \"\"\"The Registry this Domain declares as its Subregistry, if exists.\"\"\"\n subregistry: Registry\n}\n\n\"\"\"\nAdditional metadata for BaseRegistrar Registrations wrapped by the NameWrapper (i.e. in the case of a wrapped .eth name)\n\"\"\"\ntype WrappedBaseRegistrarRegistration {\n \"\"\"The Fuses for this Registration's Domain in the NameWrapper.\"\"\"\n fuses: Int!\n\n \"\"\"\n The TokenId for this Domain in the NameWrapper. This is the bigint encoding of the Domain's Node.\n \"\"\"\n tokenId: BigInt!\n}"; diff --git a/packages/ensnode-sdk/src/omnigraph-api/schema-reference.test.ts b/packages/ensnode-sdk/src/omnigraph-api/schema-reference.test.ts new file mode 100644 index 000000000..de40b81f1 --- /dev/null +++ b/packages/ensnode-sdk/src/omnigraph-api/schema-reference.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; + +import { buildCondensedSchemaReference, lookupOmnigraphSchema } from "./schema-reference"; + +describe("omnigraph schema reference", () => { + it("builds a condensed schema reference with core entry points", () => { + const reference = buildCondensedSchemaReference(); + expect(reference).toContain("account(by: AccountByInput!)"); + expect(reference).toContain("#### Account"); + expect(reference).toContain("resolve(accelerate: Boolean): ReverseResolve!"); + expect(reference).toContain("#### DomainsNameFilter"); + expect(reference).toContain("starts_with:"); + }); + + it("searches schema fields by keyword", () => { + const result = lookupOmnigraphSchema({ search: "primaryName" }) as { fields: string[] }; + expect(result.fields).toContain("ReverseResolve.primaryName"); + }); + + it("describes a type by name", () => { + const result = lookupOmnigraphSchema({ type: "Account" }) as { + name: string; + fields: unknown[]; + }; + expect(result.name).toBe("Account"); + expect(result.fields.length).toBeGreaterThan(0); + }); + + it("reports unknown types for Type.field lookups", () => { + expect(() => lookupOmnigraphSchema({ type: "NotAType.field" })).toThrow( + /Unknown type "NotAType"/, + ); + }); + + it("reports missing fields with a schema lookup hint", () => { + expect(() => lookupOmnigraphSchema({ type: "Account.notAField" })).toThrow( + /has no field "notAField"/, + ); + }); + + it("rejects Type.field lookups with extra dot-separated segments", () => { + expect(() => lookupOmnigraphSchema({ type: "Account.resolve.extra" })).toThrow( + /Invalid `type`/, + ); + }); + + it("trims type and search before lookup", () => { + const byType = lookupOmnigraphSchema({ type: " Account " }) as { name: string }; + expect(byType.name).toBe("Account"); + + const bySearch = lookupOmnigraphSchema({ search: " primaryName " }) as { fields: string[] }; + expect(bySearch.fields).toContain("ReverseResolve.primaryName"); + + const byFieldPath = lookupOmnigraphSchema({ type: " Account.resolve " }) as { + parent: string; + name: string; + }; + expect(byFieldPath.parent).toBe("Account"); + expect(byFieldPath.name).toBe("resolve"); + }); + + it("rejects type and search together", () => { + expect(() => lookupOmnigraphSchema({ type: "Account", search: "resolve" })).toThrow( + /Provide either `type` or `search`/, + ); + }); +}); diff --git a/packages/ensnode-sdk/src/omnigraph-api/schema-reference.ts b/packages/ensnode-sdk/src/omnigraph-api/schema-reference.ts new file mode 100644 index 000000000..8aa44295a --- /dev/null +++ b/packages/ensnode-sdk/src/omnigraph-api/schema-reference.ts @@ -0,0 +1,295 @@ +import { + buildSchema, + type GraphQLArgument, + type GraphQLField, + type GraphQLInputField, + type GraphQLNamedType, + type GraphQLSchema, + isEnumType, + isInputObjectType, + isInterfaceType, + isObjectType, + isUnionType, +} from "graphql"; +import { z } from "zod/v4"; + +import { makeOptionalTrimmedNonEmptyStringSchema } from "../shared/zod-schemas"; + +import { omnigraphSchemaSdl } from "./generated/schema-sdl"; + +/** Types rendered with full field listings in the condensed schema reference. */ +export const OMNIGRAPH_CORE_TYPES = [ + "Domain", + "DomainCanonical", + "Account", + "Resolver", + "DomainResolver", + "Registry", + "Permissions", + "ReverseResolve", + "ForwardResolve", + "ResolvedRecords", + "PrimaryNameRecord", + "ProfileSocials", +] as const; + +/** Filter input types listed in the condensed schema reference (snake_case field names). */ +export const OMNIGRAPH_FILTER_INPUT_TYPES = ["DomainsNameFilter", "SubdomainsWhereInput"] as const; + +let cachedSchema: GraphQLSchema | undefined; + +/** Loads the Omnigraph schema from SDL bundled with ensnode-sdk (no network). */ +export function getOmnigraphSchema(): GraphQLSchema { + cachedSchema ??= buildSchema(omnigraphSchemaSdl); + return cachedSchema; +} + +export interface OmnigraphSchemaArgInfo { + name: string; + type: string; + description: string | null; +} + +export interface OmnigraphSchemaFieldInfo { + name: string; + type: string; + description: string | null; + args?: OmnigraphSchemaArgInfo[]; +} + +function oneLine(description: string | null | undefined): string { + return description ? description.replace(/\s+/g, " ").trim() : ""; +} + +function argInfo(arg: GraphQLArgument): OmnigraphSchemaArgInfo { + return { name: arg.name, type: arg.type.toString(), description: arg.description ?? null }; +} + +function fieldInfo( + field: GraphQLField | GraphQLInputField, +): OmnigraphSchemaFieldInfo { + const args = "args" in field && field.args.length > 0 ? field.args.map(argInfo) : undefined; + return { + name: field.name, + type: field.type.toString(), + description: field.description ?? null, + ...(args ? { args } : {}), + }; +} + +function renderFieldLine(field: GraphQLField): string { + const args = + field.args.length > 0 ? `(${field.args.map((a) => `${a.name}: ${a.type}`).join(", ")})` : ""; + const description = oneLine(field.description); + return `- ${field.name}${args}: ${field.type}${description ? ` — ${description}` : ""}`; +} + +function renderInputFieldLine(field: GraphQLInputField): string { + const description = oneLine(field.description); + return `- ${field.name}: ${field.type}${description ? ` — ${description}` : ""}`; +} + +function renderTypeMarkdown(type: GraphQLNamedType): string { + if (!isObjectType(type) && !isInterfaceType(type)) return ""; + const lines = [`#### ${type.name}`]; + if (type.description) lines.push(`_${oneLine(type.description)}_`); + for (const field of Object.values(type.getFields())) lines.push(renderFieldLine(field)); + return lines.join("\n"); +} + +function renderInputTypeMarkdown(type: GraphQLNamedType): string { + if (!isInputObjectType(type)) return ""; + const lines = [`#### ${type.name}`]; + if (type.description) lines.push(`_${oneLine(type.description)}_`); + for (const field of Object.values(type.getFields())) lines.push(renderInputFieldLine(field)); + return lines.join("\n"); +} + +function describeType(type: GraphQLNamedType) { + const base = { name: type.name, description: type.description ?? null }; + if (isObjectType(type) || isInterfaceType(type)) { + return { + ...base, + kind: isObjectType(type) ? "object" : "interface", + fields: Object.values(type.getFields()).map(fieldInfo), + }; + } + if (isInputObjectType(type)) { + return { ...base, kind: "input", fields: Object.values(type.getFields()).map(fieldInfo) }; + } + if (isEnumType(type)) { + return { + ...base, + kind: "enum", + values: type.getValues().map((value) => ({ + name: value.name, + description: value.description ?? null, + })), + }; + } + if (isUnionType(type)) { + return { ...base, kind: "union", types: type.getTypes().map((member) => member.name) }; + } + return { ...base, kind: "scalar" }; +} + +function describeRoot(schema: GraphQLSchema) { + const queryType = schema.getQueryType(); + const queryFields = queryType ? Object.values(queryType.getFields()).map(fieldInfo) : []; + const types = listMajorTypeNames(schema); + return { query: queryFields, types }; +} + +function listMajorTypeNames(schema: GraphQLSchema): string[] { + const queryType = schema.getQueryType(); + return Object.values(schema.getTypeMap()) + .filter((type) => isObjectType(type) && !type.name.startsWith("__")) + .map((type) => type.name) + .filter( + (name) => + name !== queryType?.name && + !name.endsWith("Connection") && + !name.endsWith("ConnectionEdge") && + !name.endsWith("Edge") && + !name.endsWith("Payload"), + ) + .sort(); +} + +function describeFieldPath(schema: GraphQLSchema, typeName: string, fieldName: string) { + const type = schema.getType(typeName); + if (!type) { + throw new Error( + `Unknown type "${typeName}". Run enscli ensnode omnigraph schema (or omnigraph_schema with no args) to list valid types.`, + ); + } + if (!(isObjectType(type) || isInterfaceType(type) || isInputObjectType(type))) { + throw new Error(`Type "${typeName}" has no fields.`); + } + const field = type.getFields()[fieldName]; + if (!field) { + throw new Error( + `Type "${typeName}" has no field "${fieldName}". Run enscli ensnode omnigraph schema ${typeName} (or omnigraph_schema with type "${typeName}") to list its fields.`, + ); + } + return { parent: typeName, ...fieldInfo(field) }; +} + +function searchSchema(schema: GraphQLSchema, keyword: string) { + const query = keyword.toLowerCase(); + const types: string[] = []; + const fields: string[] = []; + for (const type of Object.values(schema.getTypeMap())) { + if (type.name.startsWith("__")) continue; + if (type.name.toLowerCase().includes(query)) types.push(type.name); + if (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type)) { + for (const field of Object.values(type.getFields())) { + if (field.name.toLowerCase().includes(query)) fields.push(`${type.name}.${field.name}`); + } + } + } + return { search: keyword, types: types.sort(), fields: fields.sort() }; +} + +export type OmnigraphSchemaLookupInput = { + type?: string; + search?: string; +}; + +export const OmnigraphSchemaLookupInputSchema = z + .object({ + type: makeOptionalTrimmedNonEmptyStringSchema(), + search: makeOptionalTrimmedNonEmptyStringSchema(), + }) + .superRefine((value, ctx) => { + if (value.search && value.type) { + ctx.addIssue({ + code: "custom", + message: "Provide either `type` or `search`, not both.", + }); + } + if (value.type?.includes(".")) { + const parts = value.type.split("."); + if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) { + ctx.addIssue({ + code: "custom", + message: 'Invalid `type` — expected "Type.field" (e.g. Account.resolve).', + path: ["type"], + }); + } + } + }); + +/** Looks up Omnigraph schema fields locally. Returns JSON-serializable data. */ +export function lookupOmnigraphSchema(input: OmnigraphSchemaLookupInput): unknown { + const parsed = OmnigraphSchemaLookupInputSchema.parse(input); + const schema = getOmnigraphSchema(); + + if (parsed.search) { + return searchSchema(schema, parsed.search); + } + + if (parsed.type?.includes(".")) { + const [typeName, fieldName] = parsed.type.split(".", 2) as [string, string]; + return describeFieldPath(schema, typeName, fieldName); + } + + if (parsed.type) { + const type = schema.getType(parsed.type); + if (!type) { + throw new Error(`Unknown type "${parsed.type}".`); + } + return describeType(type); + } + + return describeRoot(schema); +} + +/** Condensed Omnigraph schema reference for agent skills and MCP resources. */ +export function buildCondensedSchemaReference( + schema: GraphQLSchema = getOmnigraphSchema(), +): string { + const queryType = schema.getQueryType(); + const sections: string[] = []; + + if (queryType) { + const queryFields = Object.values(queryType.getFields()).map(renderFieldLine); + sections.push(["### Query (entry points)", ...queryFields].join("\n")); + } + + const coreSections: string[] = []; + for (const name of OMNIGRAPH_CORE_TYPES) { + const type = schema.getType(name); + if (type) { + const rendered = renderTypeMarkdown(type); + if (rendered) coreSections.push(rendered); + } + } + sections.push(["### Core types", coreSections.join("\n\n")].join("\n\n")); + + const filterSections: string[] = []; + for (const name of OMNIGRAPH_FILTER_INPUT_TYPES) { + const type = schema.getType(name); + if (type) { + const rendered = renderInputTypeMarkdown(type); + if (rendered) filterSections.push(rendered); + } + } + if (filterSections.length > 0) { + sections.push(["### Filter inputs (snake_case)", filterSections.join("\n\n")].join("\n\n")); + } + + const otherTypes = listMajorTypeNames(schema).filter( + (name) => !OMNIGRAPH_CORE_TYPES.includes(name as (typeof OMNIGRAPH_CORE_TYPES)[number]), + ); + sections.push( + [ + "### Other types", + "Run `npx enscli ensnode omnigraph schema ` for fields of:", + "", + otherTypes.map((name) => `\`${name}\``).join(", "), + ].join("\n"), + ); + + return sections.join("\n\n"); +} diff --git a/packages/ensnode-sdk/src/shared/zod-schemas.test.ts b/packages/ensnode-sdk/src/shared/zod-schemas.test.ts index 100e5492a..f5cda0901 100644 --- a/packages/ensnode-sdk/src/shared/zod-schemas.test.ts +++ b/packages/ensnode-sdk/src/shared/zod-schemas.test.ts @@ -18,6 +18,7 @@ import { makeIntegerSchema, makeNonNegativeIntegerSchema, makeNormalizedAddressSchema, + makeOptionalTrimmedNonEmptyStringSchema, makePositiveIntegerSchema, makePriceSchema, makeReinterpretedNameSchema, @@ -49,6 +50,12 @@ describe("ENSIndexer: Shared", () => { ); }); + it("trims optional strings and treats whitespace-only input as undefined", () => { + expect(makeOptionalTrimmedNonEmptyStringSchema().parse(" hello ")).toBe("hello"); + expect(makeOptionalTrimmedNonEmptyStringSchema().parse(undefined)).toBeUndefined(); + expect(makeOptionalTrimmedNonEmptyStringSchema().parse(" ")).toBeUndefined(); + }); + it("can parse integer values", () => { expect(makeIntegerSchema().parse(-1)).toBe(-1); expect(makeIntegerSchema().parse(0)).toBe(0); diff --git a/packages/ensnode-sdk/src/shared/zod-schemas.ts b/packages/ensnode-sdk/src/shared/zod-schemas.ts index 124a304af..b6ab4d58e 100644 --- a/packages/ensnode-sdk/src/shared/zod-schemas.ts +++ b/packages/ensnode-sdk/src/shared/zod-schemas.ts @@ -48,6 +48,21 @@ export const makeBooleanStringSchema = (valueLabel: string = "Value") => ) .transform((val) => val === "true"); +/** + * Optional string that trims whitespace; blank or whitespace-only input becomes undefined. + */ +export const makeOptionalTrimmedNonEmptyStringSchema = (description?: string) => { + const schema = z + .string() + .optional() + .transform((value) => { + if (value === undefined) return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; + }); + return description ? schema.describe(description) : schema; +}; + /** * Parses a numeric value as a finite non-negative number. */ diff --git a/packages/ensskills/scripts/generate.ts b/packages/ensskills/scripts/generate.ts index 076b480d5..0bdce2f20 100644 --- a/packages/ensskills/scripts/generate.ts +++ b/packages/ensskills/scripts/generate.ts @@ -14,18 +14,11 @@ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { - buildSchema, - type GraphQLField, - type GraphQLNamedType, - type GraphQLSchema, - isInterfaceType, - isObjectType, -} from "graphql"; import prettier from "prettier"; +import { buildCondensedSchemaReference } from "@ensnode/ensnode-sdk/internal"; + const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); -const SDL_PATH = resolve(SCRIPT_DIR, "../../enssdk/src/omnigraph/generated/schema.graphql"); const SKILL_PATH = resolve(SCRIPT_DIR, "../skills/omnigraph/SKILL.md"); const EXAMPLE_QUERIES_PATH = resolve( SCRIPT_DIR, @@ -36,6 +29,8 @@ const ENSCLI_EXAMPLE_COMMANDS_PATH = resolve(SCRIPT_DIR, "../../enscli/src/examp interface ExampleQuery { id: string; + title: string; + description: string; query: string; variables: { default: Record }; } @@ -47,84 +42,6 @@ interface EnscliExample { group: string; } -/** Types rendered with full field listings; everything else is listed by name only. */ -const CORE_TYPES = [ - "Domain", - "DomainCanonical", - "Account", - "Resolver", - "DomainResolver", - "Registry", - "Permissions", - "ReverseResolve", - "ForwardResolve", - "ResolvedRecords", - "PrimaryNameRecord", -]; - -function oneLine(description: string | null | undefined): string { - return description ? description.replace(/\s+/g, " ").trim() : ""; -} - -function renderFieldLine(field: GraphQLField): string { - const args = - field.args.length > 0 ? `(${field.args.map((a) => `${a.name}: ${a.type}`).join(", ")})` : ""; - const description = oneLine(field.description); - return `- ${field.name}${args}: ${field.type}${description ? ` — ${description}` : ""}`; -} - -function renderType(type: GraphQLNamedType): string { - if (!isObjectType(type) && !isInterfaceType(type)) return ""; - const lines = [`#### ${type.name}`]; - if (type.description) lines.push(`_${oneLine(type.description)}_`); - for (const field of Object.values(type.getFields())) lines.push(renderFieldLine(field)); - return lines.join("\n"); -} - -function buildSchemaReference(schema: GraphQLSchema): string { - const queryType = schema.getQueryType(); - const sections: string[] = []; - - if (queryType) { - const queryFields = Object.values(queryType.getFields()).map(renderFieldLine); - sections.push(["### Query (entry points)", ...queryFields].join("\n")); - } - - const coreSections: string[] = []; - for (const name of CORE_TYPES) { - const type = schema.getType(name); - if (type) { - const rendered = renderType(type); - if (rendered) coreSections.push(rendered); - } - } - sections.push(["### Core types", coreSections.join("\n\n")].join("\n\n")); - - const otherTypes = Object.values(schema.getTypeMap()) - .filter((type) => isObjectType(type) && !type.name.startsWith("__")) - .map((type) => type.name) - .filter( - (name) => - name !== queryType?.name && - !CORE_TYPES.includes(name) && - !name.endsWith("Connection") && - !name.endsWith("ConnectionEdge") && - !name.endsWith("Edge") && - !name.endsWith("Payload"), - ) - .sort(); - sections.push( - [ - "### Other types", - "Run `npx enscli ensnode omnigraph schema ` for fields of:", - "", - otherTypes.map((name) => `\`${name}\``).join(", "), - ].join("\n"), - ); - - return sections.join("\n\n"); -} - async function buildExamples(): Promise { // load dynamically to avoid tsconfig root error const { GRAPHQL_API_EXAMPLE_QUERIES } = (await import(EXAMPLE_QUERIES_PATH)) as { @@ -133,10 +50,17 @@ async function buildExamples(): Promise { // Skip "hello-world": it's the playground welcome blurb, not a reusable query pattern. return GRAPHQL_API_EXAMPLE_QUERIES.filter((example) => example.id !== "hello-world") .map((example) => { + if (!example.title?.trim()) { + throw new Error(`GraphQL API example "${example.id}" is missing a title`); + } + if (!example.description?.trim()) { + throw new Error(`GraphQL API example "${example.id}" is missing a description`); + } const query = example.query.trim(); const variables = JSON.stringify(example.variables.default, null, 2); return [ - `### ${example.id}`, + `### ${example.title} (${example.id})`, + example.description, "```graphql", query, "```", @@ -200,9 +124,8 @@ async function writeFormatted(path: string, content: string): Promise { } async function main(): Promise { - const schema = buildSchema(readFileSync(SDL_PATH, "utf8")); let content = readFileSync(SKILL_PATH, "utf8"); - content = replaceRegion(content, "SCHEMA", buildSchemaReference(schema), SKILL_PATH); + content = replaceRegion(content, "SCHEMA", buildCondensedSchemaReference(), SKILL_PATH); content = replaceRegion(content, "EXAMPLES", await buildExamples(), SKILL_PATH); await writeFormatted(SKILL_PATH, content); console.log(`Updated ${SKILL_PATH}`); diff --git a/packages/ensskills/skills/omnigraph/SKILL.md b/packages/ensskills/skills/omnigraph/SKILL.md index 881a92689..ad085e75d 100644 --- a/packages/ensskills/skills/omnigraph/SKILL.md +++ b/packages/ensskills/skills/omnigraph/SKILL.md @@ -218,7 +218,9 @@ These are vetted, copy-pasteable patterns. Adapt the selection set to your needs -### find-domains +### Find Domains (find-domains) + +List domains matching a name prefix with ordering and registration metadata. ```graphql query FindDomains($name: DomainsNameFilter!, $order: DomainsOrderInput) { @@ -264,7 +266,9 @@ Variables: } ``` -### domain-by-name +### Domain By Name (domain-by-name) + +Load a domain by interpreted name, including profile information. ```graphql query DomainByName($name: InterpretedName!) { @@ -297,7 +301,9 @@ Variables: } ``` -### domain-by-name-type-condition +### Domain By Name Type Condition (domain-by-name-type-condition) + +Load a domain by interpreted name with type condition. ```graphql query DomainByName($name: InterpretedName!) { @@ -344,7 +350,9 @@ Variables: } ``` -### domain-registration +### Domain Registration (domain-registration) + +Load a domain registration details. ```graphql query DomainRegistration($name: InterpretedName!) { @@ -410,7 +418,9 @@ Variables: } ``` -### domain-records +### Domain Records (domain-records) + +For given name resolve raw records like `addresses`, `texts`, `contenthash` etc. ```graphql query DomainRecords($name: InterpretedName!) { @@ -445,7 +455,9 @@ Variables: } ``` -### domain-profile +### Domain Profile (domain-profile) + +Load a domain's high-level profile (avatar, socials, addresses, and more). ```graphql query DomainProfile($name: InterpretedName!) { @@ -493,7 +505,9 @@ Variables: } ``` -### domain-subdomains +### Domain Subdomains (domain-subdomains) + +Paginate direct child names under a parent domain. ```graphql query DomainSubdomains($name: InterpretedName!) { @@ -528,7 +542,9 @@ Variables: } ``` -### domain-subdomains-recently-registered +### Recently Registered Subdomains (domain-subdomains-recently-registered) + +List a parent domain's subdomains ordered by most recent registration first. ```graphql query RecentlyRegisteredSubdomains($name: InterpretedName!) { @@ -563,7 +579,9 @@ Variables: } ``` -### subdomains-pagination +### Subdomains Pagination (subdomains-pagination) + +Paginate through all subdomains of a parent domain. ```graphql query SubdomainsPagination($first: Int!, $after: String) { @@ -605,7 +623,9 @@ Variables: } ``` -### domain-events +### Domain Events (domain-events) + +Raw contract events associated with a domain's registry records. ```graphql query DomainEvents($name: InterpretedName!) { @@ -635,7 +655,9 @@ Variables: } ``` -### domains-by-address +### Account Domains (domains-by-address) + +Load domains owned by an address via the Omnigraph `account` root field. ```graphql query AccountDomains($address: Address!) { @@ -667,7 +689,9 @@ Variables: } ``` -### account-primary-name +### Account Primary Name (account-primary-name) + +Load a primary name for an account on Ethereum, including profile information. ```graphql query AccountPrimaryName($address: Address!) { @@ -703,7 +727,9 @@ Variables: } ``` -### account-events +### Account Events (account-events) + +Events touching an account across indexed ENS contracts. ```graphql query AccountEvents($address: Address!) { @@ -730,7 +756,9 @@ Variables: } ``` -### registry-domains +### Registry Domains (registry-domains) + +Enumerate domains under a specific v2 ETH registry contract. ```graphql query RegistryDomains($registry: AccountIdInput!) { @@ -765,7 +793,9 @@ Variables: } ``` -### permissions-by-contract +### Permissions By Contract (permissions-by-contract) + +Roles and users granted on resources for a registrar or registry contract. ```graphql query PermissionsByContract($contract: AccountIdInput!) { @@ -813,7 +843,9 @@ Variables: } ``` -### permissions-by-user +### Permissions By User (permissions-by-user) + +Resources and roles for an address in the permissions graph. ```graphql query PermissionsByUser($address: Address!) { @@ -838,7 +870,9 @@ Variables: } ``` -### account-resolver-permissions +### Account Resolver Permissions (account-resolver-permissions) + +Resolver contracts where an account has been granted resolver ACLs. ```graphql query AccountResolverPermissions($address: Address!) { @@ -866,7 +900,9 @@ Variables: } ``` -### domain-resolver +### Domain Resolver (domain-resolver) + +Assigned resolver contract address and recent resolver events. ```graphql query DomainResolver($name: InterpretedName!) { @@ -899,7 +935,9 @@ Variables: } ``` -### resolver-by-address +### Resolver By Address (resolver-by-address) + +Load a resolver by its contract address. ```graphql query ResolverByAddress($contract: AccountIdInput!) { @@ -948,7 +986,9 @@ Variables: } ``` -### namegraph +### Namegraph (namegraph) + +Walk a domain's registry, parent, subregistry, and direct subdomains (as in Core Concepts). ```graphql query Namegraph { @@ -997,7 +1037,9 @@ Variables: {} ``` -### account-migrated-names +### Account Migration Counts (account-migrated-names) + +Count an account's ENSv1 vs ENSv2 domains to gauge its migration progress. ```graphql query AccountMigratedNames($address: Address!) { @@ -1020,7 +1062,9 @@ Variables: } ``` -### eth-by-version +### ETH TLD By Version (eth-by-version) + +Load the .eth TLD across protocol versions: one Domain per version, discriminated by `__typename` (ENSv1Domain / ENSv2Domain). ```graphql query GetEthDomains { @@ -1041,7 +1085,9 @@ Variables: {} ``` -### accelerate-resolve +### Resolve primary name and records, and track protocol acceleration (accelerate-resolve) + +Resolve primary name and records, and track protocol acceleration with `trace` and `accelerate` arguments. ```graphql query AccelerateResolve($address: Address!) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3dbf3298..9a7d3665a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,6 +373,9 @@ importers: '@ensnode/ponder-subgraph': specifier: workspace:* version: link:../../packages/ponder-subgraph + '@hono/mcp': + specifier: ^0.3.0 + version: 0.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(hono-rate-limiter@0.5.3(hono@4.12.23)(unstorage@1.17.5))(hono@4.12.23)(zod@4.3.6) '@hono/node-server': specifier: 'catalog:' version: 1.19.14(hono@4.12.23) @@ -382,6 +385,9 @@ importers: '@hono/zod-openapi': specifier: ^1.4.0 version: 1.4.0(hono@4.12.23)(zod@4.3.6) + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(zod@4.3.6) '@namehash/ens-referrals': specifier: workspace:* version: link:../../packages/ens-referrals @@ -1147,6 +1153,9 @@ importers: enssdk: specifier: workspace:* version: link:../enssdk + graphql: + specifier: ^16.11.0 + version: 16.11.0 zod: specifier: 'catalog:' version: 4.3.6 @@ -3034,6 +3043,14 @@ packages: peerDependencies: react: '>= 16 || ^19.0.0-rc' + '@hono/mcp@0.3.0': + resolution: {integrity: sha512-UhSWFbZwRxHBdptOn2r1HyB8Vt6gU+BL3AbTis2t8TBW69ZhG4soJQ09yEL/t/ymxszJnWp5Rq6tOXXOjuK1qg==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.1 + hono: '*' + hono-rate-limiter: ^0.5.3 + zod: ^3.25.0 || ^4.0.0 + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -3438,6 +3455,16 @@ packages: '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@n1ru4l/push-pull-async-iterable-iterator@3.2.0': resolution: {integrity: sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==} engines: {node: '>=12'} @@ -5649,6 +5676,10 @@ packages: resolution: {integrity: sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==} engines: {node: '>=12'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -5695,6 +5726,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: '>=8.18.0' + peerDependenciesMeta: + ajv: + optional: true + ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -5938,6 +5977,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -6023,6 +6066,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + camelcase@7.0.1: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} engines: {node: '>=14.16'} @@ -6231,6 +6278,18 @@ packages: resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} engines: {node: '>= 0.6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-hrtime@5.0.0: resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} engines: {node: '>=12'} @@ -6241,6 +6300,14 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -6252,6 +6319,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -6561,6 +6632,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dependency-graph@1.0.0: resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} engines: {node: '>=4'} @@ -6759,6 +6834,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.243: resolution: {integrity: sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==} @@ -6771,6 +6849,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + encoding-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} @@ -6879,6 +6961,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -6912,6 +6997,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -6949,6 +7038,16 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + expressive-code@0.42.0: resolution: {integrity: sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g==} @@ -7033,6 +7132,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -7074,6 +7177,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + framer-motion@12.23.24: resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==} peerDependencies: @@ -7088,6 +7195,10 @@ packages: react-dom: optional: true + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -7365,6 +7476,15 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + hono-rate-limiter@0.5.3: + resolution: {integrity: sha512-M0DxbVMpPELEzLi0AJg1XyBHLGJXz7GySjsPoK+gc5YeeBsdGDGe+2RvVuCAv8ydINiwlbxqYMNxUEyYfRji/A==} + peerDependencies: + hono: ^4.10.8 + unstorage: ^1.17.3 + peerDependenciesMeta: + unstorage: + optional: true + hono@4.12.23: resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} engines: {node: '>=16.9.0'} @@ -7394,6 +7514,10 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -7434,6 +7558,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + identifier-regex@1.0.1: resolution: {integrity: sha512-ZrYyM0sozNPZlvBvE7Oq9Bn44n0qKGrYu5sQ0JzMUnjIhpgWYE2JB6aBoFwEYdPjqj7jPyxXTMJiHDOxDfd8yw==} engines: {node: '>=18'} @@ -7471,6 +7599,14 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -7572,6 +7708,9 @@ packages: resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regexp@3.1.0: resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} engines: {node: '>=12'} @@ -7630,6 +7769,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -7668,6 +7810,9 @@ packages: json-schema-typed@8.0.1: resolution: {integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -8079,6 +8224,14 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -8233,6 +8386,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -8360,6 +8517,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -8443,6 +8604,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -8459,6 +8624,10 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + on-headers@1.1.0: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} @@ -8594,6 +8763,10 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -8626,6 +8799,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -8739,6 +8915,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -8918,6 +9098,10 @@ packages: resolution: {integrity: sha512-JpJpFaR7yKNb6WqKvJJ1MLbiuIQWQnbUUb06nDtf2/i8YWYYLEfP6xf9BwSJoJQg1wAy61EQB8dssQg64oX4aA==} engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -8933,6 +9117,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -8960,6 +9148,14 @@ packages: resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} engines: {node: '>= 0.6'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -9227,6 +9423,10 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -9293,9 +9493,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-handler@6.1.6: resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + serve@14.2.5: resolution: {integrity: sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==} engines: {node: '>= 14'} @@ -9311,6 +9519,9 @@ packages: resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} engines: {node: '>=11.0'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shallow-equal@1.2.1: resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} @@ -9338,6 +9549,22 @@ packages: resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} engines: {node: '>=20'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -9468,6 +9695,10 @@ packages: peerDependencies: '@astrojs/starlight': '>=0.38.0' + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -9743,6 +9974,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -9857,6 +10092,10 @@ packages: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} @@ -9964,6 +10203,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-utils@0.3.1: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} @@ -10546,6 +10789,11 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -12509,6 +12757,14 @@ snapshots: dependencies: react: 19.2.1 + '@hono/mcp@0.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(hono-rate-limiter@0.5.3(hono@4.12.23)(unstorage@1.17.5))(hono@4.12.23)(zod@4.3.6)': + dependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) + hono: 4.12.23 + hono-rate-limiter: 0.5.3(hono@4.12.23)(unstorage@1.17.5) + pkce-challenge: 5.0.1 + zod: 4.3.6 + '@hono/node-server@1.19.14(hono@4.12.23)': dependencies: hono: 4.12.23 @@ -12912,6 +13168,28 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 + '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.23) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.23 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@n1ru4l/push-pull-async-iterable-iterator@3.2.0': {} '@next/env@16.2.6': {} @@ -15565,6 +15843,11 @@ snapshots: module-error: 1.0.2 queue-microtask: 1.2.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -15595,6 +15878,10 @@ snapshots: optionalDependencies: ajv: 8.18.0 + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -16015,6 +16302,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} boring-avatars@1.11.2: {} @@ -16094,6 +16395,11 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + camelcase@7.0.1: {} caniuse-lite@1.0.30001751: {} @@ -16304,12 +16610,22 @@ snapshots: content-disposition@0.5.2: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + convert-hrtime@5.0.0: {} convert-source-map@2.0.0: {} cookie-es@1.2.2: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} copy-anything@4.0.5: @@ -16318,6 +16634,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -16643,6 +16964,8 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + dependency-graph@1.0.0: {} deprecation@2.3.1: {} @@ -16760,6 +17083,8 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + electron-to-chromium@1.5.243: {} emmet@2.4.11: @@ -16771,6 +17096,8 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 @@ -16980,6 +17307,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} esprima@4.0.1: {} @@ -17019,6 +17348,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + event-target-shim@5.0.1: {} eventemitter3@5.0.1: {} @@ -17068,6 +17399,44 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + expressive-code@0.42.0: dependencies: '@expressive-code/core': 0.42.0 @@ -17155,6 +17524,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -17197,6 +17577,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded@0.2.0: {} + framer-motion@12.23.24(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: motion-dom: 12.23.23 @@ -17206,6 +17588,8 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-extra@11.3.3: @@ -17667,6 +18051,12 @@ snapshots: highlight.js@11.11.1: {} + hono-rate-limiter@0.5.3(hono@4.12.23)(unstorage@1.17.5): + dependencies: + hono: 4.12.23 + optionalDependencies: + unstorage: 1.17.5 + hono@4.12.23: {} hookable@5.5.3: {} @@ -17692,6 +18082,14 @@ snapshots: http-cache-semantics@4.2.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -17731,6 +18129,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + identifier-regex@1.0.1: dependencies: reserved-identifiers: 1.2.0 @@ -17761,6 +18163,10 @@ snapshots: internmap@2.0.3: {} + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + iron-webcrypto@1.2.1: {} is-absolute-url@4.0.1: {} @@ -17827,6 +18233,8 @@ snapshots: is-primitive@3.0.1: {} + is-promise@4.0.0: {} + is-regexp@3.1.0: {} is-stream@2.0.1: {} @@ -17869,6 +18277,8 @@ snapshots: jiti@2.6.1: {} + jose@6.2.3: {} + joycon@3.1.1: {} js-base64@3.7.8: {} @@ -17918,6 +18328,8 @@ snapshots: json-schema-typed@8.0.1: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json5@2.2.3: {} @@ -18370,6 +18782,10 @@ snapshots: mdurl@2.0.0: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -18699,6 +19115,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -18813,6 +19233,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + neotraverse@0.6.18: {} neverpanic@0.0.7(typescript@5.9.3): @@ -18884,6 +19306,8 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + obuf@1.1.2: {} obug@2.1.1: {} @@ -18898,6 +19322,10 @@ snapshots: on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + on-headers@1.1.0: {} once@1.4.0: @@ -19052,6 +19480,8 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-data-parser@0.1.0: {} @@ -19073,6 +19503,8 @@ snapshots: path-to-regexp@3.3.0: {} + path-to-regexp@8.4.2: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -19199,6 +19631,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -19451,6 +19885,11 @@ snapshots: dependencies: long: 5.3.2 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-from-env@2.1.0: {} pump@3.0.3: @@ -19462,6 +19901,10 @@ snapshots: punycode@2.3.1: {} + qs@6.15.2: + dependencies: + side-channel: 1.1.1 + quansync@0.2.11: {} quansync@1.0.0: {} @@ -19495,6 +19938,15 @@ snapshots: range-parser@1.2.0: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -19886,6 +20338,16 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.8.0: {} run-parallel@1.2.0: @@ -19933,6 +20395,22 @@ snapshots: semver@7.8.0: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-handler@6.1.6: dependencies: bytes: 3.0.0 @@ -19943,6 +20421,15 @@ snapshots: path-to-regexp: 3.3.0 range-parser: 1.2.0 + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + serve@14.2.5: dependencies: '@zeit/schemas': 2.36.0 @@ -19968,6 +20455,8 @@ snapshots: is-plain-object: 2.0.4 is-primitive: 3.0.1 + setprototypeof@1.2.0: {} + shallow-equal@1.2.1: {} sharp@0.33.5: @@ -20047,6 +20536,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -20188,6 +20705,8 @@ snapshots: '@astrojs/starlight': 0.39.2(astro@6.3.3(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.59.0)(tsx@4.21.0)(yaml@2.8.3))(typescript@5.9.3) picomatch: 4.0.4 + statuses@2.0.2: {} + std-env@4.1.0: {} stream-replace-string@2.0.0: {} @@ -20502,6 +21021,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.17 @@ -20606,6 +21127,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + typesafe-path@0.2.2: {} typescript-auto-import-cache@0.3.6: @@ -20729,6 +21256,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 @@ -21298,6 +21827,10 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 + zod-to-json-schema@3.25.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@4.3.6: {} zustand@4.5.7(@types/react@19.2.7)(immer@9.0.21)(react@19.2.1):