Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
19b42ed
feat: add ens-labels-collector app for label submission and classific…
djstrong Apr 29, 2026
c57108e
feat: increase label submission limits and update related tests
djstrong Apr 29, 2026
c0c013e
chore: update pnpm-lock.yaml with new dependency hashes and version a…
djstrong Apr 29, 2026
3537486
feat(yoga): add masked error handling for production environment
djstrong Apr 29, 2026
440023c
feat(ens-labels-collector): enhance error handling and add timeout fo…
djstrong Apr 29, 2026
4b49138
chore(pnpm-lock): add ensnode-sdk dependency for ens-labels-collector
djstrong Apr 29, 2026
912c16c
Merge branch 'main' into 2003-add-new-app-with-endpoint-for-submittin…
djstrong Apr 30, 2026
3021d5c
feat(ens-labels-collector): enhance timeout handling in label lookups
djstrong Apr 30, 2026
8c41088
refactor: improve configuration parsing and logging
djstrong Apr 30, 2026
4634cc4
refactor(ens-labels-collector): streamline configuration handling and…
djstrong Apr 30, 2026
0b5365b
docs(ens-labels-collector): clarify submission limits and normalizati…
djstrong Apr 30, 2026
ff194f9
feat(ens-labels-collector): improve error handling for omnigraph lookups
djstrong Apr 30, 2026
2adbc92
Merge branch 'main' into 2003-add-new-app-with-endpoint-for-submittin…
djstrong Apr 30, 2026
74ff599
Merge branch 'main' into 2003-add-new-app-with-endpoint-for-submittin…
djstrong Apr 30, 2026
053913b
update zod
djstrong May 4, 2026
2005200
Merge branch 'main' into 2003-add-new-app-with-endpoint-for-submittin…
djstrong May 4, 2026
caf277d
rename app to ensrainbowbeam
djstrong May 4, 2026
a723be4
Add `EnsRainbowBeam` app with `/api/discover` endpoint for label subm…
djstrong May 4, 2026
d643370
Refactor label handling in submissions: update `SubmissionsRequestSch…
djstrong May 4, 2026
7597168
Enhance `Query.labels` functionality: Introduce stricter validation f…
djstrong May 4, 2026
6e1f410
Enhance `LabelHash` parsing: Update `parseValue` to include error han…
djstrong May 4, 2026
07c9d1f
lint
djstrong May 4, 2026
33e3ed1
Add `EnsRainbowBeamClient` for HTTP interactions: Implement `health()…
djstrong May 4, 2026
23b4eaa
Add tests for `EnsRainbowBeamClient` and `validateDiscoverParams`: Im…
djstrong May 4, 2026
b79863c
Add `EnsRainbowBeam` support: Integrate CORS handling, update deploym…
djstrong May 5, 2026
d01acae
lint
djstrong May 5, 2026
2ae9acc
Refactor label submission handling: Update submissions to skip unnorm…
djstrong May 6, 2026
5cfdb4a
Remove CORS handling from configuration and application logic: Update…
djstrong May 6, 2026
e8e4767
enables CORS for all origins
djstrong May 6, 2026
ba036a6
Merge branch 'main' into 2003-add-new-app-with-endpoint-for-submittin…
djstrong May 18, 2026
e7f9df5
refactor(ensrainbowbeam): update label submission processing to norma…
djstrong May 18, 2026
5b92ad7
refactor(ensrainbowbeam): update LabelHit structure to use 'labelhash…
djstrong May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/ens-labels-collector-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"ens-labels-collector": minor
Comment thread
djstrong marked this conversation as resolved.
Outdated
"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.
6 changes: 6 additions & 0 deletions apps/ens-labels-collector/.env.local.example
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions apps/ens-labels-collector/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
50 changes: 50 additions & 0 deletions apps/ens-labels-collector/README.md
Original file line number Diff line number Diff line change
@@ -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
```
37 changes: 37 additions & 0 deletions apps/ens-labels-collector/package.json
Original file line number Diff line number Diff line change
@@ -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:"
}
}
20 changes: 20 additions & 0 deletions apps/ens-labels-collector/src/app.ts
Original file line number Diff line number Diff line change
@@ -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 });
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
});

export default app;
43 changes: 43 additions & 0 deletions apps/ens-labels-collector/src/config.ts
Original file line number Diff line number Diff line change
@@ -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)),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
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;
Comment thread
djstrong marked this conversation as resolved.
Outdated
}

/**
* Resets the memoized config. Test-only.
*/
export function resetConfigCacheForTesting(): void {
cachedConfig = undefined;
}
5 changes: 5 additions & 0 deletions apps/ens-labels-collector/src/handlers/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Context } from "hono";

export function healthHandler(c: Context) {
return c.json({ message: "ok" });
}
Loading
Loading