From 19b42ed3ad0fb338c82f783a4c4ae1037d107401 Mon Sep 17 00:00:00 2001 From: djstrong Date: Wed, 29 Apr 2026 21:10:00 +0200 Subject: [PATCH 01/27] feat: add ens-labels-collector app for label submission and classification - Introduced `apps/ens-labels-collector`, a Hono server that accepts label submissions via `POST /api/submissions`. - Implemented classification of labels against ENSNode's Omnigraph index, emitting structured JSON logs for each submission. - Added a new `Query.labels` field in ENSApi for batch lookup of labels by their hashes. - Included necessary configurations, Dockerfile, and example environment variables for local development. - Comprehensive tests for submission handling and classification logic. This feature addresses issue [#2003](https://github.com/namehash/ensnode/issues/2003). --- .changeset/ens-labels-collector-app.md | 11 + apps/ens-labels-collector/.env.local.example | 6 + apps/ens-labels-collector/Dockerfile | 16 + apps/ens-labels-collector/README.md | 50 ++ apps/ens-labels-collector/package.json | 37 ++ apps/ens-labels-collector/src/app.ts | 20 + apps/ens-labels-collector/src/config.ts | 43 ++ .../src/handlers/health.ts | 5 + .../src/handlers/submissions.test.ts | 214 +++++++ .../src/handlers/submissions.ts | 131 ++++ apps/ens-labels-collector/src/index.ts | 43 ++ .../src/lib/error-response.ts | 60 ++ .../src/lib/labels.test.ts | 155 +++++ apps/ens-labels-collector/src/lib/labels.ts | 133 +++++ .../src/lib/omnigraph-client.ts | 64 ++ apps/ens-labels-collector/tsconfig.json | 11 + apps/ens-labels-collector/vitest.config.ts | 15 + .../schema/label.integration.test.ts | 96 +++ apps/ensapi/src/omnigraph-api/schema/label.ts | 23 + apps/ensapi/src/omnigraph-api/schema/query.ts | 33 + .../src/omnigraph/generated/introspection.ts | 52 ++ .../src/omnigraph/generated/schema.graphql | 13 + pnpm-lock.yaml | 565 +++++++++--------- 23 files changed, 1498 insertions(+), 298 deletions(-) create mode 100644 .changeset/ens-labels-collector-app.md create mode 100644 apps/ens-labels-collector/.env.local.example create mode 100644 apps/ens-labels-collector/Dockerfile create mode 100644 apps/ens-labels-collector/README.md create mode 100644 apps/ens-labels-collector/package.json create mode 100644 apps/ens-labels-collector/src/app.ts create mode 100644 apps/ens-labels-collector/src/config.ts create mode 100644 apps/ens-labels-collector/src/handlers/health.ts create mode 100644 apps/ens-labels-collector/src/handlers/submissions.test.ts create mode 100644 apps/ens-labels-collector/src/handlers/submissions.ts create mode 100644 apps/ens-labels-collector/src/index.ts create mode 100644 apps/ens-labels-collector/src/lib/error-response.ts create mode 100644 apps/ens-labels-collector/src/lib/labels.test.ts create mode 100644 apps/ens-labels-collector/src/lib/labels.ts create mode 100644 apps/ens-labels-collector/src/lib/omnigraph-client.ts create mode 100644 apps/ens-labels-collector/tsconfig.json create mode 100644 apps/ens-labels-collector/vitest.config.ts create mode 100644 apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts diff --git a/.changeset/ens-labels-collector-app.md b/.changeset/ens-labels-collector-app.md new file mode 100644 index 0000000000..e16e6b1e2b --- /dev/null +++ b/.changeset/ens-labels-collector-app.md @@ -0,0 +1,11 @@ +--- +"ens-labels-collector": minor +"ensapi": minor +"enssdk": minor +--- + +Add `apps/ens-labels-collector` and a new `Query.labels` Omnigraph field to support label submission collection (issue [#2003](https://github.com/namehash/ensnode/issues/2003)). + +- **New app `apps/ens-labels-collector`**: Hono server exposing `POST /api/submissions` that accepts `{ labels: string[], callerAddress: Address }`, classifies each label against ENSNode's index via the typed `enssdk/omnigraph` client, and emits a structured JSON line per submission to stdout. For each submitted raw label the collector computes both the literal labelhash and (when normalizable to a different value) the normalized labelhash, then assigns one of three statuses per label: `unknown_in_index` (referenced in the index but unhealed), `healed_in_index`, or `absent_from_index`. Persistent storage, batched on-chain emission, and a caller-leaderboard are explicitly deferred to follow-up work; the JSON log shape is the future row shape so adding a sink later is mechanical. +- **New ENSApi `Query.labels(by: { hashes: [Hex!]! }): [Label!]!`**: batch lookup of `Label` rows by `LabelHash`. Hashes that are not present in the index are simply omitted from the result. Capped at 100 hashes per request. +- **`enssdk/omnigraph`**: regenerated GraphQL introspection so the new `Query.labels` field is available to the typed `graphql(...)` client. diff --git a/apps/ens-labels-collector/.env.local.example b/apps/ens-labels-collector/.env.local.example new file mode 100644 index 0000000000..e112c9b7fb --- /dev/null +++ b/apps/ens-labels-collector/.env.local.example @@ -0,0 +1,6 @@ +# Port the ens-labels-collector HTTP server listens on. +PORT=4444 + +# Base URL of an ENSNode (ENSApi) instance that exposes the Omnigraph GraphQL endpoint. +# The collector calls `${ENSNODE_URL}/api/omnigraph` to classify submitted labels. +ENSNODE_URL=http://localhost:4334 diff --git a/apps/ens-labels-collector/Dockerfile b/apps/ens-labels-collector/Dockerfile new file mode 100644 index 0000000000..f677667ab0 --- /dev/null +++ b/apps/ens-labels-collector/Dockerfile @@ -0,0 +1,16 @@ +FROM node:24-slim AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +WORKDIR /app + +FROM base AS deps +COPY . . +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile + +FROM deps AS runner +WORKDIR /app/apps/ens-labels-collector +ENV NODE_ENV=production +EXPOSE 4444 + +CMD ["pnpm", "start"] diff --git a/apps/ens-labels-collector/README.md b/apps/ens-labels-collector/README.md new file mode 100644 index 0000000000..1342b5221c --- /dev/null +++ b/apps/ens-labels-collector/README.md @@ -0,0 +1,50 @@ +# ens-labels-collector + +Receives ENS Label submissions from external callers, classifies each label against ENSNode's +indexed Label table, and (for now) emits a structured JSON line per submission to stdout. + +The app is intentionally minimal; persistent storage, batched on-chain emission, and a +caller-leaderboard are explicitly deferred to follow-up work (see GitHub issue +[#2003](https://github.com/namehash/ensnode/issues/2003)). The submission JSONL shape is the +future row shape so adding a sink later is mechanical. + +## Endpoints + +- `GET /health` — liveness probe; always returns `{ message: "ok" }`. +- `POST /api/submissions` — accepts `{ labels: string[], callerAddress: Address }` and + responds with per-label classification (`unknown_in_index` / `healed_in_index` / + `absent_from_index`). + +## How label classification works + +For each submitted raw label the collector: + +1. Computes `labelhashLiteralLabel(rawLabel)`. +2. If the label is normalizable AND the normalized form differs from the raw label, also + computes `labelhashLiteralLabel(normalizedLabel)`. +3. Sends every distinct labelhash to ENSNode via the typed `enssdk/omnigraph` client using + the `labels(by: { hashes })` query. +4. Classifies each submitted label: + - `unknown_in_index` — at least one of its hashes is present in the index but not yet + healed (i.e. `interpreted` is the encoded labelhash form). These are the interesting + submissions for future on-chain emission. + - `healed_in_index` — at least one of its hashes is present in the index and all + returned hits are already healed. + - `absent_from_index` — none of its hashes are present in the index. + +## Configuration + +| Env var | Required | Description | +|---------|----------|-------------| +| `PORT` | no (default `4444`) | HTTP listen port. | +| `ENSNODE_URL` | yes | Base URL of an ENSNode (ENSApi) instance with Omnigraph at `/api/omnigraph`. | + +See `.env.local.example` for a local-development template. + +## Development + +```bash +pnpm -F ens-labels-collector dev +pnpm -F ens-labels-collector typecheck +pnpm -F ens-labels-collector test +``` diff --git a/apps/ens-labels-collector/package.json b/apps/ens-labels-collector/package.json new file mode 100644 index 0000000000..e2f190c72e --- /dev/null +++ b/apps/ens-labels-collector/package.json @@ -0,0 +1,37 @@ +{ + "private": true, + "name": "ens-labels-collector", + "version": "1.10.1", + "type": "module", + "description": "Collects ENS Label submissions and classifies them against ENSNode's Omnigraph index", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/namehash/ensnode.git", + "directory": "apps/ens-labels-collector" + }, + "homepage": "https://github.com/namehash/ensnode/tree/main/apps/ens-labels-collector", + "scripts": { + "start": "tsx src/index.ts", + "dev": "tsx watch --env-file ./.env.local src/index.ts", + "test": "vitest", + "lint": "biome check --write .", + "lint:ci": "biome ci", + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@hono/node-server": "catalog:", + "enssdk": "workspace:*", + "graphql": "^16.11.0", + "hono": "catalog:", + "viem": "catalog:", + "zod": "catalog:" + }, + "devDependencies": { + "@ensnode/shared-configs": "workspace:*", + "@types/node": "catalog:", + "tsx": "^4.19.3", + "typescript": "catalog:", + "vitest": "catalog:" + } +} diff --git a/apps/ens-labels-collector/src/app.ts b/apps/ens-labels-collector/src/app.ts new file mode 100644 index 0000000000..9eb27ee84f --- /dev/null +++ b/apps/ens-labels-collector/src/app.ts @@ -0,0 +1,20 @@ +import { Hono } from "hono"; + +import { healthHandler } from "@/handlers/health"; +import { submissionsHandler } from "@/handlers/submissions"; +import { errorResponse } from "@/lib/error-response"; + +const app = new Hono(); + +app.get("/health", healthHandler); + +app.post("/api/submissions", submissionsHandler); + +app.notFound((c) => errorResponse(c, { message: "Not Found", status: 404 })); + +app.onError((error, c) => { + console.error("[ens-labels-collector] unhandled error", error); + return errorResponse(c, { error }); +}); + +export default app; diff --git a/apps/ens-labels-collector/src/config.ts b/apps/ens-labels-collector/src/config.ts new file mode 100644 index 0000000000..d40e2d84a0 --- /dev/null +++ b/apps/ens-labels-collector/src/config.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +const ConfigSchema = z.object({ + PORT: z + .string() + .optional() + .transform((value) => (value === undefined ? 4444 : Number.parseInt(value, 10))) + .pipe(z.number().int().min(1).max(65535)), + ENSNODE_URL: z.string().url(), +}); + +export type Config = { + port: number; + ensNodeUrl: string; +}; + +let cachedConfig: Config | undefined; + +/** + * Parses the process environment into a {@link Config}. + * + * Memoized so repeated calls return the same instance and validation only runs once. + * Throws (via Zod) if any required env var is missing or invalid. + */ +export function getConfig(env: NodeJS.ProcessEnv = process.env): Config { + if (cachedConfig) return cachedConfig; + + const parsed = ConfigSchema.parse(env); + + cachedConfig = { + port: parsed.PORT, + ensNodeUrl: parsed.ENSNODE_URL, + }; + + return cachedConfig; +} + +/** + * Resets the memoized config. Test-only. + */ +export function resetConfigCacheForTesting(): void { + cachedConfig = undefined; +} diff --git a/apps/ens-labels-collector/src/handlers/health.ts b/apps/ens-labels-collector/src/handlers/health.ts new file mode 100644 index 0000000000..28f79d883f --- /dev/null +++ b/apps/ens-labels-collector/src/handlers/health.ts @@ -0,0 +1,5 @@ +import type { Context } from "hono"; + +export function healthHandler(c: Context) { + return c.json({ message: "ok" }); +} diff --git a/apps/ens-labels-collector/src/handlers/submissions.test.ts b/apps/ens-labels-collector/src/handlers/submissions.test.ts new file mode 100644 index 0000000000..bda04ae659 --- /dev/null +++ b/apps/ens-labels-collector/src/handlers/submissions.test.ts @@ -0,0 +1,214 @@ +import { + encodeLabelHash, + type InterpretedLabel, + type LabelHash, + type LiteralLabel, + labelhashLiteralLabel, +} from "enssdk"; +import { Hono } from "hono"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/omnigraph-client", () => ({ + lookupLabels: vi.fn(), +})); + +import { errorResponse } from "@/lib/error-response"; +import type { LabelHit } from "@/lib/labels"; +import { lookupLabels } from "@/lib/omnigraph-client"; + +import { type SubmissionsResponse, submissionsHandler } from "./submissions"; + +const mockedLookup = vi.mocked(lookupLabels); + +function makeApp() { + const app = new Hono(); + app.post("/api/submissions", submissionsHandler); + app.onError((error, c) => errorResponse(c, { error })); + return app; +} + +const CALLER = "0x1234567890123456789012345678901234567890"; + +describe("POST /api/submissions", () => { + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + beforeEach(() => { + mockedLookup.mockReset(); + consoleSpy.mockClear(); + }); + + it("400s on malformed JSON", async () => { + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "not-json", + }); + expect(res.status).toBe(400); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it("400s when labels is empty", async () => { + const app = makeApp(); + mockedLookup.mockResolvedValue([]); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: [], callerAddress: CALLER }), + }); + expect(res.status).toBe(400); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it("400s when callerAddress is not a valid EVM address", async () => { + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["foo"], callerAddress: "not-an-address" }), + }); + expect(res.status).toBe(400); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it("classifies a healed label correctly and emits exactly one log line", async () => { + const ethHash = labelhashLiteralLabel("eth" as LiteralLabel); + mockedLookup.mockResolvedValue([ + { hash: ethHash, interpreted: "eth" as InterpretedLabel } satisfies LabelHit, + ]); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["eth"], callerAddress: CALLER }), + }); + + expect(res.status).toBe(200); + const json = (await res.json()) as SubmissionsResponse; + expect(json.callerAddress).toBe(CALLER); + expect(json.results).toHaveLength(1); + expect(json.results[0]).toMatchObject({ + rawLabel: "eth", + labelHash: ethHash, + status: "healed_in_index", + }); + + expect(consoleSpy).toHaveBeenCalledTimes(1); + const [loggedLine] = consoleSpy.mock.calls[0] as [string]; + const parsed = JSON.parse(loggedLine) as { + ts: string; + requestId: string; + callerAddress: string; + items: Array<{ status: string }>; + }; + expect(parsed.callerAddress).toBe(CALLER); + expect(parsed.items[0].status).toBe("healed_in_index"); + }); + + it("classifies an unhealed label as unknown_in_index", async () => { + const fooHash = labelhashLiteralLabel("foo" as LiteralLabel); + mockedLookup.mockResolvedValue([ + { hash: fooHash, interpreted: encodeLabelHash(fooHash) as InterpretedLabel }, + ]); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["foo"], callerAddress: CALLER }), + }); + + expect(res.status).toBe(200); + const json = (await res.json()) as SubmissionsResponse; + expect(json.results[0].status).toBe("unknown_in_index"); + }); + + it("classifies an absent label as absent_from_index", async () => { + mockedLookup.mockResolvedValue([]); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + labels: ["zzznever-existszzz"], + callerAddress: CALLER, + }), + }); + + expect(res.status).toBe(200); + const json = (await res.json()) as SubmissionsResponse; + expect(json.results[0].status).toBe("absent_from_index"); + }); + + it("normalizes the callerAddress to lowercase in both the response and the log", async () => { + mockedLookup.mockResolvedValue([]); + const mixedCase = "0xAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAa"; + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["foo"], callerAddress: mixedCase }), + }); + + expect(res.status).toBe(200); + const json = (await res.json()) as SubmissionsResponse; + expect(json.callerAddress).toBe(mixedCase.toLowerCase()); + + const [loggedLine] = consoleSpy.mock.calls[0] as [string]; + const parsed = JSON.parse(loggedLine) as { callerAddress: string }; + expect(parsed.callerAddress).toBe(mixedCase.toLowerCase()); + }); + + it("includes normalized variants in the output when raw differs from normalized", async () => { + mockedLookup.mockResolvedValue([]); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["VITALIK"], callerAddress: CALLER }), + }); + + expect(res.status).toBe(200); + const json = (await res.json()) as SubmissionsResponse; + expect(json.results[0]).toMatchObject({ + rawLabel: "VITALIK", + normalizedLabel: "vitalik", + }); + expect(json.results[0].normalizedLabelHash).toBeDefined(); + }); + + it("rejects oversized batches", async () => { + const labels = Array.from({ length: 100 }, (_, i) => `label-${i}`); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels, callerAddress: CALLER }), + }); + expect(res.status).toBe(400); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it("dedupes labelhashes before calling the omnigraph client", async () => { + mockedLookup.mockResolvedValue([]); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["foo", "foo", "foo"], callerAddress: CALLER }), + }); + + expect(res.status).toBe(200); + expect(mockedLookup).toHaveBeenCalledTimes(1); + + const fooHash = labelhashLiteralLabel("foo" as LiteralLabel); + const passedHashes = mockedLookup.mock.calls[0][0] as readonly LabelHash[]; + expect(new Set(passedHashes)).toEqual(new Set([fooHash])); + }); +}); diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts new file mode 100644 index 0000000000..f98d43bd7f --- /dev/null +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -0,0 +1,131 @@ +import type { Address } from "enssdk"; +import type { Context } from "hono"; +import { isAddress } from "viem"; +import { z } from "zod"; + +import { errorResponse } from "@/lib/error-response"; +import { + classifySubmissions, + collectLookupHashes, + hashLabel, + type LabelClassification, +} from "@/lib/labels"; +import { lookupLabels } from "@/lib/omnigraph-client"; + +/** + * Mirror of `LABELS_BY_HASHES_MAX` in `apps/ensapi/src/omnigraph-api/schema/label.ts`. + * + * The collector pre-caps to the same limit so requests fail fast with a clear 400 instead of + * trekking to ENSApi only to be rejected. + * + * Each submitted label can produce up to 2 hashes (raw + normalized variant), so we accept at + * most `LABELS_BY_HASHES_MAX / 2` raw labels per request. + */ +export const MAX_LABELS_PER_SUBMISSION = 50; + +const SubmissionsRequestSchema = z.object({ + labels: z.array(z.string().min(1).max(1000)).min(1).max(MAX_LABELS_PER_SUBMISSION), + callerAddress: z + .string() + .refine((value) => isAddress(value, { strict: false }), { + message: "callerAddress must be a valid EVM address", + }) + .transform((value) => value.toLowerCase() as Address), +}); + +export type SubmissionsRequest = z.infer; + +export type SubmissionResultItem = { + rawLabel: string; + labelHash: string; + normalizedLabel?: string; + normalizedLabelHash?: string; + status: LabelClassification["status"]; +}; + +export type SubmissionsResponse = { + submittedAt: string; + callerAddress: Address; + results: SubmissionResultItem[]; +}; + +/** + * Structured submission record written to stdout as a single JSON line per request. + * + * Intentionally storage-agnostic: the shape is the future DB row shape so swapping the sink + * for Postgres + Drizzle later is a mechanical change. + * + * TODO(#2003): replace stdout sink with persistent store (Postgres + Drizzle) without changing + * this log shape. + * + * TODO(#2003): drain stored submissions on a schedule and submit them in batches; do not + * couple to the request lifecycle. + * + * TODO(#2003): a downstream aggregator can compute leaderboards by `callerAddress` from these + * lines (per-status counts are already implicit in `items[].status`). + */ +export type SubmissionLogLine = { + ts: string; + requestId: string; + callerAddress: Address; + items: SubmissionResultItem[]; +}; + +function toResultItem(c: LabelClassification): SubmissionResultItem { + const item: SubmissionResultItem = { + rawLabel: c.rawLabel, + labelHash: c.labelHash, + status: c.status, + }; + if (c.normalizedLabel !== undefined) item.normalizedLabel = c.normalizedLabel; + if (c.normalizedLabelHash !== undefined) item.normalizedLabelHash = c.normalizedLabelHash; + return item; +} + +function generateRequestId(): string { + if (typeof globalThis.crypto?.randomUUID === "function") { + return globalThis.crypto.randomUUID(); + } + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; +} + +export async function submissionsHandler(c: Context) { + let body: unknown; + try { + body = await c.req.json(); + } catch { + return errorResponse(c, { message: "Request body must be valid JSON", status: 400 }); + } + + const parsed = SubmissionsRequestSchema.safeParse(body); + if (!parsed.success) { + return errorResponse(c, { error: parsed.error }); + } + + const { labels, callerAddress } = parsed.data; + + const hashed = labels.map(hashLabel); + const hashes = collectLookupHashes(hashed); + + const hits = await lookupLabels(hashes); + const classifications = classifySubmissions(hashed, hits); + const results = classifications.map(toResultItem); + + const submittedAt = new Date().toISOString(); + const requestId = generateRequestId(); + + const logLine: SubmissionLogLine = { + ts: submittedAt, + requestId, + callerAddress, + items: results, + }; + console.log(JSON.stringify(logLine)); + + const response: SubmissionsResponse = { + submittedAt, + callerAddress, + results, + }; + return c.json(response); +} diff --git a/apps/ens-labels-collector/src/index.ts b/apps/ens-labels-collector/src/index.ts new file mode 100644 index 0000000000..e1b874a4c9 --- /dev/null +++ b/apps/ens-labels-collector/src/index.ts @@ -0,0 +1,43 @@ +import { getConfig } from "@/config"; + +import { serve } from "@hono/node-server"; + +import app from "@/app"; + +const config = getConfig(); + +const server = serve( + { + fetch: app.fetch, + port: config.port, + }, + (info) => { + console.log(`ens-labels-collector listening on port ${info.port}`); + }, +); + +const closeServer = () => + new Promise((resolve, reject) => + server.close((err) => { + if (err) reject(err); + else resolve(); + }), + ); + +const gracefulShutdown = async () => { + try { + await closeServer(); + process.exit(0); + } catch (error) { + console.error("[ens-labels-collector] shutdown error", error); + process.exit(1); + } +}; + +process.on("SIGINT", gracefulShutdown); +process.on("SIGTERM", gracefulShutdown); + +process.on("uncaughtException", async (error) => { + console.error("[ens-labels-collector] uncaughtException", error); + await gracefulShutdown(); +}); diff --git a/apps/ens-labels-collector/src/lib/error-response.ts b/apps/ens-labels-collector/src/lib/error-response.ts new file mode 100644 index 0000000000..f5898d2ef5 --- /dev/null +++ b/apps/ens-labels-collector/src/lib/error-response.ts @@ -0,0 +1,60 @@ +import type { Context } from "hono"; +import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status"; +import { ZodError } from "zod"; + +/** + * Standardized error response shape for the ens-labels-collector. + * + * Mirrors the shape used elsewhere in the monorepo (see AGENTS.md "API boundaries"). + */ +export type ErrorResponseBody = { + message: string; + details?: unknown; +}; + +type ErrorStatus = ClientErrorStatusCode | ServerErrorStatusCode; + +/** + * Sends a JSON error response with the canonical `{ message, details? }` shape. + * + * - `ZodError` becomes a 400 with `message: "Invalid Input"` and the flattened Zod issues as `details`. + * - `Error` instances forward their `message`. + * - Anything else becomes a generic `Internal Server Error`. + */ +type ErrorOptions = + | { error: unknown; status?: ErrorStatus; details?: unknown } + | { message: string; status: ErrorStatus; details?: unknown }; + +export function errorResponse( + c: Context, + options: ErrorOptions = { error: new Error("Internal Server Error"), status: 500 }, +) { + if ("message" in options) { + const body: ErrorResponseBody = { message: options.message }; + if (options.details !== undefined) body.details = options.details; + return c.json(body, options.status); + } + + const { error } = options; + let status: ErrorStatus = options.status ?? 500; + let body: ErrorResponseBody; + + if (error instanceof ZodError) { + status = 400; + body = { + message: "Invalid Input", + details: options.details ?? error.flatten(), + }; + } else if (error instanceof Error) { + body = { message: error.message }; + if (options.details !== undefined) body.details = options.details; + } else if (typeof error === "string") { + body = { message: error }; + if (options.details !== undefined) body.details = options.details; + } else { + body = { message: "Internal Server Error" }; + if (options.details !== undefined) body.details = options.details; + } + + return c.json(body, status); +} diff --git a/apps/ens-labels-collector/src/lib/labels.test.ts b/apps/ens-labels-collector/src/lib/labels.test.ts new file mode 100644 index 0000000000..5c2827bbf0 --- /dev/null +++ b/apps/ens-labels-collector/src/lib/labels.test.ts @@ -0,0 +1,155 @@ +import { + encodeLabelHash, + type InterpretedLabel, + type LabelHash, + type LiteralLabel, + labelhashLiteralLabel, +} from "enssdk"; +import { describe, expect, it } from "vitest"; + +import { + classifySubmissions, + collectLookupHashes, + hashLabel, + isUnhealedHit, + type LabelHit, +} from "./labels"; + +const literal = (s: string) => s as LiteralLabel; + +describe("hashLabel", () => { + it("computes labelhash for a normalized lowercase label", () => { + const result = hashLabel("vitalik"); + expect(result).toEqual({ + rawLabel: "vitalik", + labelHash: labelhashLiteralLabel(literal("vitalik")), + }); + }); + + it("does not populate normalizedLabel when raw equals normalized", () => { + const result = hashLabel("eth"); + expect(result.normalizedLabel).toBeUndefined(); + expect(result.normalizedLabelHash).toBeUndefined(); + }); + + it("populates normalizedLabel + hash when uppercase label normalizes to lowercase", () => { + const result = hashLabel("VITALIK"); + expect(result.rawLabel).toBe("VITALIK"); + expect(result.labelHash).toBe(labelhashLiteralLabel(literal("VITALIK"))); + expect(result.normalizedLabel).toBe("vitalik"); + expect(result.normalizedLabelHash).toBe(labelhashLiteralLabel(literal("vitalik"))); + expect(result.normalizedLabelHash).not.toBe(result.labelHash); + }); + + it("tolerates unnormalizable labels (e.g. labels with periods)", () => { + const result = hashLabel("foo.bar"); + expect(result.rawLabel).toBe("foo.bar"); + expect(result.labelHash).toBe(labelhashLiteralLabel(literal("foo.bar"))); + expect(result.normalizedLabel).toBeUndefined(); + expect(result.normalizedLabelHash).toBeUndefined(); + }); + + it("tolerates the empty string (cannot normalize)", () => { + const result = hashLabel(""); + expect(result.rawLabel).toBe(""); + expect(result.normalizedLabel).toBeUndefined(); + }); + + it("hashes a unicode label", () => { + const label = "vitalik\u00e9"; + const result = hashLabel(label); + expect(result.labelHash).toBe(labelhashLiteralLabel(label as LiteralLabel)); + }); +}); + +describe("collectLookupHashes", () => { + it("returns the deduped union of raw + normalized labelhashes", () => { + const a = hashLabel("VITALIK"); + const b = hashLabel("vitalik"); + const hashes = collectLookupHashes([a, b]); + expect(hashes).toHaveLength(2); + expect(new Set(hashes).size).toBe(hashes.length); + expect(hashes).toContain(a.labelHash); + expect(hashes).toContain(b.labelHash); + }); + + it("ignores undefined normalized hashes", () => { + const a = hashLabel("eth"); + const hashes = collectLookupHashes([a]); + expect(hashes).toEqual([a.labelHash]); + }); +}); + +describe("isUnhealedHit", () => { + it("returns true when interpreted equals encodeLabelHash(hash)", () => { + const hash = labelhashLiteralLabel(literal("xyz")); + const hit: LabelHit = { hash, interpreted: encodeLabelHash(hash) as InterpretedLabel }; + expect(isUnhealedHit(hit)).toBe(true); + }); + + it("returns false when interpreted is a healed literal", () => { + const hash = labelhashLiteralLabel(literal("vitalik")); + const hit: LabelHit = { hash, interpreted: "vitalik" as InterpretedLabel }; + expect(isUnhealedHit(hit)).toBe(false); + }); +}); + +describe("classifySubmissions", () => { + const vitalik = hashLabel("vitalik"); + const eth = hashLabel("eth"); + const upper = hashLabel("HELLO"); + const random = hashLabel("zzzdoesnotexistzzz"); + + function makeHealedHit(hash: LabelHash, label: string): LabelHit { + return { hash, interpreted: label as InterpretedLabel }; + } + + function makeUnhealedHit(hash: LabelHash): LabelHit { + return { hash, interpreted: encodeLabelHash(hash) as InterpretedLabel }; + } + + it("classifies absent labels as absent_from_index", () => { + const result = classifySubmissions([random], []); + expect(result).toEqual([{ ...random, status: "absent_from_index" }]); + }); + + it("classifies a healed-only hit as healed_in_index", () => { + const result = classifySubmissions([vitalik], [makeHealedHit(vitalik.labelHash, "vitalik")]); + expect(result[0].status).toBe("healed_in_index"); + }); + + it("classifies an unhealed hit as unknown_in_index", () => { + const result = classifySubmissions([vitalik], [makeUnhealedHit(vitalik.labelHash)]); + expect(result[0].status).toBe("unknown_in_index"); + }); + + it("classifies as unknown_in_index when ANY of the label's hashes is unhealed", () => { + // upper has both raw + normalized hashes; if normalized is unhealed but raw is healed, + // the submission should still be unknown_in_index. + if (upper.normalizedLabelHash === undefined) { + throw new Error("test fixture invariant: 'HELLO' must produce a normalized variant"); + } + const result = classifySubmissions( + [upper], + [makeHealedHit(upper.labelHash, upper.rawLabel), makeUnhealedHit(upper.normalizedLabelHash)], + ); + expect(result[0].status).toBe("unknown_in_index"); + }); + + it("handles a mixed batch", () => { + const result = classifySubmissions( + [vitalik, eth, random], + [ + makeUnhealedHit(vitalik.labelHash), + makeHealedHit(eth.labelHash, "eth"), + // random has no hit + ], + ); + + expect(result.map((r) => r.status)).toEqual([ + "unknown_in_index", + "healed_in_index", + "absent_from_index", + ]); + }); +}); diff --git a/apps/ens-labels-collector/src/lib/labels.ts b/apps/ens-labels-collector/src/lib/labels.ts new file mode 100644 index 0000000000..3f5362b257 --- /dev/null +++ b/apps/ens-labels-collector/src/lib/labels.ts @@ -0,0 +1,133 @@ +import { + encodeLabelHash, + type InterpretedLabel, + type LabelHash, + type LiteralLabel, + labelhashLiteralLabel, + normalizeLabel, +} from "enssdk"; + +/** + * Per-label classification status against the ENSNode index. + * + * - `unknown_in_index`: at least one of the label's hashes is present in the index but its + * interpreted form is the encoded labelhash (i.e. the literal label has not yet been healed). + * These submissions are the interesting candidates for future on-chain emission. + * - `healed_in_index`: at least one of the label's hashes is present in the index and every + * returned hit already carries a healed (normalized literal) interpreted form. + * - `absent_from_index`: none of the label's hashes are present in the index at all. + */ +export type LabelStatus = "unknown_in_index" | "healed_in_index" | "absent_from_index"; + +/** + * The hashing result for a single submitted raw label. + * + * `normalizedLabel` (and its hash) are populated only when the raw label is normalizable + * AND the normalized form differs from the raw label. + */ +export type HashedLabel = { + rawLabel: string; + labelHash: LabelHash; + normalizedLabel?: InterpretedLabel; + normalizedLabelHash?: LabelHash; +}; + +/** + * Per-label classification entry returned to the caller and emitted to the stdout sink. + */ +export type LabelClassification = HashedLabel & { + status: LabelStatus; +}; + +/** + * Subset of `Label` fields returned by the Omnigraph `labels` query that we care about. + */ +export type LabelHit = { + hash: LabelHash; + interpreted: InterpretedLabel; +}; + +/** + * Computes the hash representations of a single raw label. + * + * Always computes `labelHash = labelhashLiteralLabel(rawLabel)`. If the raw label is + * normalizable AND its normalized form differs from the raw value, also computes the + * normalized form's hash. Normalization failures are tolerated and treated as "no + * normalized variant" — many submissions will be unnormalizable by design (this app + * is meant to ingest such labels). + */ +export function hashLabel(rawLabel: string): HashedLabel { + const labelHash = labelhashLiteralLabel(rawLabel as LiteralLabel); + + let normalizedLabel: InterpretedLabel | undefined; + let normalizedLabelHash: LabelHash | undefined; + try { + const candidate = normalizeLabel(rawLabel); + if (candidate !== rawLabel) { + normalizedLabel = candidate; + normalizedLabelHash = labelhashLiteralLabel(candidate as unknown as LiteralLabel); + } + } catch { + // unnormalizable raw label is expected; leave normalized variant undefined + } + + const result: HashedLabel = { rawLabel, labelHash }; + if (normalizedLabel !== undefined) { + result.normalizedLabel = normalizedLabel; + result.normalizedLabelHash = normalizedLabelHash; + } + return result; +} + +/** + * Returns the deduped flat list of labelhashes we want to look up via the Omnigraph + * `labels(by: { hashes })` query. + */ +export function collectLookupHashes(hashed: readonly HashedLabel[]): LabelHash[] { + const set = new Set(); + for (const item of hashed) { + set.add(item.labelHash); + if (item.normalizedLabelHash !== undefined) set.add(item.normalizedLabelHash); + } + return Array.from(set); +} + +/** + * True when an Omnigraph `Label` row represents an unhealed/unknown label + * (i.e. its `interpreted` form is the Encoded LabelHash of its `hash`). + */ +export function isUnhealedHit(hit: LabelHit): boolean { + return hit.interpreted === encodeLabelHash(hit.hash); +} + +/** + * Joins per-label hashes against the omnigraph hits and assigns a {@link LabelStatus} to each + * submitted raw label. + */ +export function classifySubmissions( + hashed: readonly HashedLabel[], + hits: readonly LabelHit[], +): LabelClassification[] { + const hitsByHash = new Map(); + for (const hit of hits) hitsByHash.set(hit.hash, hit); + + return hashed.map((item) => { + const candidateHashes: LabelHash[] = [item.labelHash]; + if (item.normalizedLabelHash !== undefined) candidateHashes.push(item.normalizedLabelHash); + + const matchedHits = candidateHashes + .map((h) => hitsByHash.get(h)) + .filter((h): h is LabelHit => h !== undefined); + + let status: LabelStatus; + if (matchedHits.length === 0) { + status = "absent_from_index"; + } else if (matchedHits.some(isUnhealedHit)) { + status = "unknown_in_index"; + } else { + status = "healed_in_index"; + } + + return { ...item, status }; + }); +} diff --git a/apps/ens-labels-collector/src/lib/omnigraph-client.ts b/apps/ens-labels-collector/src/lib/omnigraph-client.ts new file mode 100644 index 0000000000..4f808e2b42 --- /dev/null +++ b/apps/ens-labels-collector/src/lib/omnigraph-client.ts @@ -0,0 +1,64 @@ +import { getConfig } from "@/config"; + +import { createEnsNodeClient } from "enssdk/core"; +import { graphql, omnigraph } from "enssdk/omnigraph"; + +import type { LabelHit } from "@/lib/labels"; + +/** + * Typed document for the `labels(by: { hashes })` Omnigraph query. + * + * Variable + result types are derived from the generated introspection in `enssdk/omnigraph`, + * so changes to the schema break this call site at typecheck time. + */ +export const LabelsByHashes = graphql(` + query LabelsByHashes($hashes: [Hex!]!) { + labels(by: { hashes: $hashes }) { + hash + interpreted + } + } +`); + +let cachedClient: ReturnType | undefined; + +function makeClient(url: string) { + return createEnsNodeClient({ url }).extend(omnigraph); +} + +function getClient() { + if (cachedClient) return cachedClient; + cachedClient = makeClient(getConfig().ensNodeUrl); + return cachedClient; +} + +/** + * Looks up Labels by a batch of LabelHashes against ENSNode's Omnigraph. + * + * The Omnigraph resolver enforces a hard cap (see `LABELS_BY_HASHES_MAX` in + * `apps/ensapi/src/omnigraph-api/schema/label.ts`); the collector should pre-cap to the same + * limit to fail fast on the client side before making the request. + */ +export async function lookupLabels(hashes: readonly string[]): Promise { + if (hashes.length === 0) return []; + + const result = await getClient().omnigraph.query({ + query: LabelsByHashes, + variables: { hashes: hashes as `0x${string}`[] }, + }); + + if (result.errors && result.errors.length > 0) { + throw new Error( + `Omnigraph labels query returned errors: ${result.errors.map((e) => e.message).join("; ")}`, + ); + } + + return (result.data?.labels ?? []) as LabelHit[]; +} + +/** + * Resets the memoized Omnigraph client. Test-only. + */ +export function resetOmnigraphClientCacheForTesting(): void { + cachedClient = undefined; +} diff --git a/apps/ens-labels-collector/tsconfig.json b/apps/ens-labels-collector/tsconfig.json new file mode 100644 index 0000000000..a70f8cfaeb --- /dev/null +++ b/apps/ens-labels-collector/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@ensnode/shared-configs/tsconfig.lib.json", + "compilerOptions": { + "target": "esnext", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/ens-labels-collector/vitest.config.ts b/apps/ens-labels-collector/vitest.config.ts new file mode 100644 index 0000000000..66da1b27f1 --- /dev/null +++ b/apps/ens-labels-collector/vitest.config.ts @@ -0,0 +1,15 @@ +import { resolve } from "node:path"; + +import { configDefaults, defineProject } from "vitest/config"; + +export default defineProject({ + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, + test: { + name: "ens-labels-collector", + exclude: [...configDefaults.exclude, "**/*.integration.test.ts"], + }, +}); diff --git a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts new file mode 100644 index 0000000000..fc44d4da43 --- /dev/null +++ b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts @@ -0,0 +1,96 @@ +import { + asInterpretedLabel, + encodeLabelHash, + type Hex, + type InterpretedLabel, + type LabelHash, + labelhashInterpretedLabel, +} from "enssdk"; +import { describe, expect, it } from "vitest"; + +import { request } from "@/test/integration/graphql-utils"; +import { gql } from "@/test/integration/omnigraph-api-client"; + +type LabelsByHashesResult = { + labels: Array<{ hash: LabelHash; interpreted: InterpretedLabel }>; +}; + +const LabelsByHashes = gql` + query LabelsByHashes($hashes: [Hex!]!) { + labels(by: { hashes: $hashes }) { + hash + interpreted + } + } +`; + +// 'eth' is always seeded in the devnet fixture as a healed label +const ETH_LABEL_HASH: LabelHash = labelhashInterpretedLabel(asInterpretedLabel("eth")); + +// a labelhash that should not exist in the index (random 0xff... bytes) +const ABSENT_LABEL_HASH = `0x${"ff".repeat(32)}` as LabelHash; + +describe("Query.labels", () => { + it("returns a healed label entry for a known LabelHash", async () => { + const result = await request(LabelsByHashes, { + hashes: [ETH_LABEL_HASH], + }); + + expect(result.labels).toHaveLength(1); + expect(result.labels[0]).toMatchObject({ + hash: ETH_LABEL_HASH, + interpreted: "eth", + }); + }); + + it("omits LabelHashes that are not present in the index", async () => { + const result = await request(LabelsByHashes, { + hashes: [ABSENT_LABEL_HASH], + }); + + expect(result.labels).toEqual([]); + }); + + it("returns only the present labels when input mixes present and absent hashes", async () => { + const result = await request(LabelsByHashes, { + hashes: [ETH_LABEL_HASH, ABSENT_LABEL_HASH], + }); + + expect(result.labels).toHaveLength(1); + expect(result.labels[0].hash).toBe(ETH_LABEL_HASH); + }); + + it("dedupes repeated input hashes", async () => { + const result = await request(LabelsByHashes, { + hashes: [ETH_LABEL_HASH, ETH_LABEL_HASH, ETH_LABEL_HASH], + }); + + expect(result.labels).toHaveLength(1); + expect(result.labels[0].hash).toBe(ETH_LABEL_HASH); + }); + + it("returns an empty list when input is empty", async () => { + const result = await request(LabelsByHashes, { hashes: [] as Hex[] }); + expect(result.labels).toEqual([]); + }); + + it("classifies returned labels: 'eth' is healed (interpreted !== encodeLabelHash(hash))", async () => { + const result = await request(LabelsByHashes, { + hashes: [ETH_LABEL_HASH], + }); + + const [row] = result.labels; + expect(row.interpreted).not.toBe(encodeLabelHash(row.hash)); + }); + + it("rejects requests over the maximum allowed hash count", async () => { + // generate (LABELS_BY_HASHES_MAX + 1) distinct labelhashes deterministically + const hashes: LabelHash[] = []; + for (let i = 0; i <= 100; i++) { + const hex = i.toString(16).padStart(64, "0"); + hashes.push(`0x${hex}` as LabelHash); + } + + await expect(request(LabelsByHashes, { hashes })).rejects.toThrow(/Too many hashes/); + }); +}); diff --git a/apps/ensapi/src/omnigraph-api/schema/label.ts b/apps/ensapi/src/omnigraph-api/schema/label.ts index 2ea3fec004..85ebd10e63 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.ts @@ -28,3 +28,26 @@ LabelRef.implement({ }), }), }); + +////////// +// Inputs +////////// + +/** + * Maximum number of LabelHashes accepted per `Query.labels` request. + * + * Caps the resolver's `inArray` query so a single GraphQL request cannot enumerate + * the entire `label` table. + */ +export const LABELS_BY_HASHES_MAX = 100; + +export const LabelsByHashesInput = builder.inputType("LabelsByHashesInput", { + description: "Look up Labels by a batch of LabelHashes.", + fields: (t) => ({ + hashes: t.field({ + type: ["Hex"], + required: true, + description: `LabelHashes to look up. Up to ${LABELS_BY_HASHES_MAX} hashes per request. Absent labels are simply omitted from the result.`, + }), + }), +}); diff --git a/apps/ensapi/src/omnigraph-api/schema/query.ts b/apps/ensapi/src/omnigraph-api/schema/query.ts index 4e5df509f7..44761b50f5 100644 --- a/apps/ensapi/src/omnigraph-api/schema/query.ts +++ b/apps/ensapi/src/omnigraph-api/schema/query.ts @@ -1,6 +1,7 @@ import config from "@/config"; import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; +import { inArray } from "drizzle-orm"; import { makeConcreteRegistryId, makePermissionsId, makeResolverId } from "enssdk"; import { getRootRegistryId } from "@ensnode/ensnode-sdk"; @@ -25,6 +26,7 @@ import { DomainsOrderInput, DomainsWhereInput, } from "@/omnigraph-api/schema/domain"; +import { LABELS_BY_HASHES_MAX, LabelRef, LabelsByHashesInput } from "@/omnigraph-api/schema/label"; import { PermissionsIdInput, PermissionsRef } from "@/omnigraph-api/schema/permissions"; import { RegistrationInterfaceRef } from "@/omnigraph-api/schema/registration"; import { RegistryIdInput, RegistryInterfaceRef } from "@/omnigraph-api/schema/registry"; @@ -140,6 +142,37 @@ builder.queryType({ }, }), + ///////////////////////// + // Find Labels by Hashes + ///////////////////////// + labels: t.field({ + description: + "Look up Labels in the index by a batch of LabelHashes. " + + "Each returned Label exposes its `hash` and `interpreted` representation, where " + + "`interpreted` is the Encoded LabelHash for unhealed/unknown labels and a normalized " + + "literal for healed labels. LabelHashes that are not present in the index are simply " + + "omitted from the result.", + type: [LabelRef], + nullable: false, + args: { by: t.arg({ type: LabelsByHashesInput, required: true }) }, + resolve: async (_parent, { by }) => { + if (by.hashes.length === 0) return []; + + if (by.hashes.length > LABELS_BY_HASHES_MAX) { + throw new Error( + `Too many hashes: received ${by.hashes.length}, max ${LABELS_BY_HASHES_MAX}.`, + ); + } + + const dedupedHashes = Array.from(new Set(by.hashes)); + + return ensDb + .select() + .from(ensIndexerSchema.label) + .where(inArray(ensIndexerSchema.label.labelHash, dedupedHashes)); + }, + }), + ///////////////////////////////////// // Get Account by Id or Address ///////////////////////////////////// diff --git a/packages/enssdk/src/omnigraph/generated/introspection.ts b/packages/enssdk/src/omnigraph/generated/introspection.ts index c6763e36ad..6207c83302 100644 --- a/packages/enssdk/src/omnigraph/generated/introspection.ts +++ b/packages/enssdk/src/omnigraph/generated/introspection.ts @@ -3246,6 +3246,29 @@ const introspection = { ], "interfaces": [] }, + { + "kind": "INPUT_OBJECT", + "name": "LabelsByHashesInput", + "inputFields": [ + { + "name": "hashes", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Hex" + } + } + } + } + } + ], + "isOneOf": false + }, { "kind": "INPUT_OBJECT", "name": "NameOrNodeInput", @@ -4325,6 +4348,35 @@ const introspection = { ], "isDeprecated": false }, + { + "name": "labels", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "OBJECT", + "name": "Label" + } + } + } + }, + "args": [ + { + "name": "by", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "INPUT_OBJECT", + "name": "LabelsByHashesInput" + } + } + } + ], + "isDeprecated": false + }, { "name": "permissions", "type": { diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql index ac546669a6..1024063f86 100644 --- a/packages/enssdk/src/omnigraph/generated/schema.graphql +++ b/packages/enssdk/src/omnigraph/generated/schema.graphql @@ -692,6 +692,14 @@ type Label { interpreted: InterpretedLabel! } +"""Look up Labels by a batch of LabelHashes.""" +input LabelsByHashesInput { + """ + LabelHashes to look up. Up to 100 hashes per request. Absent labels are simply omitted from the result. + """ + hashes: [Hex!]! +} + """Constructs a reference to a specific Node via one of `name` or `node`.""" input NameOrNodeInput @oneOf { name: InterpretedName @@ -888,6 +896,11 @@ type Query { """Find Domains by Name.""" domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: DomainsWhereInput!): QueryDomainsConnection + """ + Look up Labels in the index by a batch of LabelHashes. Each returned Label exposes its `hash` and `interpreted` representation, where `interpreted` is the Encoded LabelHash for unhealed/unknown labels and a normalized literal for healed labels. LabelHashes that are not present in the index are simply omitted from the result. + """ + labels(by: LabelsByHashesInput!): [Label!]! + """Identify Permissions by ID or AccountId.""" permissions(by: PermissionsIdInput!): Permissions diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d126548f9c..734e3b0206 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,13 +138,13 @@ overrides: patchedDependencies: '@changesets/assemble-release-plan@6.0.9': - hash: b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52 + hash: 7lkorkta6ossod7lbecbwpu4eq path: patches/@changesets__assemble-release-plan@6.0.9.patch '@opentelemetry/api': - hash: 4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a + hash: izegmddoiqidmvapuxeha6lnw4 path: patches/@opentelemetry__api.patch '@opentelemetry/otlp-exporter-base': - hash: b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d + hash: brtaqazy64vzhaznf7q5df2byy path: patches/@opentelemetry__otlp-exporter-base.patch importers: @@ -176,6 +176,43 @@ importers: specifier: 'catalog:' version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) + apps/ens-labels-collector: + dependencies: + '@hono/node-server': + specifier: 'catalog:' + version: 1.19.14(hono@4.12.14) + enssdk: + specifier: workspace:* + version: link:../../packages/enssdk + graphql: + specifier: ^16.11.0 + version: 16.11.0 + hono: + specifier: 'catalog:' + version: 4.12.14 + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@4.3.6) + zod: + specifier: 'catalog:' + version: 4.3.6 + devDependencies: + '@ensnode/shared-configs': + specifier: workspace:* + version: link:../../packages/shared-configs + '@types/node': + specifier: 'catalog:' + version: 24.10.9 + tsx: + specifier: ^4.19.3 + version: 4.21.0 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vitest: + specifier: 'catalog:' + version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) + apps/ensadmin: dependencies: '@ensnode/datasources': @@ -276,7 +313,7 @@ importers: version: 0.548.0(react@19.2.1) next: specifier: ^16.2.3 - version: 16.2.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 16.2.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -364,37 +401,37 @@ importers: version: link:../../packages/ens-referrals '@opentelemetry/api': specifier: ^1.9.0 - version: 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + version: 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/core': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/exporter-metrics-otlp-proto': specifier: ^0.202.0 - version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/exporter-trace-otlp-proto': specifier: ^0.202.0 - version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/sdk-node': specifier: ^0.202.0 - version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/sdk-trace-base': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/sdk-trace-node': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.37.0 '@ponder/client': specifier: 'catalog:' - version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) '@pothos/core': specifier: ^4.10.0 version: 4.10.0(graphql@16.11.0) @@ -409,7 +446,7 @@ importers: version: 1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0) '@pothos/tracing-opentelemetry': specifier: ^1.1.3 - version: 1.1.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0) + version: 1.1.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0) '@standard-schema/utils': specifier: ^0.3.0 version: 0.3.0 @@ -421,7 +458,7 @@ importers: version: 4.1.0 drizzle-orm: specifier: 'catalog:' - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) enssdk: specifier: workspace:* version: link:../../packages/enssdk @@ -509,7 +546,7 @@ importers: version: link:../../packages/ponder-sdk '@ponder/client': specifier: 'catalog:' - version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) caip: specifier: 'catalog:' version: 1.1.1 @@ -524,7 +561,7 @@ importers: version: 5.6.1 drizzle-orm: specifier: 'catalog:' - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) enssdk: specifier: workspace:* version: link:../../packages/enssdk @@ -536,7 +573,7 @@ importers: version: 7.1.1 ponder: specifier: 'catalog:' - version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) + version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) viem: specifier: 'catalog:' version: 2.38.5(typescript@5.9.3)(zod@4.3.6) @@ -710,7 +747,7 @@ importers: version: 0.2.9(astro@5.18.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) '@tailwindcss/vite': specifier: ^4.1.15 - version: 4.1.16(vite@7.3.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.16(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)) astro: specifier: 'catalog:' version: 5.18.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) @@ -925,10 +962,10 @@ importers: version: 0.31.10 drizzle-orm: specifier: 'catalog:' - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) ponder: specifier: 'catalog:' - version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) + version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) tsup: specifier: 'catalog:' version: 8.5.0(jiti@2.6.1)(postcss@8.5.12)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) @@ -1290,13 +1327,13 @@ importers: version: 2.5.1 '@ponder/client': specifier: 'catalog:' - version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) dataloader: specifier: ^2.2.3 version: 2.2.3 drizzle-orm: specifier: 0.41.0 - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) graphql: specifier: ^16.10.0 version: 16.11.0 @@ -1683,28 +1720,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@2.3.2': resolution: {integrity: sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.2': resolution: {integrity: sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@2.3.2': resolution: {integrity: sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@2.3.2': resolution: {integrity: sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==} @@ -2717,274 +2750,232 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.3': resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.3': resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.3': resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.3': resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.3': resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.3': resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.3': resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm64@0.34.4': resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.4': resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.4': resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.4': resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.4': resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.4': resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.4': resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -3130,28 +3121,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.3': resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.3': resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.3': resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.3': resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==} @@ -4085,79 +4072,66 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -4517,56 +4491,48 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.16': resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.16': resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.16': resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.16': resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} @@ -7119,28 +7085,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -10522,7 +10484,7 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.3 - '@changesets/assemble-release-plan@6.0.9(patch_hash=b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52)': + '@changesets/assemble-release-plan@6.0.9(patch_hash=7lkorkta6ossod7lbecbwpu4eq)': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 @@ -10546,7 +10508,7 @@ snapshots: '@changesets/cli@2.29.8(@types/node@24.10.9)': dependencies: '@changesets/apply-release-plan': 7.0.14 - '@changesets/assemble-release-plan': 6.0.9(patch_hash=b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52) + '@changesets/assemble-release-plan': 6.0.9(patch_hash=7lkorkta6ossod7lbecbwpu4eq) '@changesets/changelog-git': 0.2.1 '@changesets/config': 3.1.2 '@changesets/errors': 0.2.0 @@ -10606,7 +10568,7 @@ snapshots: '@changesets/get-release-plan@4.0.14': dependencies: - '@changesets/assemble-release-plan': 6.0.9(patch_hash=b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52) + '@changesets/assemble-release-plan': 6.0.9(patch_hash=7lkorkta6ossod7lbecbwpu4eq) '@changesets/config': 3.1.2 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.6 @@ -10821,7 +10783,7 @@ snapshots: '@esbuild-kit/core-utils@3.3.2': dependencies: - esbuild: 0.27.2 + esbuild: 0.27.4 source-map-support: 0.5.21 '@esbuild-kit/esm-loader@2.6.5': @@ -11411,7 +11373,7 @@ snapshots: '@hono/otel@0.2.2(hono@4.12.14)': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/semantic-conventions': 1.37.0 hono: 4.12.14 @@ -11963,263 +11925,263 @@ snapshots: '@opentelemetry/api-logs@0.202.0': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)': {} + '@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)': {} - '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/exporter-logs-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-logs-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/exporter-logs-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-logs-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/exporter-logs-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - - '@opentelemetry/exporter-metrics-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - - '@opentelemetry/exporter-metrics-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - - '@opentelemetry/exporter-prometheus@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - - '@opentelemetry/exporter-trace-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + + '@opentelemetry/exporter-metrics-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + + '@opentelemetry/exporter-metrics-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + + '@opentelemetry/exporter-prometheus@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + + '@opentelemetry/exporter-trace-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - - '@opentelemetry/exporter-trace-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - - '@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - - '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + + '@opentelemetry/exporter-trace-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + + '@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + + '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/instrumentation@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/instrumentation@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/api-logs': 0.202.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/otlp-exporter-base@0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-grpc-exporter-base@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/otlp-grpc-exporter-base@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) protobufjs: 7.5.5 - '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-logs@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-logs@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-node@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-node@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-logs-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-logs-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-logs-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-metrics-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-prometheus': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-trace-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-trace-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-logs-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-logs-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-logs-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-metrics-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-prometheus': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-trace-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-trace-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) '@opentelemetry/semantic-conventions@1.37.0': {} @@ -12250,9 +12212,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@ponder/client@0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3)': + '@ponder/client@0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3)': dependencies: - drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) eventsource: 3.0.7 superjson: 2.2.6 optionalDependencies: @@ -12313,9 +12275,9 @@ snapshots: '@pothos/core': 4.10.0(graphql@16.11.0) graphql: 16.11.0 - '@pothos/tracing-opentelemetry@1.1.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0)': + '@pothos/tracing-opentelemetry@1.1.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0)': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@opentelemetry/semantic-conventions': 1.37.0 '@pothos/core': 4.10.0(graphql@16.11.0) '@pothos/plugin-tracing': 1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0) @@ -13471,6 +13433,13 @@ snapshots: postcss: 8.5.12 tailwindcss: 4.1.18 + '@tailwindcss/vite@4.1.16(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + tailwindcss: 4.1.16 + vite: 6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) + '@tailwindcss/vite@4.1.16(vite@7.3.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.1.16 @@ -15189,10 +15158,10 @@ snapshots: esbuild: 0.25.11 tsx: 4.21.0 - drizzle-orm@0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3): + drizzle-orm@0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3): optionalDependencies: '@electric-sql/pglite': 0.2.13 - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) '@types/pg': 8.16.0 kysely: 0.28.14 pg: 8.16.3 @@ -17053,7 +17022,7 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - next@16.2.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.2.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 16.2.3 '@swc/helpers': 0.5.15 @@ -17072,7 +17041,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.3 '@next/swc-win32-arm64-msvc': 16.2.3 '@next/swc-win32-x64-msvc': 16.2.3 - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -17452,7 +17421,7 @@ snapshots: graphql: 16.11.0 hono: 4.12.14 - ponder@0.16.6(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6): + ponder@0.16.6(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6): dependencies: '@babel/code-frame': 7.29.0 '@commander-js/extra-typings': 12.1.0(commander@12.1.0) @@ -17469,7 +17438,7 @@ snapshots: dataloader: 2.2.3 detect-package-manager: 3.0.2 dotenv: 16.6.1 - drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) glob: 10.5.0 graphql: 16.8.2 graphql-yoga: 5.17.1(graphql@16.8.2) @@ -17648,7 +17617,7 @@ snapshots: prom-client@15.1.3: dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) tdigest: 0.1.2 prompts@2.4.2: @@ -18801,7 +18770,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.2 + esbuild: 0.27.4 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 From c57108e7f2f08e862a39910dacb35b8f576f0080 Mon Sep 17 00:00:00 2001 From: djstrong Date: Wed, 29 Apr 2026 21:17:52 +0200 Subject: [PATCH 02/27] feat: increase label submission limits and update related tests - Updated the maximum number of raw labels accepted per `POST /api/submissions` from 50 to 100. - Adjusted the `LABELS_BY_HASHES_MAX` limit in the ENSApi from 100 to 200 to accommodate larger batch lookups. - Modified tests to reflect the new limits for both submissions and label hash queries. - Enhanced documentation to clarify the relationship between submission limits and hash expansions. --- .changeset/ens-labels-collector-app.md | 2 +- .../src/handlers/submissions.test.ts | 2 +- .../src/handlers/submissions.ts | 14 +++++++------- .../src/lib/omnigraph-client.ts | 7 ++++--- .../omnigraph-api/schema/label.integration.test.ts | 2 +- apps/ensapi/src/omnigraph-api/schema/label.ts | 2 +- .../enssdk/src/omnigraph/generated/schema.graphql | 2 +- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.changeset/ens-labels-collector-app.md b/.changeset/ens-labels-collector-app.md index e16e6b1e2b..ecec31e4ab 100644 --- a/.changeset/ens-labels-collector-app.md +++ b/.changeset/ens-labels-collector-app.md @@ -7,5 +7,5 @@ Add `apps/ens-labels-collector` and a new `Query.labels` Omnigraph field to support label submission collection (issue [#2003](https://github.com/namehash/ensnode/issues/2003)). - **New app `apps/ens-labels-collector`**: Hono server exposing `POST /api/submissions` that accepts `{ labels: string[], callerAddress: Address }`, classifies each label against ENSNode's index via the typed `enssdk/omnigraph` client, and emits a structured JSON line per submission to stdout. For each submitted raw label the collector computes both the literal labelhash and (when normalizable to a different value) the normalized labelhash, then assigns one of three statuses per label: `unknown_in_index` (referenced in the index but unhealed), `healed_in_index`, or `absent_from_index`. Persistent storage, batched on-chain emission, and a caller-leaderboard are explicitly deferred to follow-up work; the JSON log shape is the future row shape so adding a sink later is mechanical. -- **New ENSApi `Query.labels(by: { hashes: [Hex!]! }): [Label!]!`**: batch lookup of `Label` rows by `LabelHash`. Hashes that are not present in the index are simply omitted from the result. Capped at 100 hashes per request. +- **New ENSApi `Query.labels(by: { hashes: [Hex!]! }): [Label!]!`**: batch lookup of `Label` rows by `LabelHash`. Hashes that are not present in the index are simply omitted from the result. Capped at 200 hashes per request. - **`enssdk/omnigraph`**: regenerated GraphQL introspection so the new `Query.labels` field is available to the typed `graphql(...)` client. diff --git a/apps/ens-labels-collector/src/handlers/submissions.test.ts b/apps/ens-labels-collector/src/handlers/submissions.test.ts index bda04ae659..32ea165e11 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.test.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.test.ts @@ -182,7 +182,7 @@ describe("POST /api/submissions", () => { }); it("rejects oversized batches", async () => { - const labels = Array.from({ length: 100 }, (_, i) => `label-${i}`); + const labels = Array.from({ length: 101 }, (_, i) => `label-${i}`); const app = makeApp(); const res = await app.request("/api/submissions", { diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts index f98d43bd7f..71663ce02e 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -13,15 +13,15 @@ import { import { lookupLabels } from "@/lib/omnigraph-client"; /** - * Mirror of `LABELS_BY_HASHES_MAX` in `apps/ensapi/src/omnigraph-api/schema/label.ts`. + * Maximum number of raw labels accepted per `POST /api/submissions` request. * - * The collector pre-caps to the same limit so requests fail fast with a clear 400 instead of - * trekking to ENSApi only to be rejected. - * - * Each submitted label can produce up to 2 hashes (raw + normalized variant), so we accept at - * most `LABELS_BY_HASHES_MAX / 2` raw labels per request. + * This is independent of how many labelhashes each label expands into (1 if already + * normalized / unnormalizable, 2 if it has a distinct normalized form). The resolver + * cap (`LABELS_BY_HASHES_MAX = 200` in `apps/ensapi/src/omnigraph-api/schema/label.ts`) + * is sized to comfortably accommodate the worst case (2 * `MAX_LABELS_PER_SUBMISSION`) + * so callers always get the same per-submission limit regardless of normalization. */ -export const MAX_LABELS_PER_SUBMISSION = 50; +export const MAX_LABELS_PER_SUBMISSION = 100; const SubmissionsRequestSchema = z.object({ labels: z.array(z.string().min(1).max(1000)).min(1).max(MAX_LABELS_PER_SUBMISSION), diff --git a/apps/ens-labels-collector/src/lib/omnigraph-client.ts b/apps/ens-labels-collector/src/lib/omnigraph-client.ts index 4f808e2b42..777a820d32 100644 --- a/apps/ens-labels-collector/src/lib/omnigraph-client.ts +++ b/apps/ens-labels-collector/src/lib/omnigraph-client.ts @@ -35,9 +35,10 @@ function getClient() { /** * Looks up Labels by a batch of LabelHashes against ENSNode's Omnigraph. * - * The Omnigraph resolver enforces a hard cap (see `LABELS_BY_HASHES_MAX` in - * `apps/ensapi/src/omnigraph-api/schema/label.ts`); the collector should pre-cap to the same - * limit to fail fast on the client side before making the request. + * The Omnigraph resolver enforces a hard cap on `hashes.length` (see `LABELS_BY_HASHES_MAX` + * in `apps/ensapi/src/omnigraph-api/schema/label.ts`). The submissions handler caps raw + * labels per request via `MAX_LABELS_PER_SUBMISSION`, sized so that the worst-case expansion + * (each label producing both a raw and a normalized hash) stays within the resolver cap. */ export async function lookupLabels(hashes: readonly string[]): Promise { if (hashes.length === 0) return []; diff --git a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts index fc44d4da43..185bc88c4c 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts @@ -86,7 +86,7 @@ describe("Query.labels", () => { it("rejects requests over the maximum allowed hash count", async () => { // generate (LABELS_BY_HASHES_MAX + 1) distinct labelhashes deterministically const hashes: LabelHash[] = []; - for (let i = 0; i <= 100; i++) { + for (let i = 0; i <= 200; i++) { const hex = i.toString(16).padStart(64, "0"); hashes.push(`0x${hex}` as LabelHash); } diff --git a/apps/ensapi/src/omnigraph-api/schema/label.ts b/apps/ensapi/src/omnigraph-api/schema/label.ts index 85ebd10e63..2bb194fb67 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.ts @@ -39,7 +39,7 @@ LabelRef.implement({ * Caps the resolver's `inArray` query so a single GraphQL request cannot enumerate * the entire `label` table. */ -export const LABELS_BY_HASHES_MAX = 100; +export const LABELS_BY_HASHES_MAX = 200; export const LabelsByHashesInput = builder.inputType("LabelsByHashesInput", { description: "Look up Labels by a batch of LabelHashes.", diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql index 1024063f86..f07102777f 100644 --- a/packages/enssdk/src/omnigraph/generated/schema.graphql +++ b/packages/enssdk/src/omnigraph/generated/schema.graphql @@ -695,7 +695,7 @@ type Label { """Look up Labels by a batch of LabelHashes.""" input LabelsByHashesInput { """ - LabelHashes to look up. Up to 100 hashes per request. Absent labels are simply omitted from the result. + LabelHashes to look up. Up to 200 hashes per request. Absent labels are simply omitted from the result. """ hashes: [Hex!]! } From c0c013e922f297d5ba1ff1b8e377c1a074512eac Mon Sep 17 00:00:00 2001 From: djstrong Date: Wed, 29 Apr 2026 21:44:18 +0200 Subject: [PATCH 03/27] chore: update pnpm-lock.yaml with new dependency hashes and version adjustments --- pnpm-lock.yaml | 526 ++++++++++++++++++++++++++++--------------------- 1 file changed, 297 insertions(+), 229 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 734e3b0206..6747bbf819 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,13 +138,13 @@ overrides: patchedDependencies: '@changesets/assemble-release-plan@6.0.9': - hash: 7lkorkta6ossod7lbecbwpu4eq + hash: b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52 path: patches/@changesets__assemble-release-plan@6.0.9.patch '@opentelemetry/api': - hash: izegmddoiqidmvapuxeha6lnw4 + hash: 4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a path: patches/@opentelemetry__api.patch '@opentelemetry/otlp-exporter-base': - hash: brtaqazy64vzhaznf7q5df2byy + hash: b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d path: patches/@opentelemetry__otlp-exporter-base.patch importers: @@ -211,7 +211,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) + version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) apps/ensadmin: dependencies: @@ -313,7 +313,7 @@ importers: version: 0.548.0(react@19.2.1) next: specifier: ^16.2.3 - version: 16.2.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 16.2.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -401,37 +401,37 @@ importers: version: link:../../packages/ens-referrals '@opentelemetry/api': specifier: ^1.9.0 - version: 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + version: 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/core': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/exporter-metrics-otlp-proto': specifier: ^0.202.0 - version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/exporter-trace-otlp-proto': specifier: ^0.202.0 - version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/sdk-node': specifier: ^0.202.0 - version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/sdk-trace-base': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/sdk-trace-node': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + version: 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.37.0 '@ponder/client': specifier: 'catalog:' - version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) '@pothos/core': specifier: ^4.10.0 version: 4.10.0(graphql@16.11.0) @@ -446,7 +446,7 @@ importers: version: 1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0) '@pothos/tracing-opentelemetry': specifier: ^1.1.3 - version: 1.1.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0) + version: 1.1.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0) '@standard-schema/utils': specifier: ^0.3.0 version: 0.3.0 @@ -458,7 +458,7 @@ importers: version: 4.1.0 drizzle-orm: specifier: 'catalog:' - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) enssdk: specifier: workspace:* version: link:../../packages/enssdk @@ -546,7 +546,7 @@ importers: version: link:../../packages/ponder-sdk '@ponder/client': specifier: 'catalog:' - version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) caip: specifier: 'catalog:' version: 1.1.1 @@ -561,7 +561,7 @@ importers: version: 5.6.1 drizzle-orm: specifier: 'catalog:' - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) enssdk: specifier: workspace:* version: link:../../packages/enssdk @@ -573,7 +573,7 @@ importers: version: 7.1.1 ponder: specifier: 'catalog:' - version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) + version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) viem: specifier: 'catalog:' version: 2.38.5(typescript@5.9.3)(zod@4.3.6) @@ -747,7 +747,7 @@ importers: version: 0.2.9(astro@5.18.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) '@tailwindcss/vite': specifier: ^4.1.15 - version: 4.1.16(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.16(vite@7.3.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)) astro: specifier: 'catalog:' version: 5.18.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.59.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) @@ -962,10 +962,10 @@ importers: version: 0.31.10 drizzle-orm: specifier: 'catalog:' - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) ponder: specifier: 'catalog:' - version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) + version: 0.16.6(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6) tsup: specifier: 'catalog:' version: 8.5.0(jiti@2.6.1)(postcss@8.5.12)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) @@ -1327,13 +1327,13 @@ importers: version: 2.5.1 '@ponder/client': specifier: 'catalog:' - version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) dataloader: specifier: ^2.2.3 version: 2.2.3 drizzle-orm: specifier: 0.41.0 - version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) graphql: specifier: ^16.10.0 version: 16.11.0 @@ -1720,24 +1720,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.3.2': resolution: {integrity: sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.2': resolution: {integrity: sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.3.2': resolution: {integrity: sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.3.2': resolution: {integrity: sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==} @@ -2750,232 +2754,274 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.3': resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.3': resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.3': resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.3': resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.3': resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.3': resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.3': resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm64@0.34.4': resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.4': resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.4': resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.4': resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.4': resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.4': resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.4': resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -3121,24 +3167,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.2.3': resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.2.3': resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.2.3': resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.2.3': resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==} @@ -4072,66 +4122,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -4491,48 +4554,56 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.16': resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.16': resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.16': resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.16': resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} @@ -7085,24 +7156,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -10484,7 +10559,7 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.3 - '@changesets/assemble-release-plan@6.0.9(patch_hash=7lkorkta6ossod7lbecbwpu4eq)': + '@changesets/assemble-release-plan@6.0.9(patch_hash=b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52)': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 @@ -10508,7 +10583,7 @@ snapshots: '@changesets/cli@2.29.8(@types/node@24.10.9)': dependencies: '@changesets/apply-release-plan': 7.0.14 - '@changesets/assemble-release-plan': 6.0.9(patch_hash=7lkorkta6ossod7lbecbwpu4eq) + '@changesets/assemble-release-plan': 6.0.9(patch_hash=b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52) '@changesets/changelog-git': 0.2.1 '@changesets/config': 3.1.2 '@changesets/errors': 0.2.0 @@ -10568,7 +10643,7 @@ snapshots: '@changesets/get-release-plan@4.0.14': dependencies: - '@changesets/assemble-release-plan': 6.0.9(patch_hash=7lkorkta6ossod7lbecbwpu4eq) + '@changesets/assemble-release-plan': 6.0.9(patch_hash=b91e9036dbd12ef14c78acf46baf46fe65bf204d25b315027bc8266c8b0fee52) '@changesets/config': 3.1.2 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.6 @@ -11373,7 +11448,7 @@ snapshots: '@hono/otel@0.2.2(hono@4.12.14)': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/semantic-conventions': 1.37.0 hono: 4.12.14 @@ -11925,263 +12000,263 @@ snapshots: '@opentelemetry/api-logs@0.202.0': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)': {} + '@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)': {} - '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) - '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/exporter-logs-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-logs-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/exporter-logs-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-logs-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/exporter-logs-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - - '@opentelemetry/exporter-metrics-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - - '@opentelemetry/exporter-metrics-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - - '@opentelemetry/exporter-prometheus@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - - '@opentelemetry/exporter-trace-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + + '@opentelemetry/exporter-metrics-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + + '@opentelemetry/exporter-metrics-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + + '@opentelemetry/exporter-prometheus@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + + '@opentelemetry/exporter-trace-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - - '@opentelemetry/exporter-trace-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - - '@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - - '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': - dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + + '@opentelemetry/exporter-trace-otlp-http@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + + '@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + + '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': + dependencies: + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/instrumentation@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/instrumentation@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/api-logs': 0.202.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/otlp-exporter-base@0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-grpc-exporter-base@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/otlp-grpc-exporter-base@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=brtaqazy64vzhaznf7q5df2byy)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-exporter-base': 0.202.0(patch_hash=b8c870e92957fbc0bd683ab4e2c0034dc892f663b22f2c8b30daeb8f321bbb8d)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) protobufjs: 7.5.5 - '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-logs@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-logs@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-node@0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-node@0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-logs-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-logs-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-logs-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-metrics-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-prometheus': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-trace-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-trace-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-logs-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-logs-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-logs-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-metrics-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-prometheus': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-trace-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-trace-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) - '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))': + '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) - '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4)) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a)) '@opentelemetry/semantic-conventions@1.37.0': {} @@ -12212,9 +12287,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@ponder/client@0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3)': + '@ponder/client@0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3)': dependencies: - drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) eventsource: 3.0.7 superjson: 2.2.6 optionalDependencies: @@ -12275,9 +12350,9 @@ snapshots: '@pothos/core': 4.10.0(graphql@16.11.0) graphql: 16.11.0 - '@pothos/tracing-opentelemetry@1.1.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0)': + '@pothos/tracing-opentelemetry@1.1.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@opentelemetry/semantic-conventions@1.37.0)(@pothos/core@4.10.0(graphql@16.11.0))(@pothos/plugin-tracing@1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0))(graphql@16.11.0)': dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@opentelemetry/semantic-conventions': 1.37.0 '@pothos/core': 4.10.0(graphql@16.11.0) '@pothos/plugin-tracing': 1.1.2(@pothos/core@4.10.0(graphql@16.11.0))(graphql@16.11.0) @@ -13433,13 +13508,6 @@ snapshots: postcss: 8.5.12 tailwindcss: 4.1.18 - '@tailwindcss/vite@4.1.16(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@tailwindcss/node': 4.1.16 - '@tailwindcss/oxide': 4.1.16 - tailwindcss: 4.1.16 - vite: 6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) - '@tailwindcss/vite@4.1.16(vite@7.3.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.1.16 @@ -15158,10 +15226,10 @@ snapshots: esbuild: 0.25.11 tsx: 4.21.0 - drizzle-orm@0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3): + drizzle-orm@0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3): optionalDependencies: '@electric-sql/pglite': 0.2.13 - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@types/pg': 8.16.0 kysely: 0.28.14 pg: 8.16.3 @@ -17022,7 +17090,7 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - next@16.2.3(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.2.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 16.2.3 '@swc/helpers': 0.5.15 @@ -17041,7 +17109,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.3 '@next/swc-win32-arm64-msvc': 16.2.3 '@next/swc-win32-x64-msvc': 16.2.3 - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -17421,7 +17489,7 @@ snapshots: graphql: 16.11.0 hono: 4.12.14 - ponder@0.16.6(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6): + ponder@0.16.6(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(yaml@2.8.3)(zod@4.3.6): dependencies: '@babel/code-frame': 7.29.0 '@commander-js/extra-typings': 12.1.0(commander@12.1.0) @@ -17438,7 +17506,7 @@ snapshots: dataloader: 2.2.3 detect-package-manager: 3.0.2 dotenv: 16.6.1 - drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) glob: 10.5.0 graphql: 16.8.2 graphql-yoga: 5.17.1(graphql@16.8.2) @@ -17617,7 +17685,7 @@ snapshots: prom-client@15.1.3: dependencies: - '@opentelemetry/api': 1.9.0(patch_hash=izegmddoiqidmvapuxeha6lnw4) + '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) tdigest: 0.1.2 prompts@2.4.2: From 3537486c77b6e7520b21e6395f619efed649fcef Mon Sep 17 00:00:00 2001 From: djstrong Date: Wed, 29 Apr 2026 21:58:14 +0200 Subject: [PATCH 04/27] feat(yoga): add masked error handling for production environment --- apps/ensapi/src/omnigraph-api/yoga.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/ensapi/src/omnigraph-api/yoga.ts b/apps/ensapi/src/omnigraph-api/yoga.ts index 528e80546d..6e767d5ed9 100644 --- a/apps/ensapi/src/omnigraph-api/yoga.ts +++ b/apps/ensapi/src/omnigraph-api/yoga.ts @@ -16,6 +16,16 @@ export const yoga = createYoga({ context, // CORS is handled by the Hono middleware in app.ts cors: false, + maskedErrors: + process.env.NODE_ENV === "production" + ? true + : { + maskError(error: unknown) { + console.error(error); + if (error instanceof Error) return error; + return new Error("Internal Server Error"); + }, + }, graphiql: { defaultQuery: `query DomainsByOwner { account(by: { address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }) { From 440023c640b0ac439aec3dbedf1e6fcb520e7584 Mon Sep 17 00:00:00 2001 From: djstrong Date: Wed, 29 Apr 2026 22:41:47 +0200 Subject: [PATCH 05/27] feat(ens-labels-collector): enhance error handling and add timeout for submissions - Updated error handling in the app to prevent leaking underlying error messages to clients, responding with a generic 500 status instead. - Introduced a timeout mechanism for the `POST /api/submissions` endpoint to prevent stalled requests from holding resources indefinitely. - Added a new utility function to handle promise timeouts, ensuring graceful degradation in case of delays during label lookups. - Updated tests to include new error handling and timeout scenarios. --- apps/ens-labels-collector/package.json | 1 + apps/ens-labels-collector/src/app.ts | 3 +- apps/ens-labels-collector/src/config.ts | 13 ++-- .../src/handlers/submissions.test.ts | 6 +- .../src/handlers/submissions.ts | 28 +++++++- apps/ens-labels-collector/src/index.ts | 53 ++++++++++---- .../src/lib/omnigraph-client.ts | 9 ++- .../schema/label.integration.test.ts | 70 ++++++++++--------- 8 files changed, 127 insertions(+), 56 deletions(-) diff --git a/apps/ens-labels-collector/package.json b/apps/ens-labels-collector/package.json index e2f190c72e..eb97a14864 100644 --- a/apps/ens-labels-collector/package.json +++ b/apps/ens-labels-collector/package.json @@ -20,6 +20,7 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { + "@ensnode/ensnode-sdk": "workspace:*", "@hono/node-server": "catalog:", "enssdk": "workspace:*", "graphql": "^16.11.0", diff --git a/apps/ens-labels-collector/src/app.ts b/apps/ens-labels-collector/src/app.ts index 9eb27ee84f..f5393b00f0 100644 --- a/apps/ens-labels-collector/src/app.ts +++ b/apps/ens-labels-collector/src/app.ts @@ -14,7 +14,8 @@ app.notFound((c) => errorResponse(c, { message: "Not Found", status: 404 })); app.onError((error, c) => { console.error("[ens-labels-collector] unhandled error", error); - return errorResponse(c, { error }); + // Do not leak the underlying error message to clients; respond with a generic 500. + return errorResponse(c, { message: "Internal Server Error", status: 500 }); }); export default app; diff --git a/apps/ens-labels-collector/src/config.ts b/apps/ens-labels-collector/src/config.ts index d40e2d84a0..4bd58241ef 100644 --- a/apps/ens-labels-collector/src/config.ts +++ b/apps/ens-labels-collector/src/config.ts @@ -1,11 +1,14 @@ import { z } from "zod"; +import { OptionalPortNumberSchema } from "@ensnode/ensnode-sdk/internal"; + +/** + * Default port for the ens-labels-collector HTTP server. Used when `PORT` env var is unset. + */ +export const ENS_LABELS_COLLECTOR_DEFAULT_PORT = 4444; + const ConfigSchema = z.object({ - PORT: z - .string() - .optional() - .transform((value) => (value === undefined ? 4444 : Number.parseInt(value, 10))) - .pipe(z.number().int().min(1).max(65535)), + PORT: OptionalPortNumberSchema.default(ENS_LABELS_COLLECTOR_DEFAULT_PORT), ENSNODE_URL: z.string().url(), }); diff --git a/apps/ens-labels-collector/src/handlers/submissions.test.ts b/apps/ens-labels-collector/src/handlers/submissions.test.ts index 32ea165e11..f5596dd1f1 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.test.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.test.ts @@ -6,7 +6,7 @@ import { labelhashLiteralLabel, } from "enssdk"; import { Hono } from "hono"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@/lib/omnigraph-client", () => ({ lookupLabels: vi.fn(), @@ -37,6 +37,10 @@ describe("POST /api/submissions", () => { consoleSpy.mockClear(); }); + afterAll(() => { + consoleSpy.mockRestore(); + }); + it("400s on malformed JSON", async () => { const app = makeApp(); const res = await app.request("/api/submissions", { diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts index 71663ce02e..8f6216c7c4 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -23,6 +23,28 @@ import { lookupLabels } from "@/lib/omnigraph-client"; */ export const MAX_LABELS_PER_SUBMISSION = 100; +/** + * Hard upper bound on how long a single `POST /api/submissions` will wait on the + * Omnigraph labels lookup before failing the request. Prevents a stalled upstream + * from holding handler resources indefinitely. + */ +export const OMNIGRAPH_LOOKUP_TIMEOUT_MS = 10_000; + +/** + * Races `promise` against a `setTimeout`-backed timeout, rejecting with `Error(message)` on + * expiry. The underlying promise is NOT cancelled (the Omnigraph SDK does not currently expose + * an `AbortSignal`); we simply stop waiting on it. The unhandled resolution is harmless. + */ +function withTimeout(promise: Promise, ms: number, message: string): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(message)), ms); + }); + return Promise.race([promise, timeout]).finally(() => { + if (timer !== undefined) clearTimeout(timer); + }); +} + const SubmissionsRequestSchema = z.object({ labels: z.array(z.string().min(1).max(1000)).min(1).max(MAX_LABELS_PER_SUBMISSION), callerAddress: z @@ -107,7 +129,11 @@ export async function submissionsHandler(c: Context) { const hashed = labels.map(hashLabel); const hashes = collectLookupHashes(hashed); - const hits = await lookupLabels(hashes); + const hits = await withTimeout( + lookupLabels(hashes), + OMNIGRAPH_LOOKUP_TIMEOUT_MS, + `Omnigraph labels lookup timed out after ${OMNIGRAPH_LOOKUP_TIMEOUT_MS}ms`, + ); const classifications = classifySubmissions(hashed, hits); const results = classifications.map(toResultItem); diff --git a/apps/ens-labels-collector/src/index.ts b/apps/ens-labels-collector/src/index.ts index e1b874a4c9..a2fedf6bfd 100644 --- a/apps/ens-labels-collector/src/index.ts +++ b/apps/ens-labels-collector/src/index.ts @@ -16,28 +16,55 @@ const server = serve( }, ); +/** + * Promisified `server.close()` that treats `ERR_SERVER_NOT_RUNNING` as a no-op so concurrent + * shutdown paths (e.g. SIGINT immediately followed by SIGTERM) don't reject the second close. + */ const closeServer = () => new Promise((resolve, reject) => server.close((err) => { - if (err) reject(err); - else resolve(); + if (!err) { + resolve(); + return; + } + if (typeof err === "object" && "code" in err && err.code === "ERR_SERVER_NOT_RUNNING") { + resolve(); + return; + } + reject(err); }), ); -const gracefulShutdown = async () => { - try { - await closeServer(); - process.exit(0); - } catch (error) { - console.error("[ens-labels-collector] shutdown error", error); - process.exit(1); +let shutdownPromise: Promise | undefined; + +/** + * Closes the HTTP server and exits with `exitCode` (default 0). + * + * Guarded with a single in-flight promise so repeated SIGINT/SIGTERM (or `uncaughtException` + * arriving during an in-progress shutdown) never trigger a duplicate close. + */ +const gracefulShutdown = (exitCode: number = 0): Promise => { + if (!shutdownPromise) { + shutdownPromise = closeServer() + .catch((error) => { + console.error("[ens-labels-collector] shutdown error", error); + exitCode = exitCode === 0 ? 1 : exitCode; + }) + .then(() => { + process.exit(exitCode); + }); } + return shutdownPromise; }; -process.on("SIGINT", gracefulShutdown); -process.on("SIGTERM", gracefulShutdown); +process.on("SIGINT", () => { + void gracefulShutdown(0); +}); +process.on("SIGTERM", () => { + void gracefulShutdown(0); +}); -process.on("uncaughtException", async (error) => { +process.on("uncaughtException", (error) => { console.error("[ens-labels-collector] uncaughtException", error); - await gracefulShutdown(); + void gracefulShutdown(1); }); diff --git a/apps/ens-labels-collector/src/lib/omnigraph-client.ts b/apps/ens-labels-collector/src/lib/omnigraph-client.ts index 777a820d32..625de509fd 100644 --- a/apps/ens-labels-collector/src/lib/omnigraph-client.ts +++ b/apps/ens-labels-collector/src/lib/omnigraph-client.ts @@ -1,5 +1,6 @@ import { getConfig } from "@/config"; +import type { LabelHash } from "enssdk"; import { createEnsNodeClient } from "enssdk/core"; import { graphql, omnigraph } from "enssdk/omnigraph"; @@ -40,12 +41,14 @@ function getClient() { * labels per request via `MAX_LABELS_PER_SUBMISSION`, sized so that the worst-case expansion * (each label producing both a raw and a normalized hash) stays within the resolver cap. */ -export async function lookupLabels(hashes: readonly string[]): Promise { +export async function lookupLabels(hashes: readonly LabelHash[]): Promise { if (hashes.length === 0) return []; const result = await getClient().omnigraph.query({ query: LabelsByHashes, - variables: { hashes: hashes as `0x${string}`[] }, + // The generated `LabelsByHashes` document types `hashes` as a mutable `Hex[]`, so we copy + // the readonly input into a fresh mutable array. No runtime cost beyond an `Array.from`. + variables: { hashes: [...hashes] }, }); if (result.errors && result.errors.length > 0) { @@ -54,7 +57,7 @@ export async function lookupLabels(hashes: readonly string[]): Promise]`; an `interpreted` value matching this regex is the +// "unhealed" representation. Used below to assert that a returned label is healed without +// reconstructing the encoded form per-row. +const ENCODED_LABEL_HASH_REGEX = /^\[[0-9a-fA-F]{64}\]$/; + describe("Query.labels", () => { it("returns a healed label entry for a known LabelHash", async () => { - const result = await request(LabelsByHashes, { - hashes: [ETH_LABEL_HASH], - }); - - expect(result.labels).toHaveLength(1); - expect(result.labels[0]).toMatchObject({ - hash: ETH_LABEL_HASH, - interpreted: "eth", + await expect( + request(LabelsByHashes, { hashes: [ETH_LABEL_HASH] }), + ).resolves.toMatchObject({ + labels: [{ hash: ETH_LABEL_HASH, interpreted: "eth" }], }); }); it("omits LabelHashes that are not present in the index", async () => { - const result = await request(LabelsByHashes, { - hashes: [ABSENT_LABEL_HASH], - }); - - expect(result.labels).toEqual([]); + await expect( + request(LabelsByHashes, { hashes: [ABSENT_LABEL_HASH] }), + ).resolves.toEqual({ labels: [] }); }); it("returns only the present labels when input mixes present and absent hashes", async () => { - const result = await request(LabelsByHashes, { - hashes: [ETH_LABEL_HASH, ABSENT_LABEL_HASH], + await expect( + request(LabelsByHashes, { + hashes: [ETH_LABEL_HASH, ABSENT_LABEL_HASH], + }), + ).resolves.toMatchObject({ + labels: [{ hash: ETH_LABEL_HASH }], }); - - expect(result.labels).toHaveLength(1); - expect(result.labels[0].hash).toBe(ETH_LABEL_HASH); }); it("dedupes repeated input hashes", async () => { - const result = await request(LabelsByHashes, { - hashes: [ETH_LABEL_HASH, ETH_LABEL_HASH, ETH_LABEL_HASH], + await expect( + request(LabelsByHashes, { + hashes: [ETH_LABEL_HASH, ETH_LABEL_HASH, ETH_LABEL_HASH], + }), + ).resolves.toMatchObject({ + labels: [{ hash: ETH_LABEL_HASH }], }); - - expect(result.labels).toHaveLength(1); - expect(result.labels[0].hash).toBe(ETH_LABEL_HASH); }); it("returns an empty list when input is empty", async () => { - const result = await request(LabelsByHashes, { hashes: [] as Hex[] }); - expect(result.labels).toEqual([]); + await expect( + request(LabelsByHashes, { hashes: [] as Hex[] }), + ).resolves.toEqual({ labels: [] }); }); it("classifies returned labels: 'eth' is healed (interpreted !== encodeLabelHash(hash))", async () => { - const result = await request(LabelsByHashes, { - hashes: [ETH_LABEL_HASH], + await expect( + request(LabelsByHashes, { hashes: [ETH_LABEL_HASH] }), + ).resolves.toMatchObject({ + labels: [ + { + hash: ETH_LABEL_HASH, + interpreted: expect.not.stringMatching(ENCODED_LABEL_HASH_REGEX), + }, + ], }); - - const [row] = result.labels; - expect(row.interpreted).not.toBe(encodeLabelHash(row.hash)); }); it("rejects requests over the maximum allowed hash count", async () => { // generate (LABELS_BY_HASHES_MAX + 1) distinct labelhashes deterministically const hashes: LabelHash[] = []; - for (let i = 0; i <= 200; i++) { + for (let i = 0; i <= LABELS_BY_HASHES_MAX; i++) { const hex = i.toString(16).padStart(64, "0"); hashes.push(`0x${hex}` as LabelHash); } From 4b49138591a4d80dfb6380c556d3cdafbe08da38 Mon Sep 17 00:00:00 2001 From: djstrong Date: Wed, 29 Apr 2026 23:02:56 +0200 Subject: [PATCH 06/27] chore(pnpm-lock): add ensnode-sdk dependency for ens-labels-collector --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6747bbf819..066079fc6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,6 +178,9 @@ importers: apps/ens-labels-collector: dependencies: + '@ensnode/ensnode-sdk': + specifier: workspace:* + version: link:../../packages/ensnode-sdk '@hono/node-server': specifier: 'catalog:' version: 1.19.14(hono@4.12.14) From 3021d5cb27c7660b9465ae9d890966bcf4d264de Mon Sep 17 00:00:00 2001 From: djstrong Date: Thu, 30 Apr 2026 09:48:56 +0200 Subject: [PATCH 07/27] feat(ens-labels-collector): enhance timeout handling in label lookups --- .changeset/config.json | 1 + .../src/handlers/submissions.ts | 49 ++++++++++++++----- .../src/lib/omnigraph-client.ts | 9 +++- apps/ensapi/src/omnigraph-api/yoga.ts | 9 ++-- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 64f132b826..90d6990e7a 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -11,6 +11,7 @@ "ensrainbow", "ensapi", "fallback-ensapi", + "ens-labels-collector", "enssdk", "enscli", "enskit", diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts index 8f6216c7c4..d78b4bf8df 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -31,18 +31,42 @@ export const MAX_LABELS_PER_SUBMISSION = 100; export const OMNIGRAPH_LOOKUP_TIMEOUT_MS = 10_000; /** - * Races `promise` against a `setTimeout`-backed timeout, rejecting with `Error(message)` on - * expiry. The underlying promise is NOT cancelled (the Omnigraph SDK does not currently expose - * an `AbortSignal`); we simply stop waiting on it. The unhandled resolution is harmless. + * Runs `fn` with an `AbortSignal` that is aborted after `ms` (rejecting with `Error(message)`) + * or when `parentSignal` aborts (propagating the parent's cancellation reason). Both timeout + * expiry and parent cancellation actively abort the underlying work via the signal passed to + * `fn`, so in-flight HTTP requests can be cancelled rather than left dangling. */ -function withTimeout(promise: Promise, ms: number, message: string): Promise { - let timer: ReturnType | undefined; - const timeout = new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error(message)), ms); - }); - return Promise.race([promise, timeout]).finally(() => { - if (timer !== undefined) clearTimeout(timer); - }); +async function withTimeout( + fn: (signal: AbortSignal) => Promise, + ms: number, + message: string, + parentSignal?: AbortSignal, +): Promise { + const controller = new AbortController(); + + const onParentAbort = () => controller.abort(parentSignal?.reason); + if (parentSignal) { + if (parentSignal.aborted) { + controller.abort(parentSignal.reason); + } else { + parentSignal.addEventListener("abort", onParentAbort, { once: true }); + } + } + + const timeoutError = new Error(message); + const timer = setTimeout(() => controller.abort(timeoutError), ms); + + try { + return await fn(controller.signal); + } catch (err) { + if (controller.signal.aborted && controller.signal.reason === timeoutError) { + throw timeoutError; + } + throw err; + } finally { + clearTimeout(timer); + if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort); + } } const SubmissionsRequestSchema = z.object({ @@ -130,9 +154,10 @@ export async function submissionsHandler(c: Context) { const hashes = collectLookupHashes(hashed); const hits = await withTimeout( - lookupLabels(hashes), + (signal) => lookupLabels(hashes, signal), OMNIGRAPH_LOOKUP_TIMEOUT_MS, `Omnigraph labels lookup timed out after ${OMNIGRAPH_LOOKUP_TIMEOUT_MS}ms`, + c.req.raw.signal, ); const classifications = classifySubmissions(hashed, hits); const results = classifications.map(toResultItem); diff --git a/apps/ens-labels-collector/src/lib/omnigraph-client.ts b/apps/ens-labels-collector/src/lib/omnigraph-client.ts index 625de509fd..3defdf7d61 100644 --- a/apps/ens-labels-collector/src/lib/omnigraph-client.ts +++ b/apps/ens-labels-collector/src/lib/omnigraph-client.ts @@ -40,8 +40,14 @@ function getClient() { * in `apps/ensapi/src/omnigraph-api/schema/label.ts`). The submissions handler caps raw * labels per request via `MAX_LABELS_PER_SUBMISSION`, sized so that the worst-case expansion * (each label producing both a raw and a normalized hash) stays within the resolver cap. + * + * Pass an optional `signal` to forward request cancellation (e.g. handler timeout, client + * disconnect) to the underlying HTTP request issued by the Omnigraph SDK. */ -export async function lookupLabels(hashes: readonly LabelHash[]): Promise { +export async function lookupLabels( + hashes: readonly LabelHash[], + signal?: AbortSignal, +): Promise { if (hashes.length === 0) return []; const result = await getClient().omnigraph.query({ @@ -49,6 +55,7 @@ export async function lookupLabels(hashes: readonly LabelHash[]): Promise 0) { diff --git a/apps/ensapi/src/omnigraph-api/yoga.ts b/apps/ensapi/src/omnigraph-api/yoga.ts index 6e767d5ed9..c85dc0663a 100644 --- a/apps/ensapi/src/omnigraph-api/yoga.ts +++ b/apps/ensapi/src/omnigraph-api/yoga.ts @@ -2,7 +2,7 @@ // import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth"; // import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens"; -import { createYoga } from "graphql-yoga"; +import { createYoga, maskError } from "graphql-yoga"; import { makeLogger } from "@/lib/logger"; import { context } from "@/omnigraph-api/context"; @@ -20,10 +20,9 @@ export const yoga = createYoga({ process.env.NODE_ENV === "production" ? true : { - maskError(error: unknown) { - console.error(error); - if (error instanceof Error) return error; - return new Error("Internal Server Error"); + maskError(error, message, isDev) { + logger.error(error); + return maskError(error, message, isDev); }, }, graphiql: { From 8c4108853c91c99820e2197f5a7909e8c4607775 Mon Sep 17 00:00:00 2001 From: djstrong Date: Thu, 30 Apr 2026 10:33:26 +0200 Subject: [PATCH 08/27] refactor: improve configuration parsing and logging --- apps/ens-labels-collector/src/config.ts | 9 ++++++--- apps/ens-labels-collector/src/index.ts | 4 +++- apps/ensapi/src/omnigraph-api/schema/query.ts | 7 ++++++- apps/ensrainbow/src/commands/entrypoint-command.ts | 11 +++++++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/ens-labels-collector/src/config.ts b/apps/ens-labels-collector/src/config.ts index 4bd58241ef..3708a8ac22 100644 --- a/apps/ens-labels-collector/src/config.ts +++ b/apps/ens-labels-collector/src/config.ts @@ -20,15 +20,18 @@ export type Config = { let cachedConfig: Config | undefined; /** - * Parses the process environment into a {@link Config}. + * Parses `process.env` into a {@link Config}. * * Memoized so repeated calls return the same instance and validation only runs once. + * Always reads from `process.env`; tests that need to vary inputs should call + * {@link resetConfigCacheForTesting} after mutating `process.env`. + * * Throws (via Zod) if any required env var is missing or invalid. */ -export function getConfig(env: NodeJS.ProcessEnv = process.env): Config { +export function getConfig(): Config { if (cachedConfig) return cachedConfig; - const parsed = ConfigSchema.parse(env); + const parsed = ConfigSchema.parse(process.env); cachedConfig = { port: parsed.PORT, diff --git a/apps/ens-labels-collector/src/index.ts b/apps/ens-labels-collector/src/index.ts index a2fedf6bfd..b41d66b82e 100644 --- a/apps/ens-labels-collector/src/index.ts +++ b/apps/ens-labels-collector/src/index.ts @@ -12,7 +12,9 @@ const server = serve( port: config.port, }, (info) => { - console.log(`ens-labels-collector listening on port ${info.port}`); + // Operational logs go to stderr so stdout stays a clean JSONL stream of submission records + // (see `submissionsHandler` in `src/handlers/submissions.ts`). + console.error(`ens-labels-collector listening on port ${info.port}`); }, ); diff --git a/apps/ensapi/src/omnigraph-api/schema/query.ts b/apps/ensapi/src/omnigraph-api/schema/query.ts index 44761b50f5..17e5881374 100644 --- a/apps/ensapi/src/omnigraph-api/schema/query.ts +++ b/apps/ensapi/src/omnigraph-api/schema/query.ts @@ -3,6 +3,7 @@ import config from "@/config"; import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { inArray } from "drizzle-orm"; import { makeConcreteRegistryId, makePermissionsId, makeResolverId } from "enssdk"; +import { createGraphQLError } from "graphql-yoga"; import { getRootRegistryId } from "@ensnode/ensnode-sdk"; @@ -159,8 +160,12 @@ builder.queryType({ if (by.hashes.length === 0) return []; if (by.hashes.length > LABELS_BY_HASHES_MAX) { - throw new Error( + // Use `createGraphQLError` so the client-facing validation message survives Yoga's + // default `maskError`, which (correctly) hides plain `Error` instances as + // "Unexpected error.". + throw createGraphQLError( `Too many hashes: received ${by.hashes.length}, max ${LABELS_BY_HASHES_MAX}.`, + { extensions: { code: "BAD_USER_INPUT" } }, ); } diff --git a/apps/ensrainbow/src/commands/entrypoint-command.ts b/apps/ensrainbow/src/commands/entrypoint-command.ts index 8e657eb47f..fa6a6ef0c8 100644 --- a/apps/ensrainbow/src/commands/entrypoint-command.ts +++ b/apps/ensrainbow/src/commands/entrypoint-command.ts @@ -293,10 +293,17 @@ async function runDbBootstrap( throw new BootstrapAbortedError(); } - // Write marker only after a successful attach. + const dbConfig = await buildDbConfig(ensRainbowServer); + + if (signal.aborted) { + throw new BootstrapAbortedError(); + } + + // Write marker only after a successful attach AND a successful `buildDbConfig` so the + // next start does not enter the existing-DB reuse path with a database that has never + // passed readiness checks (e.g. missing precalculated record count). await writeFile(markerFile, ""); - const dbConfig = await buildDbConfig(ensRainbowServer); return { publicConfig: buildEnsRainbowPublicConfig(dbConfig), dbConfig }; } catch (error) { if (!dbAttached) { From 4634cc4eb098c357984c6ff9cdf94b49f442072f Mon Sep 17 00:00:00 2001 From: djstrong Date: Thu, 30 Apr 2026 11:01:13 +0200 Subject: [PATCH 09/27] refactor(ens-labels-collector): streamline configuration handling and improve submissions timeout logic --- apps/ens-labels-collector/src/config.ts | 36 ++------- .../src/handlers/submissions.ts | 80 ++++++------------- apps/ens-labels-collector/src/index.ts | 4 +- .../src/lib/omnigraph-client.ts | 23 +----- 4 files changed, 35 insertions(+), 108 deletions(-) diff --git a/apps/ens-labels-collector/src/config.ts b/apps/ens-labels-collector/src/config.ts index 3708a8ac22..41e7a49be7 100644 --- a/apps/ens-labels-collector/src/config.ts +++ b/apps/ens-labels-collector/src/config.ts @@ -12,38 +12,16 @@ const ConfigSchema = z.object({ ENSNODE_URL: z.string().url(), }); -export type Config = { - port: number; - ensNodeUrl: string; -}; - -let cachedConfig: Config | undefined; +const parsed = ConfigSchema.parse(process.env); /** - * Parses `process.env` into a {@link Config}. - * - * Memoized so repeated calls return the same instance and validation only runs once. - * Always reads from `process.env`; tests that need to vary inputs should call - * {@link resetConfigCacheForTesting} after mutating `process.env`. + * Process configuration parsed from `process.env` at module load. * * Throws (via Zod) if any required env var is missing or invalid. */ -export function getConfig(): Config { - if (cachedConfig) return cachedConfig; - - const parsed = ConfigSchema.parse(process.env); - - cachedConfig = { - port: parsed.PORT, - ensNodeUrl: parsed.ENSNODE_URL, - }; - - return cachedConfig; -} +export const config = { + port: parsed.PORT, + ensNodeUrl: parsed.ENSNODE_URL, +}; -/** - * Resets the memoized config. Test-only. - */ -export function resetConfigCacheForTesting(): void { - cachedConfig = undefined; -} +export type Config = typeof config; diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts index d78b4bf8df..f6facec637 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -9,6 +9,7 @@ import { collectLookupHashes, hashLabel, type LabelClassification, + type LabelHit, } from "@/lib/labels"; import { lookupLabels } from "@/lib/omnigraph-client"; @@ -30,45 +31,6 @@ export const MAX_LABELS_PER_SUBMISSION = 100; */ export const OMNIGRAPH_LOOKUP_TIMEOUT_MS = 10_000; -/** - * Runs `fn` with an `AbortSignal` that is aborted after `ms` (rejecting with `Error(message)`) - * or when `parentSignal` aborts (propagating the parent's cancellation reason). Both timeout - * expiry and parent cancellation actively abort the underlying work via the signal passed to - * `fn`, so in-flight HTTP requests can be cancelled rather than left dangling. - */ -async function withTimeout( - fn: (signal: AbortSignal) => Promise, - ms: number, - message: string, - parentSignal?: AbortSignal, -): Promise { - const controller = new AbortController(); - - const onParentAbort = () => controller.abort(parentSignal?.reason); - if (parentSignal) { - if (parentSignal.aborted) { - controller.abort(parentSignal.reason); - } else { - parentSignal.addEventListener("abort", onParentAbort, { once: true }); - } - } - - const timeoutError = new Error(message); - const timer = setTimeout(() => controller.abort(timeoutError), ms); - - try { - return await fn(controller.signal); - } catch (err) { - if (controller.signal.aborted && controller.signal.reason === timeoutError) { - throw timeoutError; - } - throw err; - } finally { - clearTimeout(timer); - if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort); - } -} - const SubmissionsRequestSchema = z.object({ labels: z.array(z.string().min(1).max(1000)).min(1).max(MAX_LABELS_PER_SUBMISSION), callerAddress: z @@ -79,8 +41,6 @@ const SubmissionsRequestSchema = z.object({ .transform((value) => value.toLowerCase() as Address), }); -export type SubmissionsRequest = z.infer; - export type SubmissionResultItem = { rawLabel: string; labelHash: string; @@ -110,7 +70,7 @@ export type SubmissionsResponse = { * TODO(#2003): a downstream aggregator can compute leaderboards by `callerAddress` from these * lines (per-status counts are already implicit in `items[].status`). */ -export type SubmissionLogLine = { +type SubmissionLogLine = { ts: string; requestId: string; callerAddress: Address; @@ -128,13 +88,6 @@ function toResultItem(c: LabelClassification): SubmissionResultItem { return item; } -function generateRequestId(): string { - if (typeof globalThis.crypto?.randomUUID === "function") { - return globalThis.crypto.randomUUID(); - } - return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; -} - export async function submissionsHandler(c: Context) { let body: unknown; try { @@ -153,17 +106,32 @@ export async function submissionsHandler(c: Context) { const hashed = labels.map(hashLabel); const hashes = collectLookupHashes(hashed); - const hits = await withTimeout( - (signal) => lookupLabels(hashes, signal), - OMNIGRAPH_LOOKUP_TIMEOUT_MS, - `Omnigraph labels lookup timed out after ${OMNIGRAPH_LOOKUP_TIMEOUT_MS}ms`, - c.req.raw.signal, - ); + let hits: LabelHit[]; + try { + // `AbortSignal.any` aborts when either the timeout fires or the client disconnects, so + // the upstream HTTP request is cancelled in both cases instead of being left dangling. + const signal = AbortSignal.any([ + AbortSignal.timeout(OMNIGRAPH_LOOKUP_TIMEOUT_MS), + c.req.raw.signal, + ]); + hits = await lookupLabels(hashes, signal); + } catch (error) { + // Client disconnected mid-flight; the response will be discarded by the framework, but + // re-throw so the upstream cancellation is visible in logs (`app.onError`). + if (c.req.raw.signal.aborted) throw error; + if (error instanceof DOMException && error.name === "TimeoutError") { + return errorResponse(c, { + message: `Omnigraph labels lookup timed out after ${OMNIGRAPH_LOOKUP_TIMEOUT_MS}ms`, + status: 504, + }); + } + return errorResponse(c, { message: "Upstream Omnigraph lookup failed", status: 502 }); + } const classifications = classifySubmissions(hashed, hits); const results = classifications.map(toResultItem); const submittedAt = new Date().toISOString(); - const requestId = generateRequestId(); + const requestId = crypto.randomUUID(); const logLine: SubmissionLogLine = { ts: submittedAt, diff --git a/apps/ens-labels-collector/src/index.ts b/apps/ens-labels-collector/src/index.ts index b41d66b82e..1f65e089d1 100644 --- a/apps/ens-labels-collector/src/index.ts +++ b/apps/ens-labels-collector/src/index.ts @@ -1,11 +1,9 @@ -import { getConfig } from "@/config"; +import { config } from "@/config"; import { serve } from "@hono/node-server"; import app from "@/app"; -const config = getConfig(); - const server = serve( { fetch: app.fetch, diff --git a/apps/ens-labels-collector/src/lib/omnigraph-client.ts b/apps/ens-labels-collector/src/lib/omnigraph-client.ts index 3defdf7d61..aaa31d4a64 100644 --- a/apps/ens-labels-collector/src/lib/omnigraph-client.ts +++ b/apps/ens-labels-collector/src/lib/omnigraph-client.ts @@ -1,4 +1,4 @@ -import { getConfig } from "@/config"; +import { config } from "@/config"; import type { LabelHash } from "enssdk"; import { createEnsNodeClient } from "enssdk/core"; @@ -21,17 +21,7 @@ export const LabelsByHashes = graphql(` } `); -let cachedClient: ReturnType | undefined; - -function makeClient(url: string) { - return createEnsNodeClient({ url }).extend(omnigraph); -} - -function getClient() { - if (cachedClient) return cachedClient; - cachedClient = makeClient(getConfig().ensNodeUrl); - return cachedClient; -} +const client = createEnsNodeClient({ url: config.ensNodeUrl }).extend(omnigraph); /** * Looks up Labels by a batch of LabelHashes against ENSNode's Omnigraph. @@ -50,7 +40,7 @@ export async function lookupLabels( ): Promise { if (hashes.length === 0) return []; - const result = await getClient().omnigraph.query({ + const result = await client.omnigraph.query({ query: LabelsByHashes, // The generated `LabelsByHashes` document types `hashes` as a mutable `Hex[]`, so we copy // the readonly input into a fresh mutable array. No runtime cost beyond an `Array.from`. @@ -66,10 +56,3 @@ export async function lookupLabels( return result.data?.labels ?? []; } - -/** - * Resets the memoized Omnigraph client. Test-only. - */ -export function resetOmnigraphClientCacheForTesting(): void { - cachedClient = undefined; -} From 0b5365b219a09fcb9e5cf64f24e7d719603e8e48 Mon Sep 17 00:00:00 2001 From: djstrong Date: Thu, 30 Apr 2026 11:25:42 +0200 Subject: [PATCH 10/27] docs(ens-labels-collector): clarify submission limits and normalization handling in comments --- apps/ens-labels-collector/src/handlers/submissions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts index f6facec637..c79169ba46 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -19,8 +19,9 @@ import { lookupLabels } from "@/lib/omnigraph-client"; * This is independent of how many labelhashes each label expands into (1 if already * normalized / unnormalizable, 2 if it has a distinct normalized form). The resolver * cap (`LABELS_BY_HASHES_MAX = 200` in `apps/ensapi/src/omnigraph-api/schema/label.ts`) - * is sized to comfortably accommodate the worst case (2 * `MAX_LABELS_PER_SUBMISSION`) - * so callers always get the same per-submission limit regardless of normalization. + * is sized to exactly accommodate the worst case (2 * `MAX_LABELS_PER_SUBMISSION`). + * Keep these limits in sync so callers always get the same per-submission limit + * regardless of normalization. */ export const MAX_LABELS_PER_SUBMISSION = 100; From ff194f986d02c20427bdb56339177b40ef146d01 Mon Sep 17 00:00:00 2001 From: djstrong Date: Thu, 30 Apr 2026 12:40:41 +0200 Subject: [PATCH 11/27] feat(ens-labels-collector): improve error handling for omnigraph lookups --- .../src/handlers/submissions.test.ts | 47 ++++++++++++++++++- .../src/handlers/submissions.ts | 6 +++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/apps/ens-labels-collector/src/handlers/submissions.test.ts b/apps/ens-labels-collector/src/handlers/submissions.test.ts index f5596dd1f1..7476374d2f 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.test.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.test.ts @@ -16,7 +16,11 @@ import { errorResponse } from "@/lib/error-response"; import type { LabelHit } from "@/lib/labels"; import { lookupLabels } from "@/lib/omnigraph-client"; -import { type SubmissionsResponse, submissionsHandler } from "./submissions"; +import { + OMNIGRAPH_LOOKUP_TIMEOUT_MS, + type SubmissionsResponse, + submissionsHandler, +} from "./submissions"; const mockedLookup = vi.mocked(lookupLabels); @@ -31,14 +35,17 @@ const CALLER = "0x1234567890123456789012345678901234567890"; describe("POST /api/submissions", () => { const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); beforeEach(() => { mockedLookup.mockReset(); consoleSpy.mockClear(); + consoleErrorSpy.mockClear(); }); afterAll(() => { consoleSpy.mockRestore(); + consoleErrorSpy.mockRestore(); }); it("400s on malformed JSON", async () => { @@ -198,6 +205,44 @@ describe("POST /api/submissions", () => { expect(mockedLookup).not.toHaveBeenCalled(); }); + it("returns 504 when the omnigraph lookup times out", async () => { + mockedLookup.mockRejectedValue(new DOMException("The operation timed out.", "TimeoutError")); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["foo"], callerAddress: CALLER }), + }); + + expect(res.status).toBe(504); + const json = (await res.json()) as { message: string }; + expect(json.message).toBe( + `Omnigraph labels lookup timed out after ${OMNIGRAPH_LOOKUP_TIMEOUT_MS}ms`, + ); + expect(consoleSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalled(); + }); + + it("returns 502 when the omnigraph lookup fails with a generic error", async () => { + mockedLookup.mockRejectedValue(new Error("upstream exploded")); + + const app = makeApp(); + const res = await app.request("/api/submissions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["foo"], callerAddress: CALLER }), + }); + + expect(res.status).toBe(502); + const json = (await res.json()) as { message: string }; + expect(json.message).toBe("Upstream Omnigraph lookup failed"); + // The underlying error must be logged (not swallowed) so 502s are debuggable, while + // the response itself stays generic and does not leak upstream details. + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + it("dedupes labelhashes before calling the omnigraph client", async () => { mockedLookup.mockResolvedValue([]); diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts index c79169ba46..01dbb3ca73 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -121,11 +121,17 @@ export async function submissionsHandler(c: Context) { // re-throw so the upstream cancellation is visible in logs (`app.onError`). if (c.req.raw.signal.aborted) throw error; if (error instanceof DOMException && error.name === "TimeoutError") { + console.error("[ens-labels-collector] omnigraph lookup timed out", { + timeoutMs: OMNIGRAPH_LOOKUP_TIMEOUT_MS, + }); return errorResponse(c, { message: `Omnigraph labels lookup timed out after ${OMNIGRAPH_LOOKUP_TIMEOUT_MS}ms`, status: 504, }); } + // Log the underlying error so 502s aren't a black box in production. The client + // still sees a generic message (we don't leak upstream error details). + console.error("[ens-labels-collector] omnigraph lookup failed", error); return errorResponse(c, { message: "Upstream Omnigraph lookup failed", status: 502 }); } const classifications = classifySubmissions(hashed, hits); From 053913b748a68dece0baf8a70e7aec381de74f08 Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 10:59:03 +0200 Subject: [PATCH 12/27] update zod --- apps/ens-labels-collector/src/config.ts | 2 +- apps/ens-labels-collector/src/handlers/submissions.ts | 2 +- apps/ens-labels-collector/src/lib/error-response.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ens-labels-collector/src/config.ts b/apps/ens-labels-collector/src/config.ts index 41e7a49be7..777bad48dd 100644 --- a/apps/ens-labels-collector/src/config.ts +++ b/apps/ens-labels-collector/src/config.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { OptionalPortNumberSchema } from "@ensnode/ensnode-sdk/internal"; diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ens-labels-collector/src/handlers/submissions.ts index 01dbb3ca73..6f980af1f6 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ens-labels-collector/src/handlers/submissions.ts @@ -1,7 +1,7 @@ import type { Address } from "enssdk"; import type { Context } from "hono"; import { isAddress } from "viem"; -import { z } from "zod"; +import { z } from "zod/v4"; import { errorResponse } from "@/lib/error-response"; import { diff --git a/apps/ens-labels-collector/src/lib/error-response.ts b/apps/ens-labels-collector/src/lib/error-response.ts index f5898d2ef5..28630b34cd 100644 --- a/apps/ens-labels-collector/src/lib/error-response.ts +++ b/apps/ens-labels-collector/src/lib/error-response.ts @@ -1,6 +1,6 @@ import type { Context } from "hono"; import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status"; -import { ZodError } from "zod"; +import { ZodError } from "zod/v4"; /** * Standardized error response shape for the ens-labels-collector. From caf277d41d59def18dac1919372198e8ec65c699 Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 11:09:27 +0200 Subject: [PATCH 13/27] rename app to ensrainbowbeam --- .changeset/config.json | 2 +- .changeset/ens-labels-collector-app.md | 11 --- .changeset/ensrainbowbeam-app.md | 11 +++ apps/ens-labels-collector/.env.local.example | 6 -- apps/ensrainbowbeam/.env.local.example | 6 ++ .../Dockerfile | 2 +- .../README.md | 14 ++-- .../package.json | 8 +- .../src/app.ts | 2 +- .../src/config.ts | 6 +- .../src/handlers/health.ts | 0 .../src/handlers/submissions.test.ts | 0 .../src/handlers/submissions.ts | 4 +- .../src/index.ts | 6 +- .../src/lib/error-response.ts | 2 +- .../src/lib/labels.test.ts | 0 .../src/lib/labels.ts | 0 .../src/lib/omnigraph-client.ts | 0 .../tsconfig.json | 0 .../vitest.config.ts | 2 +- pnpm-lock.yaml | 80 +++++++++---------- 21 files changed, 82 insertions(+), 80 deletions(-) delete mode 100644 .changeset/ens-labels-collector-app.md create mode 100644 .changeset/ensrainbowbeam-app.md delete mode 100644 apps/ens-labels-collector/.env.local.example create mode 100644 apps/ensrainbowbeam/.env.local.example rename apps/{ens-labels-collector => ensrainbowbeam}/Dockerfile (88%) rename apps/{ens-labels-collector => ensrainbowbeam}/README.md (85%) rename apps/{ens-labels-collector => ensrainbowbeam}/package.json (81%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/app.ts (90%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/config.ts (68%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/handlers/health.ts (100%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/handlers/submissions.test.ts (100%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/handlers/submissions.ts (97%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/index.ts (88%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/lib/error-response.ts (96%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/lib/labels.test.ts (100%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/lib/labels.ts (100%) rename apps/{ens-labels-collector => ensrainbowbeam}/src/lib/omnigraph-client.ts (100%) rename apps/{ens-labels-collector => ensrainbowbeam}/tsconfig.json (100%) rename apps/{ens-labels-collector => ensrainbowbeam}/vitest.config.ts (89%) diff --git a/.changeset/config.json b/.changeset/config.json index 90d6990e7a..b5d7d4bb33 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -11,7 +11,7 @@ "ensrainbow", "ensapi", "fallback-ensapi", - "ens-labels-collector", + "ensrainbowbeam", "enssdk", "enscli", "enskit", diff --git a/.changeset/ens-labels-collector-app.md b/.changeset/ens-labels-collector-app.md deleted file mode 100644 index ecec31e4ab..0000000000 --- a/.changeset/ens-labels-collector-app.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"ens-labels-collector": minor -"ensapi": minor -"enssdk": minor ---- - -Add `apps/ens-labels-collector` and a new `Query.labels` Omnigraph field to support label submission collection (issue [#2003](https://github.com/namehash/ensnode/issues/2003)). - -- **New app `apps/ens-labels-collector`**: Hono server exposing `POST /api/submissions` that accepts `{ labels: string[], callerAddress: Address }`, classifies each label against ENSNode's index via the typed `enssdk/omnigraph` client, and emits a structured JSON line per submission to stdout. For each submitted raw label the collector computes both the literal labelhash and (when normalizable to a different value) the normalized labelhash, then assigns one of three statuses per label: `unknown_in_index` (referenced in the index but unhealed), `healed_in_index`, or `absent_from_index`. Persistent storage, batched on-chain emission, and a caller-leaderboard are explicitly deferred to follow-up work; the JSON log shape is the future row shape so adding a sink later is mechanical. -- **New ENSApi `Query.labels(by: { hashes: [Hex!]! }): [Label!]!`**: batch lookup of `Label` rows by `LabelHash`. Hashes that are not present in the index are simply omitted from the result. Capped at 200 hashes per request. -- **`enssdk/omnigraph`**: regenerated GraphQL introspection so the new `Query.labels` field is available to the typed `graphql(...)` client. diff --git a/.changeset/ensrainbowbeam-app.md b/.changeset/ensrainbowbeam-app.md new file mode 100644 index 0000000000..fcfce615bc --- /dev/null +++ b/.changeset/ensrainbowbeam-app.md @@ -0,0 +1,11 @@ +--- +"ensrainbowbeam": minor +"ensapi": minor +"enssdk": minor +--- + +Add **EnsRainbowBeam** (`apps/ensrainbowbeam`) and a new `Query.labels` Omnigraph field to support label submission collection (issue [#2003](https://github.com/namehash/ensnode/issues/2003)). + +- **New app `apps/ensrainbowbeam` (EnsRainbowBeam)**: Hono server exposing `POST /api/submissions` that accepts `{ labels: string[], callerAddress: Address }`, classifies each label against ENSNode's index via the typed `enssdk/omnigraph` client, and emits a structured JSON line per submission to stdout. For each submitted raw label the service computes both the literal labelhash and (when normalizable to a different value) the normalized labelhash, then assigns one of three statuses per label: `unknown_in_index` (referenced in the index but unhealed), `healed_in_index`, or `absent_from_index`. Persistent storage, batched on-chain emission, and a caller-leaderboard are explicitly deferred to follow-up work; the JSON log shape is the future row shape so adding a sink later is mechanical. +- **New ENSApi `Query.labels(by: { hashes: [Hex!]! }): [Label!]!`**: batch lookup of `Label` rows by `LabelHash`. Hashes that are not present in the index are simply omitted from the result. Capped at 200 hashes per request. +- **`enssdk/omnigraph`**: regenerated GraphQL introspection so the new `Query.labels` field is available to the typed `graphql(...)` client. diff --git a/apps/ens-labels-collector/.env.local.example b/apps/ens-labels-collector/.env.local.example deleted file mode 100644 index e112c9b7fb..0000000000 --- a/apps/ens-labels-collector/.env.local.example +++ /dev/null @@ -1,6 +0,0 @@ -# Port the ens-labels-collector HTTP server listens on. -PORT=4444 - -# Base URL of an ENSNode (ENSApi) instance that exposes the Omnigraph GraphQL endpoint. -# The collector calls `${ENSNODE_URL}/api/omnigraph` to classify submitted labels. -ENSNODE_URL=http://localhost:4334 diff --git a/apps/ensrainbowbeam/.env.local.example b/apps/ensrainbowbeam/.env.local.example new file mode 100644 index 0000000000..4a9468cc22 --- /dev/null +++ b/apps/ensrainbowbeam/.env.local.example @@ -0,0 +1,6 @@ +# Port EnsRainbowBeam listens on. +PORT=4444 + +# Base URL of an ENSNode (ENSApi) instance that exposes the Omnigraph GraphQL endpoint. +# EnsRainbowBeam calls `${ENSNODE_URL}/api/omnigraph` to classify submitted labels. +ENSNODE_URL=http://localhost:4334 diff --git a/apps/ens-labels-collector/Dockerfile b/apps/ensrainbowbeam/Dockerfile similarity index 88% rename from apps/ens-labels-collector/Dockerfile rename to apps/ensrainbowbeam/Dockerfile index f677667ab0..a2ea62c394 100644 --- a/apps/ens-labels-collector/Dockerfile +++ b/apps/ensrainbowbeam/Dockerfile @@ -9,7 +9,7 @@ COPY . . RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile FROM deps AS runner -WORKDIR /app/apps/ens-labels-collector +WORKDIR /app/apps/ensrainbowbeam ENV NODE_ENV=production EXPOSE 4444 diff --git a/apps/ens-labels-collector/README.md b/apps/ensrainbowbeam/README.md similarity index 85% rename from apps/ens-labels-collector/README.md rename to apps/ensrainbowbeam/README.md index 1342b5221c..5a7fba5d54 100644 --- a/apps/ens-labels-collector/README.md +++ b/apps/ensrainbowbeam/README.md @@ -1,6 +1,8 @@ -# ens-labels-collector +# EnsRainbowBeam -Receives ENS Label submissions from external callers, classifies each label against ENSNode's +Workspace package: `ensrainbowbeam` (`apps/ensrainbowbeam`). + +Receives ENS label submissions from external callers, classifies each label against ENSNode's indexed Label table, and (for now) emits a structured JSON line per submission to stdout. The app is intentionally minimal; persistent storage, batched on-chain emission, and a @@ -17,7 +19,7 @@ future row shape so adding a sink later is mechanical. ## How label classification works -For each submitted raw label the collector: +For each submitted raw label EnsRainbowBeam: 1. Computes `labelhashLiteralLabel(rawLabel)`. 2. If the label is normalizable AND the normalized form differs from the raw label, also @@ -44,7 +46,7 @@ See `.env.local.example` for a local-development template. ## Development ```bash -pnpm -F ens-labels-collector dev -pnpm -F ens-labels-collector typecheck -pnpm -F ens-labels-collector test +npx pnpm@10.13.1 -F ensrainbowbeam dev +npx pnpm@10.13.1 -F ensrainbowbeam typecheck +npx pnpm@10.13.1 -F ensrainbowbeam test ``` diff --git a/apps/ens-labels-collector/package.json b/apps/ensrainbowbeam/package.json similarity index 81% rename from apps/ens-labels-collector/package.json rename to apps/ensrainbowbeam/package.json index eb97a14864..704339a743 100644 --- a/apps/ens-labels-collector/package.json +++ b/apps/ensrainbowbeam/package.json @@ -1,16 +1,16 @@ { "private": true, - "name": "ens-labels-collector", + "name": "ensrainbowbeam", "version": "1.10.1", "type": "module", - "description": "Collects ENS Label submissions and classifies them against ENSNode's Omnigraph index", + "description": "EnsRainbowBeam — ingests label submissions and classifies them against ENSNode's Omnigraph index", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/namehash/ensnode.git", - "directory": "apps/ens-labels-collector" + "directory": "apps/ensrainbowbeam" }, - "homepage": "https://github.com/namehash/ensnode/tree/main/apps/ens-labels-collector", + "homepage": "https://github.com/namehash/ensnode/tree/main/apps/ensrainbowbeam", "scripts": { "start": "tsx src/index.ts", "dev": "tsx watch --env-file ./.env.local src/index.ts", diff --git a/apps/ens-labels-collector/src/app.ts b/apps/ensrainbowbeam/src/app.ts similarity index 90% rename from apps/ens-labels-collector/src/app.ts rename to apps/ensrainbowbeam/src/app.ts index f5393b00f0..3f7e8e1761 100644 --- a/apps/ens-labels-collector/src/app.ts +++ b/apps/ensrainbowbeam/src/app.ts @@ -13,7 +13,7 @@ app.post("/api/submissions", submissionsHandler); app.notFound((c) => errorResponse(c, { message: "Not Found", status: 404 })); app.onError((error, c) => { - console.error("[ens-labels-collector] unhandled error", error); + console.error("[ensrainbowbeam] unhandled error", error); // Do not leak the underlying error message to clients; respond with a generic 500. return errorResponse(c, { message: "Internal Server Error", status: 500 }); }); diff --git a/apps/ens-labels-collector/src/config.ts b/apps/ensrainbowbeam/src/config.ts similarity index 68% rename from apps/ens-labels-collector/src/config.ts rename to apps/ensrainbowbeam/src/config.ts index 777bad48dd..1b882fa46e 100644 --- a/apps/ens-labels-collector/src/config.ts +++ b/apps/ensrainbowbeam/src/config.ts @@ -3,12 +3,12 @@ import { z } from "zod/v4"; import { OptionalPortNumberSchema } from "@ensnode/ensnode-sdk/internal"; /** - * Default port for the ens-labels-collector HTTP server. Used when `PORT` env var is unset. + * Default port for EnsRainbowBeam. Used when `PORT` env var is unset. */ -export const ENS_LABELS_COLLECTOR_DEFAULT_PORT = 4444; +export const ENSRAINBOWBEAM_DEFAULT_PORT = 4444; const ConfigSchema = z.object({ - PORT: OptionalPortNumberSchema.default(ENS_LABELS_COLLECTOR_DEFAULT_PORT), + PORT: OptionalPortNumberSchema.default(ENSRAINBOWBEAM_DEFAULT_PORT), ENSNODE_URL: z.string().url(), }); diff --git a/apps/ens-labels-collector/src/handlers/health.ts b/apps/ensrainbowbeam/src/handlers/health.ts similarity index 100% rename from apps/ens-labels-collector/src/handlers/health.ts rename to apps/ensrainbowbeam/src/handlers/health.ts diff --git a/apps/ens-labels-collector/src/handlers/submissions.test.ts b/apps/ensrainbowbeam/src/handlers/submissions.test.ts similarity index 100% rename from apps/ens-labels-collector/src/handlers/submissions.test.ts rename to apps/ensrainbowbeam/src/handlers/submissions.test.ts diff --git a/apps/ens-labels-collector/src/handlers/submissions.ts b/apps/ensrainbowbeam/src/handlers/submissions.ts similarity index 97% rename from apps/ens-labels-collector/src/handlers/submissions.ts rename to apps/ensrainbowbeam/src/handlers/submissions.ts index 6f980af1f6..d1421ee118 100644 --- a/apps/ens-labels-collector/src/handlers/submissions.ts +++ b/apps/ensrainbowbeam/src/handlers/submissions.ts @@ -121,7 +121,7 @@ export async function submissionsHandler(c: Context) { // re-throw so the upstream cancellation is visible in logs (`app.onError`). if (c.req.raw.signal.aborted) throw error; if (error instanceof DOMException && error.name === "TimeoutError") { - console.error("[ens-labels-collector] omnigraph lookup timed out", { + console.error("[ensrainbowbeam] omnigraph lookup timed out", { timeoutMs: OMNIGRAPH_LOOKUP_TIMEOUT_MS, }); return errorResponse(c, { @@ -131,7 +131,7 @@ export async function submissionsHandler(c: Context) { } // Log the underlying error so 502s aren't a black box in production. The client // still sees a generic message (we don't leak upstream error details). - console.error("[ens-labels-collector] omnigraph lookup failed", error); + console.error("[ensrainbowbeam] omnigraph lookup failed", error); return errorResponse(c, { message: "Upstream Omnigraph lookup failed", status: 502 }); } const classifications = classifySubmissions(hashed, hits); diff --git a/apps/ens-labels-collector/src/index.ts b/apps/ensrainbowbeam/src/index.ts similarity index 88% rename from apps/ens-labels-collector/src/index.ts rename to apps/ensrainbowbeam/src/index.ts index 1f65e089d1..6d77fccc3e 100644 --- a/apps/ens-labels-collector/src/index.ts +++ b/apps/ensrainbowbeam/src/index.ts @@ -12,7 +12,7 @@ const server = serve( (info) => { // Operational logs go to stderr so stdout stays a clean JSONL stream of submission records // (see `submissionsHandler` in `src/handlers/submissions.ts`). - console.error(`ens-labels-collector listening on port ${info.port}`); + console.error(`EnsRainbowBeam listening on port ${info.port}`); }, ); @@ -47,7 +47,7 @@ const gracefulShutdown = (exitCode: number = 0): Promise => { if (!shutdownPromise) { shutdownPromise = closeServer() .catch((error) => { - console.error("[ens-labels-collector] shutdown error", error); + console.error("[ensrainbowbeam] shutdown error", error); exitCode = exitCode === 0 ? 1 : exitCode; }) .then(() => { @@ -65,6 +65,6 @@ process.on("SIGTERM", () => { }); process.on("uncaughtException", (error) => { - console.error("[ens-labels-collector] uncaughtException", error); + console.error("[ensrainbowbeam] uncaughtException", error); void gracefulShutdown(1); }); diff --git a/apps/ens-labels-collector/src/lib/error-response.ts b/apps/ensrainbowbeam/src/lib/error-response.ts similarity index 96% rename from apps/ens-labels-collector/src/lib/error-response.ts rename to apps/ensrainbowbeam/src/lib/error-response.ts index 28630b34cd..b20735131a 100644 --- a/apps/ens-labels-collector/src/lib/error-response.ts +++ b/apps/ensrainbowbeam/src/lib/error-response.ts @@ -3,7 +3,7 @@ import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/ht import { ZodError } from "zod/v4"; /** - * Standardized error response shape for the ens-labels-collector. + * Standardized error response shape for EnsRainbowBeam (`ensrainbowbeam`). * * Mirrors the shape used elsewhere in the monorepo (see AGENTS.md "API boundaries"). */ diff --git a/apps/ens-labels-collector/src/lib/labels.test.ts b/apps/ensrainbowbeam/src/lib/labels.test.ts similarity index 100% rename from apps/ens-labels-collector/src/lib/labels.test.ts rename to apps/ensrainbowbeam/src/lib/labels.test.ts diff --git a/apps/ens-labels-collector/src/lib/labels.ts b/apps/ensrainbowbeam/src/lib/labels.ts similarity index 100% rename from apps/ens-labels-collector/src/lib/labels.ts rename to apps/ensrainbowbeam/src/lib/labels.ts diff --git a/apps/ens-labels-collector/src/lib/omnigraph-client.ts b/apps/ensrainbowbeam/src/lib/omnigraph-client.ts similarity index 100% rename from apps/ens-labels-collector/src/lib/omnigraph-client.ts rename to apps/ensrainbowbeam/src/lib/omnigraph-client.ts diff --git a/apps/ens-labels-collector/tsconfig.json b/apps/ensrainbowbeam/tsconfig.json similarity index 100% rename from apps/ens-labels-collector/tsconfig.json rename to apps/ensrainbowbeam/tsconfig.json diff --git a/apps/ens-labels-collector/vitest.config.ts b/apps/ensrainbowbeam/vitest.config.ts similarity index 89% rename from apps/ens-labels-collector/vitest.config.ts rename to apps/ensrainbowbeam/vitest.config.ts index 66da1b27f1..91484cc733 100644 --- a/apps/ens-labels-collector/vitest.config.ts +++ b/apps/ensrainbowbeam/vitest.config.ts @@ -9,7 +9,7 @@ export default defineProject({ }, }, test: { - name: "ens-labels-collector", + name: "ensrainbowbeam", exclude: [...configDefaults.exclude, "**/*.integration.test.ts"], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a75a2af59..110a40f8ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,46 +176,6 @@ importers: specifier: 'catalog:' version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) - apps/ens-labels-collector: - dependencies: - '@ensnode/ensnode-sdk': - specifier: workspace:* - version: link:../../packages/ensnode-sdk - '@hono/node-server': - specifier: 'catalog:' - version: 1.19.14(hono@4.12.14) - enssdk: - specifier: workspace:* - version: link:../../packages/enssdk - graphql: - specifier: ^16.11.0 - version: 16.11.0 - hono: - specifier: 'catalog:' - version: 4.12.14 - viem: - specifier: 'catalog:' - version: 2.38.5(typescript@5.9.3)(zod@4.3.6) - zod: - specifier: 'catalog:' - version: 4.3.6 - devDependencies: - '@ensnode/shared-configs': - specifier: workspace:* - version: link:../../packages/shared-configs - '@types/node': - specifier: 'catalog:' - version: 24.10.9 - tsx: - specifier: ^4.19.3 - version: 4.21.0 - typescript: - specifier: 'catalog:' - version: 5.9.3 - vitest: - specifier: 'catalog:' - version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) - apps/ensadmin: dependencies: '@ensnode/datasources': @@ -670,6 +630,46 @@ importers: specifier: 'catalog:' version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + apps/ensrainbowbeam: + dependencies: + '@ensnode/ensnode-sdk': + specifier: workspace:* + version: link:../../packages/ensnode-sdk + '@hono/node-server': + specifier: 'catalog:' + version: 1.19.14(hono@4.12.14) + enssdk: + specifier: workspace:* + version: link:../../packages/enssdk + graphql: + specifier: ^16.11.0 + version: 16.11.0 + hono: + specifier: 'catalog:' + version: 4.12.14 + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@4.3.6) + zod: + specifier: 'catalog:' + version: 4.3.6 + devDependencies: + '@ensnode/shared-configs': + specifier: workspace:* + version: link:../../packages/shared-configs + '@types/node': + specifier: 'catalog:' + version: 24.10.9 + tsx: + specifier: ^4.19.3 + version: 4.21.0 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vitest: + specifier: 'catalog:' + version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) + apps/fallback-ensapi: dependencies: '@aws-sdk/client-secrets-manager': From a723be47cac7060b5a7023edc7285ed0ac67c306 Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 12:10:03 +0200 Subject: [PATCH 14/27] Add `EnsRainbowBeam` app with `/api/discover` endpoint for label submissions, enhancing label classification and introducing `LabelHash` scalar. Update Docker configurations and workflows to include the new app. --- .changeset/ensapi-omnigraph-labels.md | 5 + .changeset/ensrainbowbeam-app.md | 8 +- .changeset/enssdk-omnigraph-codegen.md | 5 + .github/workflows/release.yml | 3 +- .github/workflows/release_preview.yml | 3 +- .github/workflows/release_snapshot.yml | 4 +- apps/ensapi/src/omnigraph-api/builder.ts | 2 + .../schema/label.integration.test.ts | 99 +++++++++++-------- apps/ensapi/src/omnigraph-api/schema/label.ts | 12 +-- apps/ensapi/src/omnigraph-api/schema/query.ts | 16 +-- .../src/omnigraph-api/schema/scalars.ts | 9 ++ apps/ensapi/src/omnigraph-api/yoga.ts | 10 ++ apps/ensrainbowbeam/Dockerfile | 1 + apps/ensrainbowbeam/LICENSE | 21 ++++ apps/ensrainbowbeam/README.md | 15 +-- apps/ensrainbowbeam/src/app.ts | 2 +- .../src/handlers/submissions.test.ts | 28 +++--- .../src/handlers/submissions.ts | 13 ++- apps/ensrainbowbeam/src/lib/labels.ts | 31 +++--- .../src/lib/omnigraph-client.ts | 65 +++++++----- docker/README.md | 2 + docker/services/ensrainbowbeam.yml | 21 ++++ package.json | 1 + .../src/omnigraph/generated/introspection.ts | 14 ++- .../src/omnigraph/generated/schema.graphql | 15 ++- packages/enssdk/src/omnigraph/graphql.ts | 5 +- scripts/sync-docker-services-tags.mjs | 1 + 27 files changed, 269 insertions(+), 142 deletions(-) create mode 100644 .changeset/ensapi-omnigraph-labels.md create mode 100644 .changeset/enssdk-omnigraph-codegen.md create mode 100644 apps/ensrainbowbeam/LICENSE create mode 100644 docker/services/ensrainbowbeam.yml diff --git a/.changeset/ensapi-omnigraph-labels.md b/.changeset/ensapi-omnigraph-labels.md new file mode 100644 index 0000000000..f493a4fc81 --- /dev/null +++ b/.changeset/ensapi-omnigraph-labels.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Omnigraph **`Query.labels`** improvements: add a **`LabelHash`** GraphQL scalar (`0x` + 64 lowercase hex, parsed via `parseLabelHash`), rename the input to **`LabelsByLabelHashesInput`** with field **`labelHashes`**, enforce stricter parsing/validation through the scalar layer, normalize mixed-case hex at parse time, cap batch size to **`100`** LabelHashes per request for a round-number limit, and keep development error masking aligned with Yoga defaults while ensuring intentional `GraphQLError`s still surface useful client messages where applicable. diff --git a/.changeset/ensrainbowbeam-app.md b/.changeset/ensrainbowbeam-app.md index fcfce615bc..77b9b508f1 100644 --- a/.changeset/ensrainbowbeam-app.md +++ b/.changeset/ensrainbowbeam-app.md @@ -1,11 +1,5 @@ --- "ensrainbowbeam": minor -"ensapi": minor -"enssdk": minor --- -Add **EnsRainbowBeam** (`apps/ensrainbowbeam`) and a new `Query.labels` Omnigraph field to support label submission collection (issue [#2003](https://github.com/namehash/ensnode/issues/2003)). - -- **New app `apps/ensrainbowbeam` (EnsRainbowBeam)**: Hono server exposing `POST /api/submissions` that accepts `{ labels: string[], callerAddress: Address }`, classifies each label against ENSNode's index via the typed `enssdk/omnigraph` client, and emits a structured JSON line per submission to stdout. For each submitted raw label the service computes both the literal labelhash and (when normalizable to a different value) the normalized labelhash, then assigns one of three statuses per label: `unknown_in_index` (referenced in the index but unhealed), `healed_in_index`, or `absent_from_index`. Persistent storage, batched on-chain emission, and a caller-leaderboard are explicitly deferred to follow-up work; the JSON log shape is the future row shape so adding a sink later is mechanical. -- **New ENSApi `Query.labels(by: { hashes: [Hex!]! }): [Label!]!`**: batch lookup of `Label` rows by `LabelHash`. Hashes that are not present in the index are simply omitted from the result. Capped at 200 hashes per request. -- **`enssdk/omnigraph`**: regenerated GraphQL introspection so the new `Query.labels` field is available to the typed `graphql(...)` client. +Add **`EnsRainbowBeam`** (`apps/ensrainbowbeam`) exposing **`POST /api/discover`**, classifies each submitted label literal against ENSNode via **`labels(by: { labelHashes })`** (with client-side chunking aligned to ENSApi batch limits), emits structured JSON Lines to stdout for future sinks, mirrors other apps’ Dockerfile + Compose service patterns (`docker/services/ensrainbowbeam.yml`), and includes MIT **`LICENSE`** in the app directory ([issue \#2003](https://github.com/namehash/ensnode/issues/2003)). diff --git a/.changeset/enssdk-omnigraph-codegen.md b/.changeset/enssdk-omnigraph-codegen.md new file mode 100644 index 0000000000..24107964d1 --- /dev/null +++ b/.changeset/enssdk-omnigraph-codegen.md @@ -0,0 +1,5 @@ +--- +"enssdk": minor +--- + +Regenerate `enssdk/omnigraph` artifacts for the Omnigraph **`LabelHash`** scalar, mapped in `OmnigraphScalars` for typed **`graphql`** documents. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7822968ab5..6a4de21ece 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ # # Published artifacts: # - NPM packages: @ensnode/* packages published to npm registry with @latest tag -# - Docker images: ensindexer, ensadmin, ensapi, ensrainbow published to ghcr.io with @latest tag +# - Docker images: ensindexer, ensadmin, ensapi, ensrainbow, ensrainbowbeam published to ghcr.io with @latest tag # - GitHub Release: Created with version tag (e.g., v1.2.3) and autogenerated release notes # # Version management: @@ -88,6 +88,7 @@ jobs: or .name == "ensadmin" or .name == "ensapi" or .name == "ensrainbow" + or .name == "ensrainbowbeam" )) - name: Filter Published Packages For NPM Packages diff --git a/.github/workflows/release_preview.yml b/.github/workflows/release_preview.yml index eca8ceb0fe..2f68b5a174 100644 --- a/.github/workflows/release_preview.yml +++ b/.github/workflows/release_preview.yml @@ -230,7 +230,7 @@ jobs: strategy: fail-fast: false matrix: - app: [ensindexer, ensadmin, ensapi, ensrainbow] + app: [ensindexer, ensadmin, ensapi, ensrainbow, ensrainbowbeam] steps: - name: Checkout repository uses: actions/checkout@v6 @@ -342,6 +342,7 @@ jobs: docker pull ghcr.io/namehash/ensnode/ensadmin:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} docker pull ghcr.io/namehash/ensnode/ensapi:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} docker pull ghcr.io/namehash/ensnode/ensrainbow:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} + docker pull ghcr.io/namehash/ensnode/ensrainbowbeam:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} \`\`\` " diff --git a/.github/workflows/release_snapshot.yml b/.github/workflows/release_snapshot.yml index 44040a97eb..fe987176c1 100644 --- a/.github/workflows/release_snapshot.yml +++ b/.github/workflows/release_snapshot.yml @@ -12,7 +12,7 @@ # # Published artifacts: # - NPM packages: All @ensnode/* packages published with @next tag -# - Docker images: ensindexer, ensadmin, ensapi, ensrainbow published with @next tag +# - Docker images: ensindexer, ensadmin, ensapi, ensrainbow, ensrainbowbeam published with @next tag # - No GitHub releases or tags are created # # Version behavior: @@ -96,7 +96,7 @@ jobs: strategy: fail-fast: false matrix: - app: [ensindexer, ensadmin, ensrainbow, ensapi] + app: [ensindexer, ensadmin, ensrainbow, ensapi, ensrainbowbeam] steps: - name: Checkout repository uses: actions/checkout@v6 diff --git a/apps/ensapi/src/omnigraph-api/builder.ts b/apps/ensapi/src/omnigraph-api/builder.ts index eafbae8696..7cec8b79ea 100644 --- a/apps/ensapi/src/omnigraph-api/builder.ts +++ b/apps/ensapi/src/omnigraph-api/builder.ts @@ -11,6 +11,7 @@ import type { Hex, InterpretedLabel, InterpretedName, + LabelHash, Node, NormalizedAddress, PermissionsId, @@ -60,6 +61,7 @@ export type BuilderScalars = { BigInt: { Input: bigint; Output: bigint }; Address: { Input: NormalizedAddress; Output: NormalizedAddress }; Hex: { Input: Hex; Output: Hex }; + LabelHash: { Input: LabelHash; Output: LabelHash }; ChainId: { Input: ChainId; Output: ChainId }; CoinType: { Input: CoinType; Output: CoinType }; Node: { Input: Node; Output: Node }; diff --git a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts index 75e1376e09..483fc1c7df 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts @@ -1,23 +1,24 @@ import { asInterpretedLabel, - type Hex, + encodeLabelHash, type InterpretedLabel, type LabelHash, labelhashInterpretedLabel, + parseLabelHash, } from "enssdk"; import { describe, expect, it } from "vitest"; -import { LABELS_BY_HASHES_MAX } from "@/omnigraph-api/schema/label"; +import { LABELS_BY_LABELHASH_MAX } from "@/omnigraph-api/schema/label"; import { request } from "@/test/integration/graphql-utils"; import { gql } from "@/test/integration/omnigraph-api-client"; -type LabelsByHashesResult = { +type LabelsByLabelHashResult = { labels: Array<{ hash: LabelHash; interpreted: InterpretedLabel }>; }; -const LabelsByHashes = gql` - query LabelsByHashes($hashes: [Hex!]!) { - labels(by: { hashes: $hashes }) { +const LabelsByLabelHash = gql` + query LabelsByLabelHash($labelHashes: [LabelHash!]!) { + labels(by: { labelHashes: $labelHashes }) { hash interpreted } @@ -27,18 +28,26 @@ const LabelsByHashes = gql` // 'eth' is always seeded in the devnet fixture as a healed label const ETH_LABEL_HASH: LabelHash = labelhashInterpretedLabel(asInterpretedLabel("eth")); -// a labelhash that should not exist in the index (random 0xff... bytes) -const ABSENT_LABEL_HASH = `0x${"ff".repeat(32)}` as LabelHash; - -// `encodeLabelHash` produces `[<64 hex chars>]`; an `interpreted` value matching this regex is the -// "unhealed" representation. Used below to assert that a returned label is healed without -// reconstructing the encoded form per-row. -const ENCODED_LABEL_HASH_REGEX = /^\[[0-9a-fA-F]{64}\]$/; +// a LabelHash that should not exist in the index (deterministic dummy bytes) +const ABSENT_LABEL_HASH = parseLabelHash(`0x${"ff".repeat(32)}`); describe("Query.labels", () => { it("returns a healed label entry for a known LabelHash", async () => { await expect( - request(LabelsByHashes, { hashes: [ETH_LABEL_HASH] }), + request(LabelsByLabelHash, { labelHashes: [ETH_LABEL_HASH] }), + ).resolves.toMatchObject({ + labels: [{ hash: ETH_LABEL_HASH, interpreted: "eth" }], + }); + }); + + it("accepts non-normalized (mixed-case hex) LabelHash variables and resolves matches", async () => { + const uppercaseVariable = ETH_LABEL_HASH.toUpperCase(); + expect(parseLabelHash(uppercaseVariable)).toBe(ETH_LABEL_HASH); + + await expect( + request(LabelsByLabelHash, { + labelHashes: [uppercaseVariable as LabelHash], + }), ).resolves.toMatchObject({ labels: [{ hash: ETH_LABEL_HASH, interpreted: "eth" }], }); @@ -46,24 +55,24 @@ describe("Query.labels", () => { it("omits LabelHashes that are not present in the index", async () => { await expect( - request(LabelsByHashes, { hashes: [ABSENT_LABEL_HASH] }), + request(LabelsByLabelHash, { labelHashes: [ABSENT_LABEL_HASH] }), ).resolves.toEqual({ labels: [] }); }); - it("returns only the present labels when input mixes present and absent hashes", async () => { + it("returns only the present labels when input mixes present and absent LabelHashes", async () => { await expect( - request(LabelsByHashes, { - hashes: [ETH_LABEL_HASH, ABSENT_LABEL_HASH], + request(LabelsByLabelHash, { + labelHashes: [ETH_LABEL_HASH, ABSENT_LABEL_HASH], }), ).resolves.toMatchObject({ labels: [{ hash: ETH_LABEL_HASH }], }); }); - it("dedupes repeated input hashes", async () => { + it("dedupes repeated input LabelHashes", async () => { await expect( - request(LabelsByHashes, { - hashes: [ETH_LABEL_HASH, ETH_LABEL_HASH, ETH_LABEL_HASH], + request(LabelsByLabelHash, { + labelHashes: [ETH_LABEL_HASH, ETH_LABEL_HASH, ETH_LABEL_HASH], }), ).resolves.toMatchObject({ labels: [{ hash: ETH_LABEL_HASH }], @@ -71,32 +80,42 @@ describe("Query.labels", () => { }); it("returns an empty list when input is empty", async () => { - await expect( - request(LabelsByHashes, { hashes: [] as Hex[] }), - ).resolves.toEqual({ labels: [] }); + await expect(request(LabelsByLabelHash, { labelHashes: [] })).resolves.toEqual({ labels: [] }); }); it("classifies returned labels: 'eth' is healed (interpreted !== encodeLabelHash(hash))", async () => { - await expect( - request(LabelsByHashes, { hashes: [ETH_LABEL_HASH] }), - ).resolves.toMatchObject({ - labels: [ - { - hash: ETH_LABEL_HASH, - interpreted: expect.not.stringMatching(ENCODED_LABEL_HASH_REGEX), - }, - ], + const { labels } = await request(LabelsByLabelHash, { + labelHashes: [ETH_LABEL_HASH], }); + + expect(labels).toHaveLength(1); + expect(labels[0].interpreted).not.toEqual(encodeLabelHash(ETH_LABEL_HASH)); + }); + + it("rejects junk strings that cannot be parsed as LabelHashes", async () => { + await expect( + request(LabelsByLabelHash, { + labelHashes: ["not-even-hex"], + }), + ).rejects.toThrow(/Invalid labelHash/i); + }); + + it("rejects hex values that are not exactly 32 bytes", async () => { + await expect( + request(LabelsByLabelHash, { + labelHashes: ["0x00"], + }), + ).rejects.toThrow(/Invalid labelHash/i); }); - it("rejects requests over the maximum allowed hash count", async () => { - // generate (LABELS_BY_HASHES_MAX + 1) distinct labelhashes deterministically - const hashes: LabelHash[] = []; - for (let i = 0; i <= LABELS_BY_HASHES_MAX; i++) { - const hex = i.toString(16).padStart(64, "0"); - hashes.push(`0x${hex}` as LabelHash); + it("rejects requests over the maximum allowed LabelHash count", async () => { + const labelHashes: LabelHash[] = []; + for (let i = 0; i <= LABELS_BY_LABELHASH_MAX; i++) { + labelHashes.push(parseLabelHash(`0x${i.toString(16).padStart(64, "0")}`)); } - await expect(request(LabelsByHashes, { hashes })).rejects.toThrow(/Too many hashes/); + await expect(request(LabelsByLabelHash, { labelHashes })).rejects.toThrow( + /Too many LabelHashes/i, + ); }); }); diff --git a/apps/ensapi/src/omnigraph-api/schema/label.ts b/apps/ensapi/src/omnigraph-api/schema/label.ts index 2bb194fb67..a54bd73d62 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.ts @@ -11,7 +11,7 @@ LabelRef.implement({ hash: t.field({ description: "The Label's LabelHash\n(@see https://ensnode.io/docs/reference/terminology#labels-labelhashes-labelhash-function)", - type: "Hex", + type: "LabelHash", nullable: false, resolve: (parent) => parent.labelHash, }), @@ -39,15 +39,15 @@ LabelRef.implement({ * Caps the resolver's `inArray` query so a single GraphQL request cannot enumerate * the entire `label` table. */ -export const LABELS_BY_HASHES_MAX = 200; +export const LABELS_BY_LABELHASH_MAX = 100; -export const LabelsByHashesInput = builder.inputType("LabelsByHashesInput", { +export const LabelsByLabelHashesInput = builder.inputType("LabelsByLabelHashesInput", { description: "Look up Labels by a batch of LabelHashes.", fields: (t) => ({ - hashes: t.field({ - type: ["Hex"], + labelHashes: t.field({ + type: ["LabelHash"], required: true, - description: `LabelHashes to look up. Up to ${LABELS_BY_HASHES_MAX} hashes per request. Absent labels are simply omitted from the result.`, + description: `LabelHashes to look up. Up to ${LABELS_BY_LABELHASH_MAX} LabelHashes per request (each normalized to lowercase at parse time). LabelHashes absent from the index are omitted from the result.`, }), }), }); diff --git a/apps/ensapi/src/omnigraph-api/schema/query.ts b/apps/ensapi/src/omnigraph-api/schema/query.ts index 17e5881374..7525907491 100644 --- a/apps/ensapi/src/omnigraph-api/schema/query.ts +++ b/apps/ensapi/src/omnigraph-api/schema/query.ts @@ -27,7 +27,11 @@ import { DomainsOrderInput, DomainsWhereInput, } from "@/omnigraph-api/schema/domain"; -import { LABELS_BY_HASHES_MAX, LabelRef, LabelsByHashesInput } from "@/omnigraph-api/schema/label"; +import { + LABELS_BY_LABELHASH_MAX, + LabelRef, + LabelsByLabelHashesInput, +} from "@/omnigraph-api/schema/label"; import { PermissionsIdInput, PermissionsRef } from "@/omnigraph-api/schema/permissions"; import { RegistrationInterfaceRef } from "@/omnigraph-api/schema/registration"; import { RegistryIdInput, RegistryInterfaceRef } from "@/omnigraph-api/schema/registry"; @@ -155,21 +159,21 @@ builder.queryType({ "omitted from the result.", type: [LabelRef], nullable: false, - args: { by: t.arg({ type: LabelsByHashesInput, required: true }) }, + args: { by: t.arg({ type: LabelsByLabelHashesInput, required: true }) }, resolve: async (_parent, { by }) => { - if (by.hashes.length === 0) return []; + if (by.labelHashes.length === 0) return []; - if (by.hashes.length > LABELS_BY_HASHES_MAX) { + if (by.labelHashes.length > LABELS_BY_LABELHASH_MAX) { // Use `createGraphQLError` so the client-facing validation message survives Yoga's // default `maskError`, which (correctly) hides plain `Error` instances as // "Unexpected error.". throw createGraphQLError( - `Too many hashes: received ${by.hashes.length}, max ${LABELS_BY_HASHES_MAX}.`, + `Too many LabelHashes: received ${by.labelHashes.length}, max ${LABELS_BY_LABELHASH_MAX}.`, { extensions: { code: "BAD_USER_INPUT" } }, ); } - const dedupedHashes = Array.from(new Set(by.hashes)); + const dedupedHashes = Array.from(new Set(by.labelHashes)); return ensDb .select() diff --git a/apps/ensapi/src/omnigraph-api/schema/scalars.ts b/apps/ensapi/src/omnigraph-api/schema/scalars.ts index 9ccbbf235a..26c3f26d97 100644 --- a/apps/ensapi/src/omnigraph-api/schema/scalars.ts +++ b/apps/ensapi/src/omnigraph-api/schema/scalars.ts @@ -7,12 +7,14 @@ import { type Hex, isInterpretedLabel, isInterpretedName, + type LabelHash, type Name, type Node, type NormalizedAddress, type PermissionsId, type PermissionsResourceId, type PermissionsUserId, + parseLabelHash, type RegistrationId, type RegistryId, type RenewalId, @@ -61,6 +63,13 @@ builder.scalarType("Hex", { .parse(value), }); +builder.scalarType("LabelHash", { + description: + "LabelHash represents enssdk#LabelHash: a 32-byte (64 hex digit) value, `0x`-prefixed and lowercased.", + serialize: (value: LabelHash) => value, + parseValue: (value) => parseLabelHash(z.coerce.string().parse(value)), +}); + builder.scalarType("ChainId", { description: "ChainId represents a enssdk#ChainId.", serialize: (value: ChainId) => value, diff --git a/apps/ensapi/src/omnigraph-api/yoga.ts b/apps/ensapi/src/omnigraph-api/yoga.ts index c85dc0663a..ac79cbb1f4 100644 --- a/apps/ensapi/src/omnigraph-api/yoga.ts +++ b/apps/ensapi/src/omnigraph-api/yoga.ts @@ -16,6 +16,16 @@ export const yoga = createYoga({ context, // CORS is handled by the Hono middleware in app.ts cors: false, + // Error masking: + // - Production: use Yoga defaults so internal details are not exposed to clients. + // - Non-production: still apply the same masked client payload, but log the **original** + // error server-side first. This makes debugging much easier than only seeing the masked + // message, while keeping the client-facing behavior aligned with production. + // + // Motivation: some resolvers intentionally throw `GraphQLError` (e.g. validation for + // `Query.labels`), but other code paths may throw plain `Error`. Yoga's default `maskError` + // maps unknown errors to a generic "Unexpected error." on the client; logging here ensures + // the real stack/message is still visible in local/staging logs. maskedErrors: process.env.NODE_ENV === "production" ? true diff --git a/apps/ensrainbowbeam/Dockerfile b/apps/ensrainbowbeam/Dockerfile index a2ea62c394..b6f0394d04 100644 --- a/apps/ensrainbowbeam/Dockerfile +++ b/apps/ensrainbowbeam/Dockerfile @@ -1,6 +1,7 @@ FROM node:24-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* RUN corepack enable WORKDIR /app diff --git a/apps/ensrainbowbeam/LICENSE b/apps/ensrainbowbeam/LICENSE new file mode 100644 index 0000000000..24d66814d7 --- /dev/null +++ b/apps/ensrainbowbeam/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 NameHash + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/ensrainbowbeam/README.md b/apps/ensrainbowbeam/README.md index 5a7fba5d54..86c5663300 100644 --- a/apps/ensrainbowbeam/README.md +++ b/apps/ensrainbowbeam/README.md @@ -13,19 +13,20 @@ future row shape so adding a sink later is mechanical. ## Endpoints - `GET /health` — liveness probe; always returns `{ message: "ok" }`. -- `POST /api/submissions` — accepts `{ labels: string[], callerAddress: Address }` and - responds with per-label classification (`unknown_in_index` / `healed_in_index` / - `absent_from_index`). +- `POST /api/discover` — accepts `{ labels: string[], callerAddress: Address }` and responds + with per-label classification (`unknown_in_index` / `healed_in_index` / `absent_from_index`). ## How label classification works For each submitted raw label EnsRainbowBeam: -1. Computes `labelhashLiteralLabel(rawLabel)`. +1. Computes `labelhashLiteralLabel(asLiteralLabel(rawLabel))`. 2. If the label is normalizable AND the normalized form differs from the raw label, also - computes `labelhashLiteralLabel(normalizedLabel)`. -3. Sends every distinct labelhash to ENSNode via the typed `enssdk/omnigraph` client using - the `labels(by: { hashes })` query. + computes `labelhashLiteralLabel` for that normalized literal (still a literal-string path — + input is never treated as an Encoded LabelHash before hashing). +3. Sends every distinct LabelHash to ENSNode via the typed `enssdk/omnigraph` client using + the `labels(by: { labelHashes })` query (batched when a submission exceeds the Omnigraph + per-request cap). 4. Classifies each submitted label: - `unknown_in_index` — at least one of its hashes is present in the index but not yet healed (i.e. `interpreted` is the encoded labelhash form). These are the interesting diff --git a/apps/ensrainbowbeam/src/app.ts b/apps/ensrainbowbeam/src/app.ts index 3f7e8e1761..4675a263d2 100644 --- a/apps/ensrainbowbeam/src/app.ts +++ b/apps/ensrainbowbeam/src/app.ts @@ -8,7 +8,7 @@ const app = new Hono(); app.get("/health", healthHandler); -app.post("/api/submissions", submissionsHandler); +app.post("/api/discover", submissionsHandler); app.notFound((c) => errorResponse(c, { message: "Not Found", status: 404 })); diff --git a/apps/ensrainbowbeam/src/handlers/submissions.test.ts b/apps/ensrainbowbeam/src/handlers/submissions.test.ts index 7476374d2f..15eb987cbe 100644 --- a/apps/ensrainbowbeam/src/handlers/submissions.test.ts +++ b/apps/ensrainbowbeam/src/handlers/submissions.test.ts @@ -26,14 +26,14 @@ const mockedLookup = vi.mocked(lookupLabels); function makeApp() { const app = new Hono(); - app.post("/api/submissions", submissionsHandler); + app.post("/api/discover", submissionsHandler); app.onError((error, c) => errorResponse(c, { error })); return app; } const CALLER = "0x1234567890123456789012345678901234567890"; -describe("POST /api/submissions", () => { +describe("POST /api/discover", () => { const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -50,7 +50,7 @@ describe("POST /api/submissions", () => { it("400s on malformed JSON", async () => { const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: "not-json", @@ -62,7 +62,7 @@ describe("POST /api/submissions", () => { it("400s when labels is empty", async () => { const app = makeApp(); mockedLookup.mockResolvedValue([]); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: [], callerAddress: CALLER }), @@ -73,7 +73,7 @@ describe("POST /api/submissions", () => { it("400s when callerAddress is not a valid EVM address", async () => { const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["foo"], callerAddress: "not-an-address" }), @@ -89,7 +89,7 @@ describe("POST /api/submissions", () => { ]); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["eth"], callerAddress: CALLER }), @@ -124,7 +124,7 @@ describe("POST /api/submissions", () => { ]); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["foo"], callerAddress: CALLER }), @@ -139,7 +139,7 @@ describe("POST /api/submissions", () => { mockedLookup.mockResolvedValue([]); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -158,7 +158,7 @@ describe("POST /api/submissions", () => { const mixedCase = "0xAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAa"; const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["foo"], callerAddress: mixedCase }), @@ -177,7 +177,7 @@ describe("POST /api/submissions", () => { mockedLookup.mockResolvedValue([]); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["VITALIK"], callerAddress: CALLER }), @@ -196,7 +196,7 @@ describe("POST /api/submissions", () => { const labels = Array.from({ length: 101 }, (_, i) => `label-${i}`); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels, callerAddress: CALLER }), @@ -209,7 +209,7 @@ describe("POST /api/submissions", () => { mockedLookup.mockRejectedValue(new DOMException("The operation timed out.", "TimeoutError")); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["foo"], callerAddress: CALLER }), @@ -228,7 +228,7 @@ describe("POST /api/submissions", () => { mockedLookup.mockRejectedValue(new Error("upstream exploded")); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["foo"], callerAddress: CALLER }), @@ -247,7 +247,7 @@ describe("POST /api/submissions", () => { mockedLookup.mockResolvedValue([]); const app = makeApp(); - const res = await app.request("/api/submissions", { + const res = await app.request("/api/discover", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels: ["foo", "foo", "foo"], callerAddress: CALLER }), diff --git a/apps/ensrainbowbeam/src/handlers/submissions.ts b/apps/ensrainbowbeam/src/handlers/submissions.ts index d1421ee118..fcca9179e9 100644 --- a/apps/ensrainbowbeam/src/handlers/submissions.ts +++ b/apps/ensrainbowbeam/src/handlers/submissions.ts @@ -14,19 +14,18 @@ import { import { lookupLabels } from "@/lib/omnigraph-client"; /** - * Maximum number of raw labels accepted per `POST /api/submissions` request. + * Maximum number of raw labels accepted per `POST /api/discover` request. * * This is independent of how many labelhashes each label expands into (1 if already - * normalized / unnormalizable, 2 if it has a distinct normalized form). The resolver - * cap (`LABELS_BY_HASHES_MAX = 200` in `apps/ensapi/src/omnigraph-api/schema/label.ts`) - * is sized to exactly accommodate the worst case (2 * `MAX_LABELS_PER_SUBMISSION`). - * Keep these limits in sync so callers always get the same per-submission limit - * regardless of normalization. + * normalized / unnormalizable, 2 if it has a distinct normalized form). `lookupLabels` + * batches Omnigraph requests against `LABELS_BY_LABELHASH_MAX` (see + * `apps/ensapi/src/omnigraph-api/schema/label.ts`); the worst-case distinct LabelHash count + * per submission is still capped at `2 * MAX_LABELS_PER_SUBMISSION`. */ export const MAX_LABELS_PER_SUBMISSION = 100; /** - * Hard upper bound on how long a single `POST /api/submissions` will wait on the + * Hard upper bound on how long a single `POST /api/discover` will wait on the * Omnigraph labels lookup before failing the request. Prevents a stalled upstream * from holding handler resources indefinitely. */ diff --git a/apps/ensrainbowbeam/src/lib/labels.ts b/apps/ensrainbowbeam/src/lib/labels.ts index 3f5362b257..bfeff60c12 100644 --- a/apps/ensrainbowbeam/src/lib/labels.ts +++ b/apps/ensrainbowbeam/src/lib/labels.ts @@ -1,4 +1,5 @@ import { + asLiteralLabel, encodeLabelHash, type InterpretedLabel, type LabelHash, @@ -23,12 +24,14 @@ export type LabelStatus = "unknown_in_index" | "healed_in_index" | "absent_from_ * The hashing result for a single submitted raw label. * * `normalizedLabel` (and its hash) are populated only when the raw label is normalizable - * AND the normalized form differs from the raw label. + * AND the normalized form differs from the raw label. Both the raw submission and any + * normalization branch use {@link LiteralLabel} so callers can submit unnormalized literals + * deliberately (the service never coerces input to an {@link InterpretedLabel} up front). */ export type HashedLabel = { rawLabel: string; labelHash: LabelHash; - normalizedLabel?: InterpretedLabel; + normalizedLabel?: LiteralLabel; normalizedLabelHash?: LabelHash; }; @@ -50,22 +53,22 @@ export type LabelHit = { /** * Computes the hash representations of a single raw label. * - * Always computes `labelHash = labelhashLiteralLabel(rawLabel)`. If the raw label is - * normalizable AND its normalized form differs from the raw value, also computes the - * normalized form's hash. Normalization failures are tolerated and treated as "no - * normalized variant" — many submissions will be unnormalizable by design (this app - * is meant to ingest such labels). + * Always computes `labelHash = labelhashLiteralLabel(asLiteralLabel(rawLabel))`. If the label + * normalizes under ENSIP-15 to a **different string** than the raw submission, also computes + * hashes for that normalized {@link LiteralLabel}. Normalization failures are tolerated and + * treated as "no normalized variant". */ export function hashLabel(rawLabel: string): HashedLabel { - const labelHash = labelhashLiteralLabel(rawLabel as LiteralLabel); + const literal = asLiteralLabel(rawLabel); + const labelHash = labelhashLiteralLabel(literal); - let normalizedLabel: InterpretedLabel | undefined; + let normalizedLabel: LiteralLabel | undefined; let normalizedLabelHash: LabelHash | undefined; try { - const candidate = normalizeLabel(rawLabel); - if (candidate !== rawLabel) { - normalizedLabel = candidate; - normalizedLabelHash = labelhashLiteralLabel(candidate as unknown as LiteralLabel); + const normalizedInterpreted = normalizeLabel(literal); + if (normalizedInterpreted !== rawLabel) { + normalizedLabel = asLiteralLabel(normalizedInterpreted); + normalizedLabelHash = labelhashLiteralLabel(normalizedLabel); } } catch { // unnormalizable raw label is expected; leave normalized variant undefined @@ -81,7 +84,7 @@ export function hashLabel(rawLabel: string): HashedLabel { /** * Returns the deduped flat list of labelhashes we want to look up via the Omnigraph - * `labels(by: { hashes })` query. + * `labels(by: { labelHashes })` query. */ export function collectLookupHashes(hashed: readonly HashedLabel[]): LabelHash[] { const set = new Set(); diff --git a/apps/ensrainbowbeam/src/lib/omnigraph-client.ts b/apps/ensrainbowbeam/src/lib/omnigraph-client.ts index aaa31d4a64..a567604439 100644 --- a/apps/ensrainbowbeam/src/lib/omnigraph-client.ts +++ b/apps/ensrainbowbeam/src/lib/omnigraph-client.ts @@ -7,14 +7,20 @@ import { graphql, omnigraph } from "enssdk/omnigraph"; import type { LabelHit } from "@/lib/labels"; /** - * Typed document for the `labels(by: { hashes })` Omnigraph query. + * Must equal `LABELS_BY_LABELHASH_MAX` in `apps/ensapi/src/omnigraph-api/schema/label.ts`. + * EnsRainbowBeam chunks Omnigraph requests so a single submission can exceed this cap. + */ +const OMNIGRAPH_LABEL_LOOKUP_BATCH_SIZE = 100; + +/** + * Typed document for the `labels(by: { labelHashes })` Omnigraph query. * * Variable + result types are derived from the generated introspection in `enssdk/omnigraph`, * so changes to the schema break this call site at typecheck time. */ -export const LabelsByHashes = graphql(` - query LabelsByHashes($hashes: [Hex!]!) { - labels(by: { hashes: $hashes }) { +export const LabelsByLabelHash = graphql(` + query LabelsByLabelHash($labelHashes: [LabelHash!]!) { + labels(by: { labelHashes: $labelHashes }) { hash interpreted } @@ -26,33 +32,42 @@ const client = createEnsNodeClient({ url: config.ensNodeUrl }).extend(omnigraph) /** * Looks up Labels by a batch of LabelHashes against ENSNode's Omnigraph. * - * The Omnigraph resolver enforces a hard cap on `hashes.length` (see `LABELS_BY_HASHES_MAX` - * in `apps/ensapi/src/omnigraph-api/schema/label.ts`). The submissions handler caps raw - * labels per request via `MAX_LABELS_PER_SUBMISSION`, sized so that the worst-case expansion - * (each label producing both a raw and a normalized hash) stays within the resolver cap. + * The Omnigraph resolver enforces a hard cap on how many LabelHashes a single query may carry + * (`LABELS_BY_LABELHASH_MAX` / `OMNIGRAPH_LABEL_LOOKUP_BATCH_SIZE`). When the caller provides + * more (e.g. a full submission expanding to up to 200 distinct LabelHashes), this function + * automatically issues multiple batched requests and merges results. * * Pass an optional `signal` to forward request cancellation (e.g. handler timeout, client - * disconnect) to the underlying HTTP request issued by the Omnigraph SDK. + * disconnect) to the underlying HTTP requests issued by the Omnigraph SDK. */ export async function lookupLabels( - hashes: readonly LabelHash[], + labelHashes: readonly LabelHash[], signal?: AbortSignal, ): Promise { - if (hashes.length === 0) return []; - - const result = await client.omnigraph.query({ - query: LabelsByHashes, - // The generated `LabelsByHashes` document types `hashes` as a mutable `Hex[]`, so we copy - // the readonly input into a fresh mutable array. No runtime cost beyond an `Array.from`. - variables: { hashes: [...hashes] }, - signal, - }); - - if (result.errors && result.errors.length > 0) { - throw new Error( - `Omnigraph labels query returned errors: ${result.errors.map((e) => e.message).join("; ")}`, - ); + if (labelHashes.length === 0) return []; + + const chunks: LabelHash[][] = []; + for (let i = 0; i < labelHashes.length; i += OMNIGRAPH_LABEL_LOOKUP_BATCH_SIZE) { + chunks.push(labelHashes.slice(i, i + OMNIGRAPH_LABEL_LOOKUP_BATCH_SIZE) as LabelHash[]); + } + + const results = await Promise.all( + chunks.map((batch) => + client.omnigraph.query({ + query: LabelsByLabelHash, + variables: { labelHashes: [...batch] }, + signal, + }), + ), + ); + + for (const result of results) { + if (result.errors && result.errors.length > 0) { + throw new Error( + `Omnigraph labels query returned errors: ${result.errors.map((e) => e.message).join("; ")}`, + ); + } } - return result.data?.labels ?? []; + return results.flatMap((r) => r.data?.labels ?? []); } diff --git a/docker/README.md b/docker/README.md index 03ad9754a7..dce7268f5a 100644 --- a/docker/README.md +++ b/docker/README.md @@ -15,6 +15,7 @@ All commands are run from the **monorepo root**. | `docker/envs/.env.docker.example` | Example for user-specific config. Copy to `.env.docker.local` for mainnet/sepolia. | | `docker/envs/.env.docker.local` | User config (gitignored). Required for base stack, optional for devnet overrides. | +EnsRainbowBeam (`docker/services/ensrainbowbeam.yml`) is optional: extend that file when you want label-discovery HTTP alongside ENSApi (`ENSNODE_URL` must reach the Omnigraph-capable ENSApi URL). > To inspect the fully resolved config for any compose file (resolves all `extends`): > > ``` @@ -78,6 +79,7 @@ pnpm docker:build:ensnode pnpm docker:build:ensindexer pnpm docker:build:ensapi pnpm docker:build:ensrainbow +pnpm docker:build:ensrainbowbeam pnpm docker:build:ensadmin ``` diff --git a/docker/services/ensrainbowbeam.yml b/docker/services/ensrainbowbeam.yml new file mode 100644 index 0000000000..df0b8ddd81 --- /dev/null +++ b/docker/services/ensrainbowbeam.yml @@ -0,0 +1,21 @@ +services: + ensrainbowbeam: + container_name: ensrainbowbeam + image: ghcr.io/namehash/ensnode/ensrainbowbeam:${ENSNODE_VERSION:-1.10.1} + build: + dockerfile: ./apps/ensrainbowbeam/Dockerfile + context: ../.. + ports: + - "4444:4444" + environment: + ENSNODE_URL: http://ensapi:4334 + depends_on: + ensapi: + condition: service_started + healthcheck: + test: ["CMD", "curl", "--fail", "-s", "http://localhost:4444/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + start_interval: 1s diff --git a/package.json b/package.json index a0ca2ef2a8..a06761eb0a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "docker:build:ensadmin": "docker build -f apps/ensadmin/Dockerfile -t ghcr.io/namehash/ensnode/ensadmin:latest .", "docker:build:ensrainbow": "docker build -f apps/ensrainbow/Dockerfile -t ghcr.io/namehash/ensnode/ensrainbow:latest .", "docker:build:ensapi": "docker build -f apps/ensapi/Dockerfile -t ghcr.io/namehash/ensnode/ensapi:latest .", + "docker:build:ensrainbowbeam": "docker build -f apps/ensrainbowbeam/Dockerfile -t ghcr.io/namehash/ensnode/ensrainbowbeam:latest .", "otel-desktop-viewer": "docker run -p 8000:8000 -p 4317:4317 -p 4318:4318 davetron5000/otel-desktop-viewer:alpine-3", "generate:openapi": "pnpm -r --if-present generate:openapi", "generate:gqlschema": "pnpm -F ensapi generate:gqlschema && pnpm -F enssdk generate:gqlschema" diff --git a/packages/enssdk/src/omnigraph/generated/introspection.ts b/packages/enssdk/src/omnigraph/generated/introspection.ts index f60306b4e9..8674009083 100644 --- a/packages/enssdk/src/omnigraph/generated/introspection.ts +++ b/packages/enssdk/src/omnigraph/generated/introspection.ts @@ -3251,7 +3251,7 @@ const introspection = { "kind": "NON_NULL", "ofType": { "kind": "SCALAR", - "name": "Hex" + "name": "LabelHash" } }, "args": [], @@ -3272,12 +3272,16 @@ const introspection = { ], "interfaces": [] }, + { + "kind": "SCALAR", + "name": "LabelHash" + }, { "kind": "INPUT_OBJECT", - "name": "LabelsByHashesInput", + "name": "LabelsByLabelHashesInput", "inputFields": [ { - "name": "hashes", + "name": "labelHashes", "type": { "kind": "NON_NULL", "ofType": { @@ -3286,7 +3290,7 @@ const introspection = { "kind": "NON_NULL", "ofType": { "kind": "SCALAR", - "name": "Hex" + "name": "LabelHash" } } } @@ -4396,7 +4400,7 @@ const introspection = { "kind": "NON_NULL", "ofType": { "kind": "INPUT_OBJECT", - "name": "LabelsByHashesInput" + "name": "LabelsByLabelHashesInput" } } } diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql index 9b4d9d8f8e..260e4d5d8f 100644 --- a/packages/enssdk/src/omnigraph/generated/schema.graphql +++ b/packages/enssdk/src/omnigraph/generated/schema.graphql @@ -716,7 +716,7 @@ type Label { The Label's LabelHash (@see https://ensnode.io/docs/reference/terminology#labels-labelhashes-labelhash-function) """ - hash: Hex! + hash: LabelHash! """ The Label represented as an Interpreted Label. This is either a normalized Literal Label or an Encoded LabelHash. @@ -725,12 +725,17 @@ type Label { interpreted: InterpretedLabel! } +""" +LabelHash represents enssdk#LabelHash: a 32-byte (64 hex digit) value, `0x`-prefixed and lowercased. +""" +scalar LabelHash + """Look up Labels by a batch of LabelHashes.""" -input LabelsByHashesInput { +input LabelsByLabelHashesInput { """ - LabelHashes to look up. Up to 200 hashes per request. Absent labels are simply omitted from the result. + LabelHashes to look up. Up to 100 LabelHashes per request (each normalized to lowercase at parse time). LabelHashes absent from the index are omitted from the result. """ - hashes: [Hex!]! + labelHashes: [LabelHash!]! } """Constructs a reference to a specific Node via one of `name` or `node`.""" @@ -938,7 +943,7 @@ type Query { """ Look up Labels in the index by a batch of LabelHashes. Each returned Label exposes its `hash` and `interpreted` representation, where `interpreted` is the Encoded LabelHash for unhealed/unknown labels and a normalized literal for healed labels. LabelHashes that are not present in the index are simply omitted from the result. """ - labels(by: LabelsByHashesInput!): [Label!]! + labels(by: LabelsByLabelHashesInput!): [Label!]! """Identify Permissions by ID or AccountId.""" permissions(by: PermissionsIdInput!): Permissions diff --git a/packages/enssdk/src/omnigraph/graphql.ts b/packages/enssdk/src/omnigraph/graphql.ts index f7b6e99178..e896bd1059 100644 --- a/packages/enssdk/src/omnigraph/graphql.ts +++ b/packages/enssdk/src/omnigraph/graphql.ts @@ -7,6 +7,7 @@ import type { Hex, InterpretedLabel, InterpretedName, + LabelHash, Node, NormalizedAddress, PermissionsId, @@ -29,7 +30,8 @@ export { introspection } from "./generated/introspection"; * Scalar type mappings for the Omnigraph schema, representing the type of the serialized response * from the Omnigraph API. * - * Keep in sync with the scalars in apps/ensapi/src/omnigraph-api/builder.ts. + * Keep in sync with the scalars registered in `apps/ensapi/src/omnigraph-api/schema/scalars.ts` + * and the `BuilderScalars` map in `apps/ensapi/src/omnigraph-api/builder.ts`. */ export type OmnigraphScalars = { ID: string; @@ -38,6 +40,7 @@ export type OmnigraphScalars = { BigInt: `${bigint}`; Address: NormalizedAddress; Hex: Hex; + LabelHash: LabelHash; ChainId: ChainId; CoinType: CoinType; InterpretedName: InterpretedName; diff --git a/scripts/sync-docker-services-tags.mjs b/scripts/sync-docker-services-tags.mjs index 22f13f7973..f423ca9eb4 100644 --- a/scripts/sync-docker-services-tags.mjs +++ b/scripts/sync-docker-services-tags.mjs @@ -16,6 +16,7 @@ const serviceFiles = [ "docker/services/ensapi.yml", "docker/services/ensindexer.yml", "docker/services/ensrainbow.yml", + "docker/services/ensrainbowbeam.yml", ]; const semverRegex = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; From d643370dab862b63fe03c6d7480e8a02499b6cce Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 12:47:51 +0200 Subject: [PATCH 15/27] Refactor label handling in submissions: update `SubmissionsRequestSchema` to transform labels into `LiteralLabel` and adjust `SubmissionResultItem` to use typed labels. Enhance tests to reflect these changes and ensure proper handling of raw and normalized labels. --- .../src/handlers/submissions.ts | 16 +++++--- apps/ensrainbowbeam/src/lib/labels.test.ts | 39 ++++++++++--------- apps/ensrainbowbeam/src/lib/labels.ts | 33 +++++++++------- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/apps/ensrainbowbeam/src/handlers/submissions.ts b/apps/ensrainbowbeam/src/handlers/submissions.ts index fcca9179e9..30cecbee00 100644 --- a/apps/ensrainbowbeam/src/handlers/submissions.ts +++ b/apps/ensrainbowbeam/src/handlers/submissions.ts @@ -1,4 +1,4 @@ -import type { Address } from "enssdk"; +import { type Address, asLiteralLabel, type LabelHash, type LiteralLabel } from "enssdk"; import type { Context } from "hono"; import { isAddress } from "viem"; import { z } from "zod/v4"; @@ -32,7 +32,11 @@ export const MAX_LABELS_PER_SUBMISSION = 100; export const OMNIGRAPH_LOOKUP_TIMEOUT_MS = 10_000; const SubmissionsRequestSchema = z.object({ - labels: z.array(z.string().min(1).max(1000)).min(1).max(MAX_LABELS_PER_SUBMISSION), + labels: z + .array(z.string().min(1).max(1000)) + .min(1) + .max(MAX_LABELS_PER_SUBMISSION) + .transform((items) => items.map(asLiteralLabel)), callerAddress: z .string() .refine((value) => isAddress(value, { strict: false }), { @@ -42,10 +46,10 @@ const SubmissionsRequestSchema = z.object({ }); export type SubmissionResultItem = { - rawLabel: string; - labelHash: string; - normalizedLabel?: string; - normalizedLabelHash?: string; + rawLabel: LiteralLabel; + labelHash: LabelHash; + normalizedLabel?: LiteralLabel; + normalizedLabelHash?: LabelHash; status: LabelClassification["status"]; }; diff --git a/apps/ensrainbowbeam/src/lib/labels.test.ts b/apps/ensrainbowbeam/src/lib/labels.test.ts index 5c2827bbf0..70c777c866 100644 --- a/apps/ensrainbowbeam/src/lib/labels.test.ts +++ b/apps/ensrainbowbeam/src/lib/labels.test.ts @@ -1,4 +1,5 @@ import { + asLiteralLabel, encodeLabelHash, type InterpretedLabel, type LabelHash, @@ -19,53 +20,53 @@ const literal = (s: string) => s as LiteralLabel; describe("hashLabel", () => { it("computes labelhash for a normalized lowercase label", () => { - const result = hashLabel("vitalik"); + const result = hashLabel(literal("vitalik")); expect(result).toEqual({ - rawLabel: "vitalik", + rawLabel: literal("vitalik"), labelHash: labelhashLiteralLabel(literal("vitalik")), }); }); it("does not populate normalizedLabel when raw equals normalized", () => { - const result = hashLabel("eth"); + const result = hashLabel(literal("eth")); expect(result.normalizedLabel).toBeUndefined(); expect(result.normalizedLabelHash).toBeUndefined(); }); it("populates normalizedLabel + hash when uppercase label normalizes to lowercase", () => { - const result = hashLabel("VITALIK"); - expect(result.rawLabel).toBe("VITALIK"); + const result = hashLabel(literal("VITALIK")); + expect(result.rawLabel).toBe(literal("VITALIK")); expect(result.labelHash).toBe(labelhashLiteralLabel(literal("VITALIK"))); - expect(result.normalizedLabel).toBe("vitalik"); + expect(result.normalizedLabel).toBe(literal("vitalik")); expect(result.normalizedLabelHash).toBe(labelhashLiteralLabel(literal("vitalik"))); expect(result.normalizedLabelHash).not.toBe(result.labelHash); }); it("tolerates unnormalizable labels (e.g. labels with periods)", () => { - const result = hashLabel("foo.bar"); - expect(result.rawLabel).toBe("foo.bar"); + const result = hashLabel(literal("foo.bar")); + expect(result.rawLabel).toBe(literal("foo.bar")); expect(result.labelHash).toBe(labelhashLiteralLabel(literal("foo.bar"))); expect(result.normalizedLabel).toBeUndefined(); expect(result.normalizedLabelHash).toBeUndefined(); }); it("tolerates the empty string (cannot normalize)", () => { - const result = hashLabel(""); - expect(result.rawLabel).toBe(""); + const result = hashLabel(asLiteralLabel("")); + expect(result.rawLabel).toBe(asLiteralLabel("")); expect(result.normalizedLabel).toBeUndefined(); }); it("hashes a unicode label", () => { const label = "vitalik\u00e9"; - const result = hashLabel(label); - expect(result.labelHash).toBe(labelhashLiteralLabel(label as LiteralLabel)); + const result = hashLabel(asLiteralLabel(label)); + expect(result.labelHash).toBe(labelhashLiteralLabel(asLiteralLabel(label))); }); }); describe("collectLookupHashes", () => { it("returns the deduped union of raw + normalized labelhashes", () => { - const a = hashLabel("VITALIK"); - const b = hashLabel("vitalik"); + const a = hashLabel(literal("VITALIK")); + const b = hashLabel(literal("vitalik")); const hashes = collectLookupHashes([a, b]); expect(hashes).toHaveLength(2); expect(new Set(hashes).size).toBe(hashes.length); @@ -74,7 +75,7 @@ describe("collectLookupHashes", () => { }); it("ignores undefined normalized hashes", () => { - const a = hashLabel("eth"); + const a = hashLabel(literal("eth")); const hashes = collectLookupHashes([a]); expect(hashes).toEqual([a.labelHash]); }); @@ -95,10 +96,10 @@ describe("isUnhealedHit", () => { }); describe("classifySubmissions", () => { - const vitalik = hashLabel("vitalik"); - const eth = hashLabel("eth"); - const upper = hashLabel("HELLO"); - const random = hashLabel("zzzdoesnotexistzzz"); + const vitalik = hashLabel(literal("vitalik")); + const eth = hashLabel(literal("eth")); + const upper = hashLabel(literal("HELLO")); + const random = hashLabel(literal("zzzdoesnotexistzzz")); function makeHealedHit(hash: LabelHash, label: string): LabelHit { return { hash, interpreted: label as InterpretedLabel }; diff --git a/apps/ensrainbowbeam/src/lib/labels.ts b/apps/ensrainbowbeam/src/lib/labels.ts index bfeff60c12..1412492f52 100644 --- a/apps/ensrainbowbeam/src/lib/labels.ts +++ b/apps/ensrainbowbeam/src/lib/labels.ts @@ -2,6 +2,7 @@ import { asLiteralLabel, encodeLabelHash, type InterpretedLabel, + type Label, type LabelHash, type LiteralLabel, labelhashLiteralLabel, @@ -21,15 +22,17 @@ import { export type LabelStatus = "unknown_in_index" | "healed_in_index" | "absent_from_index"; /** - * The hashing result for a single submitted raw label. + * The hashing result for a single submitted label. * - * `normalizedLabel` (and its hash) are populated only when the raw label is normalizable - * AND the normalized form differs from the raw label. Both the raw submission and any - * normalization branch use {@link LiteralLabel} so callers can submit unnormalized literals - * deliberately (the service never coerces input to an {@link InterpretedLabel} up front). + * `rawLabel` is always a {@link LiteralLabel}: submissions are never typed or coerced as + * {@link InterpretedLabel}, so unnormalized discovery remains representable. + * + * `normalizedLabel` (and its hash) are populated only when the label is normalizable under + * ENSIP-15 and the normalized form differs from the raw literal. That branch still hashes via + * {@link labelhashLiteralLabel} on a new {@link LiteralLabel} cast of the normalized string. */ export type HashedLabel = { - rawLabel: string; + rawLabel: LiteralLabel; labelHash: LabelHash; normalizedLabel?: LiteralLabel; normalizedLabelHash?: LabelHash; @@ -51,22 +54,22 @@ export type LabelHit = { }; /** - * Computes the hash representations of a single raw label. + * Computes the hash representations of a single submitted {@link LiteralLabel}. * - * Always computes `labelHash = labelhashLiteralLabel(asLiteralLabel(rawLabel))`. If the label - * normalizes under ENSIP-15 to a **different string** than the raw submission, also computes - * hashes for that normalized {@link LiteralLabel}. Normalization failures are tolerated and + * Always computes `labelHash = labelhashLiteralLabel(rawLabel)`. If the label normalizes under + * ENSIP-15 to a **different string** than the submission, also computes hashes for that + * normalized form as a distinct {@link LiteralLabel}. Normalization failures are tolerated and * treated as "no normalized variant". */ -export function hashLabel(rawLabel: string): HashedLabel { - const literal = asLiteralLabel(rawLabel); - const labelHash = labelhashLiteralLabel(literal); +export function hashLabel(rawLabel: LiteralLabel): HashedLabel { + const labelHash = labelhashLiteralLabel(rawLabel); let normalizedLabel: LiteralLabel | undefined; let normalizedLabelHash: LabelHash | undefined; try { - const normalizedInterpreted = normalizeLabel(literal); - if (normalizedInterpreted !== rawLabel) { + const normalizedInterpreted = normalizeLabel(rawLabel); + // Compare as unbranded labels: normalization yields InterpretedLabel; submission is LiteralLabel. + if ((normalizedInterpreted as Label) !== (rawLabel as Label)) { normalizedLabel = asLiteralLabel(normalizedInterpreted); normalizedLabelHash = labelhashLiteralLabel(normalizedLabel); } From 759716858469f8c21ae315fab6b0e12f4483bd5e Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 12:56:52 +0200 Subject: [PATCH 16/27] Enhance `Query.labels` functionality: Introduce stricter validation for distinct `LabelHash` inputs, update error messages for clarity, and allow duplicate `LabelHashes` within the maximum distinct count. Adjust related tests and documentation to reflect these changes. --- .changeset/ensapi-omnigraph-labels.md | 2 +- .../omnigraph-api/schema/label.integration.test.ts | 14 ++++++++++++-- apps/ensapi/src/omnigraph-api/schema/label.ts | 2 +- apps/ensapi/src/omnigraph-api/schema/query.ts | 8 ++++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.changeset/ensapi-omnigraph-labels.md b/.changeset/ensapi-omnigraph-labels.md index f493a4fc81..799c6cba6d 100644 --- a/.changeset/ensapi-omnigraph-labels.md +++ b/.changeset/ensapi-omnigraph-labels.md @@ -2,4 +2,4 @@ "ensapi": minor --- -Omnigraph **`Query.labels`** improvements: add a **`LabelHash`** GraphQL scalar (`0x` + 64 lowercase hex, parsed via `parseLabelHash`), rename the input to **`LabelsByLabelHashesInput`** with field **`labelHashes`**, enforce stricter parsing/validation through the scalar layer, normalize mixed-case hex at parse time, cap batch size to **`100`** LabelHashes per request for a round-number limit, and keep development error masking aligned with Yoga defaults while ensuring intentional `GraphQLError`s still surface useful client messages where applicable. +Omnigraph **`Query.labels`** improvements: add a **`LabelHash`** GraphQL scalar (`0x` + 64 lowercase hex, parsed via `parseLabelHash`), rename the input to **`LabelsByLabelHashesInput`** with field **`labelHashes`**, enforce stricter parsing/validation through the scalar layer, normalize mixed-case hex at parse time, cap batch size to **`100`** distinct LabelHashes per request (after deduplication) for a round-number limit aligned with the `inArray` workload, and keep development error masking aligned with Yoga defaults while ensuring intentional `GraphQLError`s still surface useful client messages where applicable. diff --git a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts index 483fc1c7df..e0ed68d177 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts @@ -108,14 +108,24 @@ describe("Query.labels", () => { ).rejects.toThrow(/Invalid labelHash/i); }); - it("rejects requests over the maximum allowed LabelHash count", async () => { + it("rejects requests over the maximum allowed distinct LabelHash count", async () => { const labelHashes: LabelHash[] = []; for (let i = 0; i <= LABELS_BY_LABELHASH_MAX; i++) { labelHashes.push(parseLabelHash(`0x${i.toString(16).padStart(64, "0")}`)); } await expect(request(LabelsByLabelHash, { labelHashes })).rejects.toThrow( - /Too many LabelHashes/i, + /Too many distinct LabelHashes/i, ); }); + + it("allows input with duplicate LabelHashes when the distinct count is within the max", async () => { + await expect( + request(LabelsByLabelHash, { + labelHashes: [ETH_LABEL_HASH, ETH_LABEL_HASH, ETH_LABEL_HASH], + }), + ).resolves.toMatchObject({ + labels: [{ hash: ETH_LABEL_HASH, interpreted: "eth" }], + }); + }); }); diff --git a/apps/ensapi/src/omnigraph-api/schema/label.ts b/apps/ensapi/src/omnigraph-api/schema/label.ts index a54bd73d62..4e77fb61f9 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.ts @@ -47,7 +47,7 @@ export const LabelsByLabelHashesInput = builder.inputType("LabelsByLabelHashesIn labelHashes: t.field({ type: ["LabelHash"], required: true, - description: `LabelHashes to look up. Up to ${LABELS_BY_LABELHASH_MAX} LabelHashes per request (each normalized to lowercase at parse time). LabelHashes absent from the index are omitted from the result.`, + description: `LabelHashes to look up. After deduplication, at most ${LABELS_BY_LABELHASH_MAX} distinct LabelHashes per request (each normalized to lowercase at parse time). LabelHashes absent from the index are omitted from the result.`, }), }), }); diff --git a/apps/ensapi/src/omnigraph-api/schema/query.ts b/apps/ensapi/src/omnigraph-api/schema/query.ts index 7525907491..01fdc852df 100644 --- a/apps/ensapi/src/omnigraph-api/schema/query.ts +++ b/apps/ensapi/src/omnigraph-api/schema/query.ts @@ -163,18 +163,18 @@ builder.queryType({ resolve: async (_parent, { by }) => { if (by.labelHashes.length === 0) return []; - if (by.labelHashes.length > LABELS_BY_LABELHASH_MAX) { + const dedupedHashes = Array.from(new Set(by.labelHashes)); + + if (dedupedHashes.length > LABELS_BY_LABELHASH_MAX) { // Use `createGraphQLError` so the client-facing validation message survives Yoga's // default `maskError`, which (correctly) hides plain `Error` instances as // "Unexpected error.". throw createGraphQLError( - `Too many LabelHashes: received ${by.labelHashes.length}, max ${LABELS_BY_LABELHASH_MAX}.`, + `Too many distinct LabelHashes: received ${dedupedHashes.length}, max ${LABELS_BY_LABELHASH_MAX}.`, { extensions: { code: "BAD_USER_INPUT" } }, ); } - const dedupedHashes = Array.from(new Set(by.labelHashes)); - return ensDb .select() .from(ensIndexerSchema.label) From 6e1f410a7313ef227ff27c3bf09aa67b3f4e8085 Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 12:58:51 +0200 Subject: [PATCH 17/27] Enhance `LabelHash` parsing: Update `parseValue` to include error handling for invalid inputs, ensuring clearer error messages. Modify tests to validate mixed-case hex digits and reject uppercase `0X` prefixes for `LabelHash` variables. --- .../schema/label.integration.test.ts | 19 +++++++++++++++---- .../src/omnigraph-api/schema/scalars.ts | 10 +++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts index e0ed68d177..a6942d67a7 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts @@ -40,19 +40,30 @@ describe("Query.labels", () => { }); }); - it("accepts non-normalized (mixed-case hex) LabelHash variables and resolves matches", async () => { - const uppercaseVariable = ETH_LABEL_HASH.toUpperCase(); - expect(parseLabelHash(uppercaseVariable)).toBe(ETH_LABEL_HASH); + it("accepts non-normalized (mixed-case hex digits) LabelHash variables and resolves matches", async () => { + // Lowercase `0x` prefix only; uppercase `0X` is rejected (see enssdk `parseLabelHash`). + const mixedCaseVariable = `0x${ETH_LABEL_HASH.slice(2) + .split("") + .map((c, i) => (i % 2 === 0 ? c.toUpperCase() : c)) + .join("")}` as LabelHash; + expect(parseLabelHash(mixedCaseVariable)).toBe(ETH_LABEL_HASH); await expect( request(LabelsByLabelHash, { - labelHashes: [uppercaseVariable as LabelHash], + labelHashes: [mixedCaseVariable], }), ).resolves.toMatchObject({ labels: [{ hash: ETH_LABEL_HASH, interpreted: "eth" }], }); }); + it("rejects uppercase 0X hex prefix", async () => { + const badPrefix = `0X${ETH_LABEL_HASH.slice(2)}`; + await expect( + request(LabelsByLabelHash, { labelHashes: [badPrefix] }), + ).rejects.toThrow(/Invalid labelHash/i); + }); + it("omits LabelHashes that are not present in the index", async () => { await expect( request(LabelsByLabelHash, { labelHashes: [ABSENT_LABEL_HASH] }), diff --git a/apps/ensapi/src/omnigraph-api/schema/scalars.ts b/apps/ensapi/src/omnigraph-api/schema/scalars.ts index 26c3f26d97..4fcedd192d 100644 --- a/apps/ensapi/src/omnigraph-api/schema/scalars.ts +++ b/apps/ensapi/src/omnigraph-api/schema/scalars.ts @@ -21,6 +21,7 @@ import { type ResolverId, type ResolverRecordsId, } from "enssdk"; +import { createGraphQLError } from "graphql-yoga"; import { isHex, size } from "viem"; import { z } from "zod/v4"; @@ -67,7 +68,14 @@ builder.scalarType("LabelHash", { description: "LabelHash represents enssdk#LabelHash: a 32-byte (64 hex digit) value, `0x`-prefixed and lowercased.", serialize: (value: LabelHash) => value, - parseValue: (value) => parseLabelHash(z.coerce.string().parse(value)), + parseValue: (value) => { + try { + return parseLabelHash(z.coerce.string().parse(value)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw createGraphQLError(message, { extensions: { code: "BAD_USER_INPUT" } }); + } + }, }); builder.scalarType("ChainId", { From 07c9d1fb770f317d77e36998c969af5ce87c9f4d Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 13:03:06 +0200 Subject: [PATCH 18/27] lint --- .../src/omnigraph-api/schema/label.integration.test.ts | 6 +++--- packages/enssdk/src/omnigraph/generated/schema.graphql | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts index a6942d67a7..3384eb984c 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.integration.test.ts @@ -59,9 +59,9 @@ describe("Query.labels", () => { it("rejects uppercase 0X hex prefix", async () => { const badPrefix = `0X${ETH_LABEL_HASH.slice(2)}`; - await expect( - request(LabelsByLabelHash, { labelHashes: [badPrefix] }), - ).rejects.toThrow(/Invalid labelHash/i); + await expect(request(LabelsByLabelHash, { labelHashes: [badPrefix] })).rejects.toThrow( + /Invalid labelHash/i, + ); }); it("omits LabelHashes that are not present in the index", async () => { diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql index 260e4d5d8f..e0d471e3d6 100644 --- a/packages/enssdk/src/omnigraph/generated/schema.graphql +++ b/packages/enssdk/src/omnigraph/generated/schema.graphql @@ -733,7 +733,7 @@ scalar LabelHash """Look up Labels by a batch of LabelHashes.""" input LabelsByLabelHashesInput { """ - LabelHashes to look up. Up to 100 LabelHashes per request (each normalized to lowercase at parse time). LabelHashes absent from the index are omitted from the result. + LabelHashes to look up. After deduplication, at most 100 distinct LabelHashes per request (each normalized to lowercase at parse time). LabelHashes absent from the index are omitted from the result. """ labelHashes: [LabelHash!]! } From 33e3ed16c9d3680e8e4d5d2f716616d5e00c34e3 Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 20:55:11 +0200 Subject: [PATCH 19/27] Add `EnsRainbowBeamClient` for HTTP interactions: Implement `health()` and `discover()` methods, introduce `EnsRainbowBeamHttpError` for error handling, and ensure client-side validation aligns with server expectations. Update exports and configuration for new client integration. --- .changeset/ensrainbow-sdk-beam-client.md | 5 + apps/ensapi/src/omnigraph-api/schema/label.ts | 4 +- apps/ensrainbowbeam/LICENSE | 2 +- .../src/handlers/submissions.ts | 6 +- .../src/lib/omnigraph-client.ts | 6 +- packages/ensrainbow-sdk/package.json | 7 +- .../src/ensrainbowbeam-client.ts | 179 ++++++++++++++++++ packages/ensrainbow-sdk/src/index.ts | 1 + packages/ensrainbow-sdk/tsup.config.ts | 1 + packages/enssdk/src/lib/index.ts | 1 + 10 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 .changeset/ensrainbow-sdk-beam-client.md create mode 100644 packages/ensrainbow-sdk/src/ensrainbowbeam-client.ts diff --git a/.changeset/ensrainbow-sdk-beam-client.md b/.changeset/ensrainbow-sdk-beam-client.md new file mode 100644 index 0000000000..adcabd436a --- /dev/null +++ b/.changeset/ensrainbow-sdk-beam-client.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensrainbow-sdk": minor +--- + +Add a light **EnsRainbowBeam** HTTP client (`EnsRainbowBeamClient`): `health()` and `discover()` against EnsRainbowBeam, client-side validation aligned with the server, `EnsRainbowBeamHttpError` for non-2xx responses with optional `{ message, details }` parsing, and subpath export `@ensnode/ensrainbow-sdk/ensrainbowbeam-client`. diff --git a/apps/ensapi/src/omnigraph-api/schema/label.ts b/apps/ensapi/src/omnigraph-api/schema/label.ts index 4e77fb61f9..d57cc8621a 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.ts @@ -1,3 +1,5 @@ +import { OMNIGRAPH_LABELS_BY_LABELHASH_MAX } from "enssdk"; + import type { ensIndexerSchema } from "@/lib/ensdb/singleton"; import { builder } from "@/omnigraph-api/builder"; @@ -39,7 +41,7 @@ LabelRef.implement({ * Caps the resolver's `inArray` query so a single GraphQL request cannot enumerate * the entire `label` table. */ -export const LABELS_BY_LABELHASH_MAX = 100; +export const LABELS_BY_LABELHASH_MAX = OMNIGRAPH_LABELS_BY_LABELHASH_MAX; export const LabelsByLabelHashesInput = builder.inputType("LabelsByLabelHashesInput", { description: "Look up Labels by a batch of LabelHashes.", diff --git a/apps/ensrainbowbeam/LICENSE b/apps/ensrainbowbeam/LICENSE index 24d66814d7..0d70998c14 100644 --- a/apps/ensrainbowbeam/LICENSE +++ b/apps/ensrainbowbeam/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 NameHash +Copyright (c) 2026 NameHash Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/apps/ensrainbowbeam/src/handlers/submissions.ts b/apps/ensrainbowbeam/src/handlers/submissions.ts index 30cecbee00..f2be9e3974 100644 --- a/apps/ensrainbowbeam/src/handlers/submissions.ts +++ b/apps/ensrainbowbeam/src/handlers/submissions.ts @@ -54,7 +54,6 @@ export type SubmissionResultItem = { }; export type SubmissionsResponse = { - submittedAt: string; callerAddress: Address; results: SubmissionResultItem[]; }; @@ -140,11 +139,11 @@ export async function submissionsHandler(c: Context) { const classifications = classifySubmissions(hashed, hits); const results = classifications.map(toResultItem); - const submittedAt = new Date().toISOString(); + const ts = new Date().toISOString(); const requestId = crypto.randomUUID(); const logLine: SubmissionLogLine = { - ts: submittedAt, + ts, requestId, callerAddress, items: results, @@ -152,7 +151,6 @@ export async function submissionsHandler(c: Context) { console.log(JSON.stringify(logLine)); const response: SubmissionsResponse = { - submittedAt, callerAddress, results, }; diff --git a/apps/ensrainbowbeam/src/lib/omnigraph-client.ts b/apps/ensrainbowbeam/src/lib/omnigraph-client.ts index a567604439..859981a814 100644 --- a/apps/ensrainbowbeam/src/lib/omnigraph-client.ts +++ b/apps/ensrainbowbeam/src/lib/omnigraph-client.ts @@ -1,6 +1,6 @@ import { config } from "@/config"; -import type { LabelHash } from "enssdk"; +import { type LabelHash, OMNIGRAPH_LABELS_BY_LABELHASH_MAX } from "enssdk"; import { createEnsNodeClient } from "enssdk/core"; import { graphql, omnigraph } from "enssdk/omnigraph"; @@ -10,7 +10,7 @@ import type { LabelHit } from "@/lib/labels"; * Must equal `LABELS_BY_LABELHASH_MAX` in `apps/ensapi/src/omnigraph-api/schema/label.ts`. * EnsRainbowBeam chunks Omnigraph requests so a single submission can exceed this cap. */ -const OMNIGRAPH_LABEL_LOOKUP_BATCH_SIZE = 100; +const OMNIGRAPH_LABEL_LOOKUP_BATCH_SIZE = OMNIGRAPH_LABELS_BY_LABELHASH_MAX; /** * Typed document for the `labels(by: { labelHashes })` Omnigraph query. @@ -33,7 +33,7 @@ const client = createEnsNodeClient({ url: config.ensNodeUrl }).extend(omnigraph) * Looks up Labels by a batch of LabelHashes against ENSNode's Omnigraph. * * The Omnigraph resolver enforces a hard cap on how many LabelHashes a single query may carry - * (`LABELS_BY_LABELHASH_MAX` / `OMNIGRAPH_LABEL_LOOKUP_BATCH_SIZE`). When the caller provides + * (`OMNIGRAPH_LABELS_BY_LABELHASH_MAX`). When the caller provides * more (e.g. a full submission expanding to up to 200 distinct LabelHashes), this function * automatically issues multiple batched requests and merges results. * diff --git a/packages/ensrainbow-sdk/package.json b/packages/ensrainbow-sdk/package.json index 371ff966ae..03f21256d0 100644 --- a/packages/ensrainbow-sdk/package.json +++ b/packages/ensrainbow-sdk/package.json @@ -21,7 +21,8 @@ "exports": { ".": "./src/index.ts", "./client": "./src/client.ts", - "./consts": "./src/consts.ts" + "./consts": "./src/consts.ts", + "./ensrainbowbeam-client": "./src/ensrainbowbeam-client.ts" }, "publishConfig": { "access": "public", @@ -37,6 +38,10 @@ "./consts": { "types": "./dist/consts.d.ts", "default": "./dist/consts.js" + }, + "./ensrainbowbeam-client": { + "types": "./dist/ensrainbowbeam-client.d.ts", + "default": "./dist/ensrainbowbeam-client.js" } }, "main": "./dist/index.js", diff --git a/packages/ensrainbow-sdk/src/ensrainbowbeam-client.ts b/packages/ensrainbow-sdk/src/ensrainbowbeam-client.ts new file mode 100644 index 0000000000..605f48e3b2 --- /dev/null +++ b/packages/ensrainbow-sdk/src/ensrainbowbeam-client.ts @@ -0,0 +1,179 @@ +import { + type LabelHash, + type LiteralLabel, + type NormalizedAddress, + toNormalizedAddress, +} from "enssdk"; + +/** + * Default EnsRainbowBeam base URL for local development (see Compose / app README). + */ +export const DEFAULT_ENSRAINBOWBEAM_URL = "http://localhost:4444" as const; + +/** + * Must stay in sync with `MAX_LABELS_PER_SUBMISSION` in + * `apps/ensrainbowbeam/src/handlers/submissions.ts`. + */ +export const ENSRAINBOWBEAM_DISCOVER_MAX_LABELS = 100; + +/** + * Must stay in sync with the per-label max length in + * `apps/ensrainbowbeam/src/handlers/submissions.ts` (`z.string().max(1000)`). + */ +export const ENSRAINBOWBEAM_LABEL_MAX_LENGTH = 1000; + +/** + * Per-label classification status from EnsRainbowBeam. + * + * @see apps/ensrainbowbeam/src/lib/labels.ts — `LabelStatus` + */ +export type DiscoverLabelStatus = "unknown_in_index" | "healed_in_index" | "absent_from_index"; + +export type DiscoverResultItem = { + rawLabel: LiteralLabel; + labelHash: LabelHash; + normalizedLabel?: LiteralLabel; + normalizedLabelHash?: LabelHash; + status: DiscoverLabelStatus; +}; + +export type DiscoverResponse = { + callerAddress: NormalizedAddress; + results: DiscoverResultItem[]; +}; + +export type EnsRainbowBeamHealthResponse = { + message: "ok"; +}; + +export class EnsRainbowBeamHttpError extends Error { + readonly name = "EnsRainbowBeamHttpError"; + + readonly status: number; + + readonly statusText: string; + + readonly details?: unknown; + + constructor(message: string, status: number, statusText = "", details?: unknown) { + super(message); + this.status = status; + this.statusText = statusText; + if (details !== undefined) { + this.details = details; + } + } +} + +export type EnsRainbowBeamClientOptions = { + /** + * EnsRainbowBeam HTTP origin. Defaults to {@link DEFAULT_ENSRAINBOWBEAM_URL}. + */ + baseUrl?: URL | string; + + /** + * `fetch` implementation (defaults to `globalThis.fetch`). + */ + fetch?: typeof fetch; +}; + +function toBaseUrl(url: URL | string | undefined): URL { + if (url === undefined) { + return new URL(DEFAULT_ENSRAINBOWBEAM_URL); + } + return typeof url === "string" ? new URL(url) : url; +} + +export function validateDiscoverParams(params: { labels: string[]; callerAddress: string }): { + labels: string[]; + callerAddress: NormalizedAddress; +} { + const { labels, callerAddress } = params; + + if (!Array.isArray(labels) || labels.length === 0) { + throw new Error("labels must be a non-empty array"); + } + if (labels.length > ENSRAINBOWBEAM_DISCOVER_MAX_LABELS) { + throw new Error( + `labels must contain at most ${ENSRAINBOWBEAM_DISCOVER_MAX_LABELS} items (received ${labels.length})`, + ); + } + + for (let i = 0; i < labels.length; i++) { + const label = labels[i]; + if (typeof label !== "string") { + throw new Error(`labels[${i}] must be a string`); + } + if (label.length === 0) { + throw new Error(`labels[${i}] must be non-empty`); + } + if (label.length > ENSRAINBOWBEAM_LABEL_MAX_LENGTH) { + throw new Error(`labels[${i}] exceeds maximum length ${ENSRAINBOWBEAM_LABEL_MAX_LENGTH}`); + } + } + + return { + labels, + callerAddress: toNormalizedAddress(callerAddress), + }; +} + +async function throwIfNotOk(response: Response): Promise { + if (response.ok) return; + + let message = `EnsRainbowBeam request failed (HTTP ${response.status})`; + let details: unknown; + + try { + const data: unknown = await response.json(); + if ( + data !== null && + typeof data === "object" && + "message" in data && + typeof (data as { message: unknown }).message === "string" + ) { + message = (data as { message: string }).message; + if ("details" in data) { + details = (data as { details: unknown }).details; + } + } + } catch { + // ignore non-JSON bodies + } + + throw new EnsRainbowBeamHttpError(message, response.status, response.statusText, details); +} + +export class EnsRainbowBeamClient { + private readonly baseUrl: URL; + + private readonly fetchImpl: typeof fetch; + + constructor(options: EnsRainbowBeamClientOptions = {}) { + this.baseUrl = toBaseUrl(options.baseUrl); + this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis); + } + + async health(signal?: AbortSignal): Promise { + const response = await this.fetchImpl(new URL("/health", this.baseUrl), { signal }); + await throwIfNotOk(response); + return response.json() as Promise; + } + + async discover( + params: { labels: string[]; callerAddress: string }, + signal?: AbortSignal, + ): Promise { + const { labels, callerAddress } = validateDiscoverParams(params); + + const response = await this.fetchImpl(new URL("/api/discover", this.baseUrl), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels, callerAddress }), + signal, + }); + + await throwIfNotOk(response); + return response.json() as Promise; + } +} diff --git a/packages/ensrainbow-sdk/src/index.ts b/packages/ensrainbow-sdk/src/index.ts index 7888ba6e29..0832181d32 100644 --- a/packages/ensrainbow-sdk/src/index.ts +++ b/packages/ensrainbow-sdk/src/index.ts @@ -10,3 +10,4 @@ export { buildEnsRainbowClientLabelSet } from "@ensnode/ensnode-sdk"; export * from "./client"; export * from "./consts"; +export * from "./ensrainbowbeam-client"; diff --git a/packages/ensrainbow-sdk/tsup.config.ts b/packages/ensrainbow-sdk/tsup.config.ts index 7771960ec4..5c9121fd74 100644 --- a/packages/ensrainbow-sdk/tsup.config.ts +++ b/packages/ensrainbow-sdk/tsup.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ entry: { client: "src/client.ts", consts: "src/consts.ts", + "ensrainbowbeam-client": "src/ensrainbowbeam-client.ts", index: "src/index.ts", }, platform: "browser", diff --git a/packages/enssdk/src/lib/index.ts b/packages/enssdk/src/lib/index.ts index b7855d70b4..33462ba026 100644 --- a/packages/enssdk/src/lib/index.ts +++ b/packages/enssdk/src/lib/index.ts @@ -11,6 +11,7 @@ export * from "./labelhash"; export * from "./namehash"; export * from "./names"; export * from "./normalization"; +export * from "./omnigraph-consts"; export * from "./parse-labelhash"; export * from "./parse-reverse-name"; export * from "./reinterpretation"; From 23b4eaa0df05fae408eb6a70efe439f73f481c2f Mon Sep 17 00:00:00 2001 From: djstrong Date: Mon, 4 May 2026 21:08:34 +0200 Subject: [PATCH 20/27] Add tests for `EnsRainbowBeamClient` and `validateDiscoverParams`: Implement comprehensive unit tests covering validation logic, health checks, and discover functionality, ensuring proper error handling and response validation. Introduce `omnigraph-consts.ts` for server-side limits on label requests. --- .../src/ensrainbowbeam-client.test.ts | 240 ++++++++++++++++++ packages/enssdk/src/lib/omnigraph-consts.ts | 7 + 2 files changed, 247 insertions(+) create mode 100644 packages/ensrainbow-sdk/src/ensrainbowbeam-client.test.ts create mode 100644 packages/enssdk/src/lib/omnigraph-consts.ts diff --git a/packages/ensrainbow-sdk/src/ensrainbowbeam-client.test.ts b/packages/ensrainbow-sdk/src/ensrainbowbeam-client.test.ts new file mode 100644 index 0000000000..dd43772a64 --- /dev/null +++ b/packages/ensrainbow-sdk/src/ensrainbowbeam-client.test.ts @@ -0,0 +1,240 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + DEFAULT_ENSRAINBOWBEAM_URL, + ENSRAINBOWBEAM_DISCOVER_MAX_LABELS, + EnsRainbowBeamClient, + EnsRainbowBeamHttpError, + validateDiscoverParams, +} from "./ensrainbowbeam-client"; + +const mockFetch = vi.fn(); +vi.stubGlobal("fetch", mockFetch); + +describe("validateDiscoverParams", () => { + const caller = "0x1234567890123456789012345678901234567890"; + + it("accepts valid input and lowercases caller", () => { + expect( + validateDiscoverParams({ + labels: ["a"], + callerAddress: "0xAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAa", + }), + ).toEqual({ + labels: ["a"], + callerAddress: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }); + }); + + it("rejects empty labels array", () => { + expect(() => validateDiscoverParams({ labels: [], callerAddress: caller })).toThrow( + /non-empty array/i, + ); + }); + + it("rejects too many labels", () => { + const labels = Array.from( + { length: ENSRAINBOWBEAM_DISCOVER_MAX_LABELS + 1 }, + (_, i) => `x${i}`, + ); + expect(() => validateDiscoverParams({ labels, callerAddress: caller })).toThrow(/at most 100/i); + }); + + it("rejects empty string label", () => { + expect(() => validateDiscoverParams({ labels: [""], callerAddress: caller })).toThrow( + /non-empty/i, + ); + }); + + it("rejects invalid caller", () => { + expect(() => + validateDiscoverParams({ labels: ["x"], callerAddress: "not-an-address" }), + ).toThrow(/does not represent an EVM Address/i); + }); +}); + +describe("EnsRainbowBeamClient", () => { + let client: EnsRainbowBeamClient; + + beforeEach(() => { + client = new EnsRainbowBeamClient({ baseUrl: new URL("http://beam.test") }); + mockFetch.mockReset(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("defaults base URL to localhost:4444", () => { + const defaultClient = new EnsRainbowBeamClient(); + expect(defaultClient).toBeDefined(); + // baseUrl is private; behavior covered by URL construction in health test with explicit URL + expect(DEFAULT_ENSRAINBOWBEAM_URL).toBe("http://localhost:4444"); + }); + + it("health resolves on 200", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: "OK", + json: () => Promise.resolve({ message: "ok" }), + }); + + await expect(client.health()).resolves.toEqual({ message: "ok" }); + + expect(mockFetch).toHaveBeenCalledWith( + new URL("http://beam.test/health"), + expect.objectContaining({ signal: undefined }), + ); + }); + + it("health throws EnsRainbowBeamHttpError on non-ok", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 503, + statusText: "Service Unavailable", + json: () => Promise.resolve({ message: "down" }), + }); + + await expect(client.health()).rejects.toMatchObject({ + name: "EnsRainbowBeamHttpError", + message: "down", + status: 503, + statusText: "Service Unavailable", + }); + }); + + it("discover posts JSON body and resolves", async () => { + const caller = "0x1234567890123456789012345678901234567890"; + const payload = { + callerAddress: caller, + results: [ + { + rawLabel: "eth", + labelHash: "0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0" as const, + status: "healed_in_index" as const, + }, + ], + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: "OK", + json: () => Promise.resolve(payload), + }); + + await expect(client.discover({ labels: ["eth"], callerAddress: caller })).resolves.toEqual( + payload, + ); + + expect(mockFetch).toHaveBeenCalledWith( + new URL("http://beam.test/api/discover"), + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ labels: ["eth"], callerAddress: caller }), + }), + ); + }); + + it("discover throws before fetch when validation fails", async () => { + await expect( + client.discover({ + labels: [], + callerAddress: "0x1234567890123456789012345678901234567890", + }), + ).rejects.toThrow(/non-empty array/i); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("discover throws EnsRainbowBeamHttpError with message and details on 400", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 400, + statusText: "Bad Request", + json: () => + Promise.resolve({ + message: "Invalid Input", + details: { formErrors: [], fieldErrors: {} }, + }), + }); + + try { + await client.discover({ + labels: ["x"], + callerAddress: "0x1234567890123456789012345678901234567890", + }); + expect.fail("expected throw"); + } catch (e) { + expect(e).toBeInstanceOf(EnsRainbowBeamHttpError); + const err = e as EnsRainbowBeamHttpError; + expect(err.message).toBe("Invalid Input"); + expect(err.status).toBe(400); + expect(err.details).toEqual({ formErrors: [], fieldErrors: {} }); + } + }); + + it("discover throws EnsRainbowBeamHttpError on 502", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 502, + statusText: "Bad Gateway", + json: () => Promise.resolve({ message: "Upstream Omnigraph lookup failed" }), + }); + + await expect( + client.discover({ + labels: ["x"], + callerAddress: "0x1234567890123456789012345678901234567890", + }), + ).rejects.toMatchObject({ + name: "EnsRainbowBeamHttpError", + message: "Upstream Omnigraph lookup failed", + status: 502, + }); + }); + + it("discover throws EnsRainbowBeamHttpError on 504", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 504, + statusText: "Gateway Timeout", + json: () => + Promise.resolve({ + message: "Omnigraph labels lookup timed out after 10000ms", + }), + }); + + await expect( + client.discover({ + labels: ["x"], + callerAddress: "0x1234567890123456789012345678901234567890", + }), + ).rejects.toMatchObject({ + name: "EnsRainbowBeamHttpError", + message: "Omnigraph labels lookup timed out after 10000ms", + status: 504, + }); + }); + + it("uses injected fetch", async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: "OK", + json: () => Promise.resolve({ message: "ok" }), + }); + + const c = new EnsRainbowBeamClient({ + baseUrl: "http://custom.example", + fetch: customFetch as typeof fetch, + }); + + await c.health(); + expect(customFetch).toHaveBeenCalledWith( + new URL("http://custom.example/health"), + expect.anything(), + ); + }); +}); diff --git a/packages/enssdk/src/lib/omnigraph-consts.ts b/packages/enssdk/src/lib/omnigraph-consts.ts new file mode 100644 index 0000000000..b7b1604c44 --- /dev/null +++ b/packages/enssdk/src/lib/omnigraph-consts.ts @@ -0,0 +1,7 @@ +/** + * Omnigraph server-side cap for `Query.labels(by: { labelHashes })`. + * + * This limit is enforced in ENSApi and must be respected by any callers that batch requests + * (e.g. EnsRainbowBeam) to avoid `BAD_USER_INPUT` errors. + */ +export const OMNIGRAPH_LABELS_BY_LABELHASH_MAX = 100 as const; From b79863cd0c08e0d341ff7ab6b9de2272e6f1672c Mon Sep 17 00:00:00 2001 From: djstrong Date: Tue, 5 May 2026 14:07:50 +0200 Subject: [PATCH 21/27] Add `EnsRainbowBeam` support: Integrate CORS handling, update deployment workflows for `ensrainbowbeam`, and enhance configuration with CORS origins. Update Terraform modules for deployment and add new environment variables. Enhance UI components for label submission and results display. --- .../workflows/deploy_ensnode_blue_green.yml | 10 + .github/workflows/deploy_ensnode_yellow.yml | 2 + .../deploy_switch_ensnode_environment.yml | 4 + apps/ensrainbowbeam/.env.local.example | 5 + apps/ensrainbowbeam/README.md | 1 + apps/ensrainbowbeam/src/app.ts | 12 ++ apps/ensrainbowbeam/src/config.ts | 10 + docs/ensrainbow.io/package.json | 1 + .../src/components/atoms/LearnMoreButton.tsx | 2 +- .../components/organisms/HealUnknownName.tsx | 189 +++++++++++++++++- docs/ensrainbow.io/src/pages/index.astro | 2 +- pnpm-lock.yaml | 3 + terraform/main.tf | 12 ++ terraform/modules/ensrainbowbeam/main.tf | 19 ++ terraform/modules/ensrainbowbeam/provider.tf | 9 + terraform/modules/ensrainbowbeam/variables.tf | 31 +++ 16 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 terraform/modules/ensrainbowbeam/main.tf create mode 100644 terraform/modules/ensrainbowbeam/provider.tf create mode 100644 terraform/modules/ensrainbowbeam/variables.tf diff --git a/.github/workflows/deploy_ensnode_blue_green.yml b/.github/workflows/deploy_ensnode_blue_green.yml index 36b8d3e9da..b42d37a742 100644 --- a/.github/workflows/deploy_ensnode_blue_green.yml +++ b/.github/workflows/deploy_ensnode_blue_green.yml @@ -28,6 +28,7 @@ jobs: ENSINDEXER_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensindexer:${{ inputs.tag }}" ENSAPI_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensapi:${{ inputs.tag }}" ENSRAINBOW_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensrainbow:${{ inputs.tag }}" + ENSRAINBOWBEAM_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensrainbowbeam:${{ inputs.tag }}" ENSADMIN_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensadmin:${{ inputs.tag }}" RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }} @@ -45,6 +46,7 @@ jobs: docker manifest inspect ${{ env.ENSINDEXER_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSINDEXER_DOCKER_IMAGE }}"; exit 1; } docker manifest inspect ${{ env.ENSAPI_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSAPI_DOCKER_IMAGE }}"; exit 1; } docker manifest inspect ${{ env.ENSRAINBOW_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSRAINBOW_DOCKER_IMAGE }}"; exit 1; } + docker manifest inspect ${{ env.ENSRAINBOWBEAM_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSRAINBOWBEAM_DOCKER_IMAGE }}"; exit 1; } docker manifest inspect ${{ env.ENSADMIN_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSADMIN_DOCKER_IMAGE }}"; exit 1; } - name: Print switch target @@ -76,6 +78,8 @@ jobs: echo "ENSRAINBOW_SVC_ID="${{ secrets.GREEN_ENSRAINBOW_SVC_ID }} >> "$GITHUB_ENV" #ENSRAINBOW SEARCHLIGHT echo "ENSRAINBOW_SEARCHLIGHT_SVC_ID="${{ secrets.GREEN_ENSRAINBOW_SEARCHLIGHT_SVC_ID }} >> "$GITHUB_ENV" + #ENSRAINBOWBEAM + echo "ENSRAINBOWBEAM_SVC_ID="${{ secrets.GREEN_ENSRAINBOWBEAM_SVC_ID }} >> "$GITHUB_ENV" #ENSADMIN echo "ENSADMIN_SVC_ID="${{ secrets.GREEN_ENSADMIN_SVC_ID }} >> "$GITHUB_ENV" echo "SLACK_TITLE=':large_green_circle: GREEN environment is now having new ENSNode version - '"${{ env.TAG }} >> "$GITHUB_ENV" @@ -102,6 +106,8 @@ jobs: echo "ENSRAINBOW_SVC_ID="${{ secrets.BLUE_ENSRAINBOW_SVC_ID }} >> "$GITHUB_ENV" #ENSRAINBOW SEARCHLIGHT echo "ENSRAINBOW_SEARCHLIGHT_SVC_ID="${{ secrets.BLUE_ENSRAINBOW_SEARCHLIGHT_SVC_ID }} >> "$GITHUB_ENV" + #ENSRAINBOWBEAM + echo "ENSRAINBOWBEAM_SVC_ID="${{ secrets.BLUE_ENSRAINBOWBEAM_SVC_ID }} >> "$GITHUB_ENV" #ENSADMIN echo "ENSADMIN_SVC_ID="${{ secrets.BLUE_ENSADMIN_SVC_ID }} >> "$GITHUB_ENV" echo "SLACK_TITLE=':large_blue_circle: BLUE environment is now having new ENSNode version - '"${{ env.TAG }} >> "$GITHUB_ENV" @@ -155,6 +161,8 @@ jobs: update_service_image ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOW_SVC_ID} ${{ env.ENSRAINBOW_DOCKER_IMAGE }} #ENSRAINBOW SEARCHLIGHT update_service_image ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOW_SEARCHLIGHT_SVC_ID} ${{ env.ENSRAINBOW_DOCKER_IMAGE }} + #ENSRAINBOWBEAM + update_service_image ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOWBEAM_SVC_ID} ${{ env.ENSRAINBOWBEAM_DOCKER_IMAGE }} #ENSADMIN update_service_image ${RAILWAY_ENVIRONMENT_ID} ${ENSADMIN_SVC_ID} ${{ env.ENSADMIN_DOCKER_IMAGE }} @@ -216,6 +224,8 @@ jobs: redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOW_SVC_ID} #ENSRAINBOW SEARCHLIGHT redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOW_SEARCHLIGHT_SVC_ID} + #ENSRAINBOWBEAM + redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${ENSRAINBOWBEAM_SVC_ID} #ENSADMIN redeploy_service ${RAILWAY_ENVIRONMENT_ID} ${ENSADMIN_SVC_ID} diff --git a/.github/workflows/deploy_ensnode_yellow.yml b/.github/workflows/deploy_ensnode_yellow.yml index baf448d1a6..93231f72b3 100644 --- a/.github/workflows/deploy_ensnode_yellow.yml +++ b/.github/workflows/deploy_ensnode_yellow.yml @@ -20,6 +20,7 @@ jobs: ENSINDEXER_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensindexer:${{ inputs.tag }}" ENSAPI_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensapi:${{ inputs.tag }}" ENSRAINBOW_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensrainbow:${{ inputs.tag }}" + ENSRAINBOWBEAM_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensrainbowbeam:${{ inputs.tag }}" ENSADMIN_DOCKER_IMAGE: "ghcr.io/namehash/ensnode/ensadmin:${{ inputs.tag }}" # Terraform related envs @@ -55,6 +56,7 @@ jobs: docker manifest inspect ${{ env.ENSINDEXER_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSINDEXER_DOCKER_IMAGE }}"; exit 1; } docker manifest inspect ${{ env.ENSAPI_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSAPI_DOCKER_IMAGE }}"; exit 1; } docker manifest inspect ${{ env.ENSRAINBOW_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSRAINBOW_DOCKER_IMAGE }}"; exit 1; } + docker manifest inspect ${{ env.ENSRAINBOWBEAM_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSRAINBOWBEAM_DOCKER_IMAGE }}"; exit 1; } docker manifest inspect ${{ env.ENSADMIN_DOCKER_IMAGE }} || { echo "Given docker image does not exist: ${{ env.ENSADMIN_DOCKER_IMAGE }}"; exit 1; } - name: Setup Terraform diff --git a/.github/workflows/deploy_switch_ensnode_environment.yml b/.github/workflows/deploy_switch_ensnode_environment.yml index 2b5255beb8..f080f6b194 100644 --- a/.github/workflows/deploy_switch_ensnode_environment.yml +++ b/.github/workflows/deploy_switch_ensnode_environment.yml @@ -92,6 +92,10 @@ jobs: # ENSRAINBOW SEARCHLIGHT redis-cli -u $REDIS_URL SET traefik/http/routers/ensrainbow-searchlight-api-router/service "${TARGET_ENVIRONMENT}-ensrainbow-searchlight-api" + # ENSRAINBOWBEAM + # NOTE: Router/service names must match Traefik IaC (likely in namehash-tf-iac-live). + redis-cli -u $REDIS_URL SET traefik/http/routers/ensrainbowbeam-api-router/service "${TARGET_ENVIRONMENT}-ensrainbowbeam-api" + - name: Promote ENSAdmin Vercel Deployment uses: ./.github/actions/promote_vercel_deployment with: diff --git a/apps/ensrainbowbeam/.env.local.example b/apps/ensrainbowbeam/.env.local.example index 4a9468cc22..11d0bf219d 100644 --- a/apps/ensrainbowbeam/.env.local.example +++ b/apps/ensrainbowbeam/.env.local.example @@ -4,3 +4,8 @@ PORT=4444 # Base URL of an ENSNode (ENSApi) instance that exposes the Omnigraph GraphQL endpoint. # EnsRainbowBeam calls `${ENSNODE_URL}/api/omnigraph` to classify submitted labels. ENSNODE_URL=http://localhost:4334 + +# Comma-separated allowlist of CORS origins for browser calls. +# Example for local dev + prod marketing site: +# CORS_ORIGINS=http://localhost:4321,https://ensrainbow.io +CORS_ORIGINS=http://localhost:4321 diff --git a/apps/ensrainbowbeam/README.md b/apps/ensrainbowbeam/README.md index 86c5663300..6803b55420 100644 --- a/apps/ensrainbowbeam/README.md +++ b/apps/ensrainbowbeam/README.md @@ -41,6 +41,7 @@ For each submitted raw label EnsRainbowBeam: |---------|----------|-------------| | `PORT` | no (default `4444`) | HTTP listen port. | | `ENSNODE_URL` | yes | Base URL of an ENSNode (ENSApi) instance with Omnigraph at `/api/omnigraph`. | +| `CORS_ORIGINS` | no | Comma-separated allowlist of CORS origins (e.g. `https://ensrainbow.io,http://localhost:4321`). When unset/empty, no cross-origin requests are allowed. | See `.env.local.example` for a local-development template. diff --git a/apps/ensrainbowbeam/src/app.ts b/apps/ensrainbowbeam/src/app.ts index 4675a263d2..a1974a66b3 100644 --- a/apps/ensrainbowbeam/src/app.ts +++ b/apps/ensrainbowbeam/src/app.ts @@ -1,4 +1,7 @@ +import { config } from "@/config"; + import { Hono } from "hono"; +import { cors } from "hono/cors"; import { healthHandler } from "@/handlers/health"; import { submissionsHandler } from "@/handlers/submissions"; @@ -6,6 +9,15 @@ import { errorResponse } from "@/lib/error-response"; const app = new Hono(); +app.use( + cors({ + origin: (origin) => (config.corsOrigins.includes(origin) ? origin : undefined), + allowMethods: ["GET", "POST", "OPTIONS"], + allowHeaders: ["Content-Type"], + maxAge: 86400, + }), +); + app.get("/health", healthHandler); app.post("/api/discover", submissionsHandler); diff --git a/apps/ensrainbowbeam/src/config.ts b/apps/ensrainbowbeam/src/config.ts index 1b882fa46e..af57cfd91d 100644 --- a/apps/ensrainbowbeam/src/config.ts +++ b/apps/ensrainbowbeam/src/config.ts @@ -7,9 +7,18 @@ import { OptionalPortNumberSchema } from "@ensnode/ensnode-sdk/internal"; */ export const ENSRAINBOWBEAM_DEFAULT_PORT = 4444; +function parseCorsOrigins(value: string | undefined): string[] { + if (!value) return []; + return value + .split(",") + .map((origin) => origin.trim()) + .filter((origin) => origin.length > 0); +} + const ConfigSchema = z.object({ PORT: OptionalPortNumberSchema.default(ENSRAINBOWBEAM_DEFAULT_PORT), ENSNODE_URL: z.string().url(), + CORS_ORIGINS: z.string().optional(), }); const parsed = ConfigSchema.parse(process.env); @@ -22,6 +31,7 @@ const parsed = ConfigSchema.parse(process.env); export const config = { port: parsed.PORT, ensNodeUrl: parsed.ENSNODE_URL, + corsOrigins: parseCorsOrigins(parsed.CORS_ORIGINS), }; export type Config = typeof config; diff --git a/docs/ensrainbow.io/package.json b/docs/ensrainbow.io/package.json index 08dce6d274..c33491450e 100644 --- a/docs/ensrainbow.io/package.json +++ b/docs/ensrainbow.io/package.json @@ -17,6 +17,7 @@ "dependencies": { "@astrojs/react": "catalog:", "@heroicons/react": "^2.2.0", + "@ensnode/ensrainbow-sdk": "workspace:*", "@namehash/namehash-ui": "workspace:*", "@tailwindcss/vite": "^4.1.15", "astro": "catalog:", diff --git a/docs/ensrainbow.io/src/components/atoms/LearnMoreButton.tsx b/docs/ensrainbow.io/src/components/atoms/LearnMoreButton.tsx index edf5ae41fe..d6106928c0 100644 --- a/docs/ensrainbow.io/src/components/atoms/LearnMoreButton.tsx +++ b/docs/ensrainbow.io/src/components/atoms/LearnMoreButton.tsx @@ -24,7 +24,7 @@ export const LearnMoreButton = ({ className={legacyButtonVariants({ variant: "secondary", size: "medium", - className: cc("max-w-full overflow-x-hidden", styles), + className: cc(["max-w-full overflow-x-hidden", styles]), })} > {text} diff --git a/docs/ensrainbow.io/src/components/organisms/HealUnknownName.tsx b/docs/ensrainbow.io/src/components/organisms/HealUnknownName.tsx index c8110b1504..9fe22bfcda 100644 --- a/docs/ensrainbow.io/src/components/organisms/HealUnknownName.tsx +++ b/docs/ensrainbow.io/src/components/organisms/HealUnknownName.tsx @@ -1,3 +1,190 @@ +import { legacyButtonVariants } from "@namehash/namehash-ui/legacy"; +import { useMemo, useState } from "react"; + +import { + EnsRainbowBeamClient, + EnsRainbowBeamHttpError, + validateDiscoverParams, +} from "@ensnode/ensrainbow-sdk/ensrainbowbeam-client"; + +type StatusBadgeProps = { + status: "unknown_in_index" | "healed_in_index" | "absent_from_index"; +}; + +function StatusBadge({ status }: StatusBadgeProps) { + const { label, className } = + status === "unknown_in_index" + ? { label: "Unknown", className: "bg-amber-100 text-amber-900" } + : status === "healed_in_index" + ? { label: "Healed", className: "bg-emerald-100 text-emerald-900" } + : { label: "Absent", className: "bg-gray-100 text-gray-900" }; + + return ( + + {label} + + ); +} + +const DEFAULT_BEAM_URL = "https://beam.ensrainbow.io"; + +function parseTextareaLabels(text: string): string[] { + return text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0); +} + export default function HealUnknownName() { - return

HEAL UNKNOWN NAME

; + const [rawLabels, setRawLabels] = useState(""); + const [callerAddress, setCallerAddress] = useState(""); + const [beamUrl, setBeamUrl] = useState(DEFAULT_BEAM_URL); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [results, setResults] = useState< + Array<{ + rawLabel: string; + normalizedLabel?: string; + status: "unknown_in_index" | "healed_in_index" | "absent_from_index"; + }> + >([]); + + const labels = useMemo(() => parseTextareaLabels(rawLabels), [rawLabels]); + + const canSubmit = !isSubmitting && labels.length > 0 && callerAddress.trim().length > 0; + + async function onBeamIt() { + setIsSubmitting(true); + setError(null); + setResults([]); + + try { + const validated = validateDiscoverParams({ labels, callerAddress }); + const client = new EnsRainbowBeamClient({ baseUrl: beamUrl }); + const res = await client.discover({ + labels: validated.labels, + callerAddress: validated.callerAddress, + }); + + setResults( + res.results.map((item) => ({ + rawLabel: item.rawLabel, + normalizedLabel: item.normalizedLabel, + status: item.status, + })), + ); + } catch (err) { + if (err instanceof EnsRainbowBeamHttpError) { + setError(`Beam request failed (${err.status}): ${err.message}`); + } else if (err instanceof Error) { + setError(err.message); + } else { + setError(String(err)); + } + } finally { + setIsSubmitting(false); + } + } + + return ( +
+
+

Beam labels for discovery

+

+ Paste one ENS label per line. We’ll classify each one against ENSNode’s index and tell you + whether it’s already healed, still unknown, or absent. +

+
+ +
+
+ + +