Skip to content

Build ENSRainbow config#1425

Open
djstrong wants to merge 38 commits intomainfrom
1407-build-ensrainbow-config
Open

Build ENSRainbow config#1425
djstrong wants to merge 38 commits intomainfrom
1407-build-ensrainbow-config

Conversation

@djstrong
Copy link
Contributor

@djstrong djstrong commented Dec 22, 2025

Related to #1407

Lite PR

Summary

  • Built new Zod-based configuration system for ENSRainbow, replacing ad-hoc env parsing with structured validation
  • Added /v1/config endpoint returning public config (version, label set, records count) and deprecated /v1/version

Why

  • Centralizes configuration management with proper validation and type safety
  • Provides comprehensive public config endpoint for clients to discover service capabilities
  • Improves developer experience with better error messages and validation

Testing

  • Added comprehensive test suite for config schema validation (config.schema.test.ts) covering success cases, validation errors, invariants, and edge cases
  • Updated existing CLI and server command tests to work with new config system
  • Verified new /v1/config endpoint in server command tests

Notes for Reviewer (Optional)

  • The old /v1/version endpoint is deprecated but still functional for backward compatibility
  • Config is built at module load time and will exit process on validation failure (appropriate for CLI apps)

Pre-Review Checklist (Blocking)

  • This PR does not introduce significant changes and is low-risk to review quickly.
  • Relevant changesets are included (or are not required)

@vercel
Copy link
Contributor

vercel bot commented Dec 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
admin.ensnode.io Skipped Skipped Feb 3, 2026 9:47pm
ensnode.io Skipped Skipped Feb 3, 2026 9:47pm
ensrainbow.io Skipped Skipped Feb 3, 2026 9:47pm

@changeset-bot
Copy link

changeset-bot bot commented Dec 22, 2025

🦋 Changeset detected

Latest commit: 75e72f8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
ensrainbow Patch
@ensnode/ensrainbow-sdk Patch
ensindexer Patch
@ensnode/ponder-metadata Patch
ensadmin Patch
ensapi Patch
fallback-ensapi Patch
@ensnode/datasources Patch
@ensnode/ensnode-schema Patch
@ensnode/ensnode-react Patch
@ensnode/ponder-subgraph Patch
@ensnode/ensnode-sdk Patch
@ensnode/shared-configs Patch
@docs/ensnode Patch
@docs/ensrainbow Patch
@docs/mintlify Patch
@namehash/ens-referrals Patch
@namehash/namehash-ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI review requested due to automatic review settings January 21, 2026 13:22
@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Centralizes ENSRainbow configuration with Zod schemas and a runtime config, removes legacy env helpers, tightens port validation, adds a cached GET /v1/config and SDK client support, updates CLI/tests for port coercion and isolation, adds startup DB sanity checks, and introduces comprehensive config tests and a zod dependency.

Changes

Cohort / File(s) Summary
Config schema & infra
apps/ensrainbow/src/config/config.schema.ts, apps/ensrainbow/src/config/index.ts, apps/ensrainbow/src/config/types.ts, apps/ensrainbow/src/config/defaults.ts, apps/ensrainbow/src/config/environment.ts, apps/ensrainbow/src/config/validations.ts
Add Zod-backed schemas, ENSRainbowEnvironment type, ENSRAINBOW_DEFAULT_PORT, getDefaultDataDir(), DB-schema invariant validation, buildConfigFromEnvironment, buildENSRainbowPublicConfig, and export a default config from process.env.
CLI, tests & env typing
apps/ensrainbow/src/cli.ts, apps/ensrainbow/src/cli.test.ts, apps/ensrainbow/types/env.d.ts
CLI now reads defaults from centralized config; port option uses PortSchema coercion and errors on invalid ports; tests refactored for module isolation, env stubbing, and expanded port scenarios; global ProcessEnv now extends ENSRainbowEnvironment.
Removed env helpers
apps/ensrainbow/src/lib/env.ts
Remove legacy env helpers (DEFAULT_PORT, getDefaultDataSubDir, getEnvPort) and in-file env parsing — logic moved to config layer.
API startup & endpoints
apps/ensrainbow/src/lib/api.ts, apps/ensrainbow/src/commands/server-command.ts, apps/ensrainbow/src/commands/server-command.test.ts
On startup perform DB sanity check (precalculated record count) and abort on failure; compute and cache public config at startup; add GET /v1/config returning cached config; reduce per-request debug logs; tests added for /v1/config and related CORS behavior.
SDK client
packages/ensrainbow-sdk/src/client.ts
Add config(): Promise<ENSRainbowPublicConfig> to fetch /v1/config, introduce ENSRainbowPublicConfig type, and deprecate prior version response in favour of the new public config.
Shared validation tightening
packages/ensnode-sdk/src/shared/config/zod-schemas.ts, packages/ensnode-sdk/src/shared/config/types.ts
Introduce PortSchemaBase and PortNumber type; require integer ports and tighten min/max validations and error messages.
Config tests
apps/ensrainbow/src/config/config.schema.test.ts, apps/ensrainbow/src/config/env-config.test.ts
Add extensive unit tests covering buildConfigFromEnvironment, buildENSRainbowPublicConfig, path normalization, port/db/label validations, invariants, and many edge/error cases.
Dependency & changeset
apps/ensrainbow/package.json, .changeset/young-carrots-cheer.md
Add zod dependency to ENSRainbow package and add a changeset documenting the new /v1/config endpoint and deprecation of /v1/version.
Misc tests
apps/ensrainbow/src/cli.test.ts, apps/ensrainbow/src/commands/server-command.test.ts
Refactor tests for CLI port behavior, add health/serve port tests, and tests ensuring cached /v1/config behavior remains stable after DB changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant SDK as "SDK Client"
  participant Server as "ENSRainbow API"
  participant DB as "Database / Label store"
  participant Builder as "buildENSRainbowPublicConfig"

  SDK->>Server: GET /v1/config
  Server->>DB: getServerLabelSet() (cached)
  Server->>DB: labelCount() (startup-cached)
  DB-->>Server: labelSet, count
  Server->>Builder: assemble public config (version, labelSet, count)
  Builder-->>Server: ENSRainbowPublicConfig
  Server-->>SDK: 200 + ENSRainbowPublicConfig
Loading
sequenceDiagram
  autonumber
  participant CLI as "CLI"
  participant ConfigBuilder as "buildConfigFromEnvironment"
  participant Zod as "Zod schemas"
  participant Process as "process"

  CLI->>ConfigBuilder: request config from env
  ConfigBuilder->>Zod: parse & validate environment -> ENSRainbowConfig
  Zod-->>ConfigBuilder: validation result
  alt valid
    ConfigBuilder-->>CLI: return ENSRainbowConfig
    CLI->>CLI: continue startup (serve/ingest) using config
  else invalid
    ConfigBuilder->>Process: log errors (pretty)
    Process->>Process: exit non-zero
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

ensnode-sdk

Poem

🐇 I nibbled env vars, parsed paths with care,

Zod stitched defaults and kept the rules fair,
Ports now count proper, startup checks stand guard,
Cached config hums softly, server starts unmarred,
A happy hop for code — carrot dreams prepared 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Build ENSRainbow config' directly summarizes the main change: building a new configuration system for ENSRainbow with Zod-based validation.
Description check ✅ Passed The PR description follows the required template structure with Summary, Why, Testing, Notes for Reviewer, and Pre-Review Checklist sections, though the blocking checklist item is unchecked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 1407-build-ensrainbow-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@apps/ensrainbow/src/config/config.schema.ts`:
- Around line 65-71: The current ternary for labelSet uses a truthy check
(env.LABEL_SET_ID || env.LABEL_SET_VERSION) which treats empty strings as
missing; change the condition to explicit undefined checks so an empty string is
treated as a provided value and validation will run — e.g. replace the condition
with (env.LABEL_SET_ID !== undefined || env.LABEL_SET_VERSION !== undefined) and
still return the object with labelSetId: env.LABEL_SET_ID and labelSetVersion:
env.LABEL_SET_VERSION when true; keep the symbol name labelSet and the env keys
env.LABEL_SET_ID / env.LABEL_SET_VERSION so locators remain obvious.
- Around line 33-36: The schema currently calls getDefaultDataDir() at module
load in ENSRainbowConfigSchema (dataDir:
DataDirSchema.default(getDefaultDataDir())), capturing process.cwd() too early;
remove the eager default from ENSRainbowConfigSchema and instead handle lazy
evaluation in buildConfigFromEnvironment by supplying dataDir: env.DATA_DIR ??
getDefaultDataDir() when parsing/building the config, keeping
ENSRainbowConfigSchema (and DataDirSchema/PortSchema) purely declarative and
ensuring getDefaultDataDir() runs only at build time.
- Around line 18-24: The path transform in the config schema currently treats
paths starting with "/" as absolute; update the transform used on the config
field to use Node's path.isAbsolute(path) instead of path.startsWith("/"), and
ensure the Node "path" module is imported (or isAbsolute is referenced)
alongside the existing join and process.cwd() usage in the transform callback so
Windows absolute paths like "C:\..." are detected correctly and returned
unchanged.
- Around line 73-83: Replace the terminal process.exit(1) in the catch block
with throwing a descriptive error so callers can handle failures; specifically,
inside the catch for buildConfigFromEnvironment (or whatever function constructs
ENSRainbowConfig) throw a custom error (e.g., ConfigBuildError) or rethrow the
existing Error with context including the prettified ZodError output and the
message "Failed to build ENSRainbowConfig", while keeping the existing logger
calls for ZodError and generic Error; move any process.exit(1) behavior out to
the CLI/entrypoint so tests can catch the thrown error and decide whether to
exit.

In `@apps/ensrainbow/src/config/validations.ts`:
- Around line 7-10: The current type ZodCheckFnInput<T> uses the internal
z.core.ParsePayload<T>; change it to rely on Zod's documented types or a simple
explicit input shape instead: remove z.core.ParsePayload and either use the
public helper z.input with a Zod type (e.g., z.input<z.ZodType<T>>) or replace
ZodCheckFnInput<T> with a small explicit interface/alias (e.g., unknown or
Record<string, any> or a narrow shape your check expects) so the code no longer
depends on the unstable z.core namespace; update any usages of ZodCheckFnInput
to match the new public type.

In `@apps/ensrainbow/src/lib/env.ts`:
- Around line 7-10: The getEnvPort function unsafely asserts process.env as
ENSRainbowEnvironment and rebuilds the full config on every call; remove the
type assertion and instead import the ENSRainbowConfig type (import type {
ENSRainbowConfig } ...) and let buildConfigFromEnvironment validate process.env
at runtime, receiving an ENSRainbowConfig result; then read and return
config.port. Also memoize the built config in a module-level variable so
getEnvPort calls reuse the same config instead of reconstructing it each time
(references: getEnvPort, buildConfigFromEnvironment, ENSRainbowEnvironment,
ENSRainbowConfig).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a Zod-based, centralized environment configuration builder for the ENSRainbow app, aligning it with the configuration patterns used in other apps in the monorepo.

Changes:

  • Added ENSRainbow config schema, environment types, defaults, and cross-field validations.
  • Updated ENSRainbow CLI/env port handling to use the new config builder and centralized defaults.
  • Tightened shared PortSchema validation to require integer ports; added zod as a direct ENSRainbow dependency.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds zod to the ENSRainbow importer lock entry.
packages/ensnode-sdk/src/shared/config/zod-schemas.ts Updates shared PortSchema to require integer ports.
apps/ensrainbow/src/lib/env.ts Switches env port resolution to buildConfigFromEnvironment(...).
apps/ensrainbow/src/config/validations.ts Adds ENSRainbow-specific invariant validation for schema version.
apps/ensrainbow/src/config/types.ts Re-exports ENSRainbow config type.
apps/ensrainbow/src/config/index.ts Adds a config module entrypoint exporting types/functions/defaults.
apps/ensrainbow/src/config/environment.ts Defines typed raw environment shape for ENSRainbow.
apps/ensrainbow/src/config/defaults.ts Centralizes ENSRainbow default port and data dir.
apps/ensrainbow/src/config/config.schema.ts Adds ENSRainbow Zod schema + config builder with logging/exit-on-failure behavior.
apps/ensrainbow/src/cli.ts Uses new defaults module for data dir default; continues using env-derived port.
apps/ensrainbow/src/cli.test.ts Updates port tests to reflect process-exit behavior on invalid PORT values.
apps/ensrainbow/package.json Adds zod as an explicit dependency for ENSRainbow.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 21, 2026 15:45 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 21, 2026 15:45 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 21, 2026 15:45 Inactive
Copilot AI review requested due to automatic review settings January 21, 2026 15:48
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 21, 2026 15:48 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 21, 2026 15:48 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 2, 2026 17:39 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 2, 2026 17:39 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 2, 2026 17:39 Inactive
@djstrong djstrong requested a review from Copilot February 2, 2026 17:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel vercel bot temporarily deployed to Preview – ensnode.io February 3, 2026 16:05 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 3, 2026 16:05 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 3, 2026 16:05 Inactive
Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@djstrong Thanks. Sharing some preliminary feedback.

* - If only one of LABEL_SET_ID or LABEL_SET_VERSION is provided in the environment,
* configuration parsing will fail with a clear error message
*/
labelSet?: Required<EnsRainbowClientLabelSet>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a server label set?

dbSchemaVersion: number;

/**
* Optional label set configuration that specifies which label set to use.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this optional? I assume it should be required?

* Both `labelSetId` and `labelSetVersion` must be provided together to create a "fully pinned"
* label set reference, ensuring deterministic and reproducible label healing.
*
* If not provided, ENSRainbow will start without any label set loaded, and label healing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what "the management API" is?

I assume ENSRainbow should refuse to start if this is undefined or a valid server label set is not configured?

*
* Examples:
* - `{ labelSetId: "subgraph", labelSetVersion: 0 }` - The legacy subgraph label set
* - `{ labelSetId: "ensip-15", labelSetVersion: 1 }` - ENSIP-15 normalized labels
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume better to use searchlight for this other example?

*
* Invariants:
* - If provided, both `labelSetId` and `labelSetVersion` must be defined
* - `labelSetId` must be 1-50 characters, containing only lowercase letters (a-z) and hyphens (-)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't repeat the definitions of ideas in lots of places. We already define what a label set id is somewhere else. This makes our code very difficult to maintain.

* This prevents version mismatches between the codebase and the database schema, which could
* lead to data corruption or runtime errors.
*
* Default: {@link DB_SCHEMA_VERSION} (currently 3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaults should not be documented here. Those ideas belong in a different layer.

*
* Invariants:
* - Must be a valid port number (1-65535)
* - Must not be already in use by another process
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This idea belongs at a different layer.

* Default: {@link DB_SCHEMA_VERSION} (currently 3)
*
* Invariants:
* - Must be a positive integer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to:

  1. Define a type alias for DbSchemaVersion.
  2. Document this invariant on the type alias.
  3. Use this type alias wherever it is relevant.
  4. Remove duplicate definitions of this invariant. The documentation of this invariant should only happen once on the type alias otherwise its so difficult to maintain.

* - Must be a valid port number (1-65535)
* - Must not be already in use by another process
*/
port: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to:

  1. Define a type alias for PortNumber.
  2. Document relevant invariants on the type alias.
  3. Use this type alias wherever it is relevant.
  4. Remove duplicate definitions of this invariant. The documentation of this invariant should only happen once on the type alias otherwise its so difficult to maintain.

/**
* Optional label set configuration that specifies which label set to use.
*
* A label set defines which labels (domain name segments) are available for label healing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This idea does not belong here! We are repeating the documentation of ideas in way too many places which quickly becomes impossible to maintain!

…clarity and update related types and functions
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/ensrainbow/src/config/config.schema.ts`:
- Around line 56-120: The LABEL_SET_ID and LABEL_SET_VERSION values are
forwarded raw in buildConfigFromEnvironment's envToConfigSchema.transform even
though presence checks use trimmed values; update the transform in
envToConfigSchema (inside buildConfigFromEnvironment) to trim LABEL_SET_ID and
LABEL_SET_VERSION before constructing the labelSet object (e.g., compute const
id = env.LABEL_SET_ID?.trim(); const ver = env.LABEL_SET_VERSION?.trim(); and
use those when setting labelSet), so downstream validation sees cleaned values;
keep existing hasValue checks as-is and only change the transform that builds
labelSet/returns the config.

Comment on lines 56 to 120
export function buildConfigFromEnvironment(env: ENSRainbowEnvironment): ENSRainbowEnvConfig {
try {
const envToConfigSchema = z
.object({
PORT: z.string().optional(),
DATA_DIR: z.string().optional(),
DB_SCHEMA_VERSION: z.string().optional(),
LABEL_SET_ID: z.string().optional(),
LABEL_SET_VERSION: z.string().optional(),
})
.check((ctx) => {
const { value: env } = ctx;
const hasLabelSetId = hasValue(env.LABEL_SET_ID);
const hasLabelSetVersion = hasValue(env.LABEL_SET_VERSION);

if (hasLabelSetId && !hasLabelSetVersion) {
ctx.issues.push({
code: "custom",
path: ["LABEL_SET_VERSION"],
input: env,
message:
"LABEL_SET_ID is set but LABEL_SET_VERSION is missing. Both LABEL_SET_ID and LABEL_SET_VERSION must be provided together, or neither.",
});
}

if (!hasLabelSetId && hasLabelSetVersion) {
ctx.issues.push({
code: "custom",
path: ["LABEL_SET_ID"],
input: env,
message:
"LABEL_SET_VERSION is set but LABEL_SET_ID is missing. Both LABEL_SET_ID and LABEL_SET_VERSION must be provided together, or neither.",
});
}
})
.transform((env) => {
const hasLabelSetId = hasValue(env.LABEL_SET_ID);
const hasLabelSetVersion = hasValue(env.LABEL_SET_VERSION);

const labelSet =
hasLabelSetId && hasLabelSetVersion
? {
labelSetId: env.LABEL_SET_ID,
labelSetVersion: env.LABEL_SET_VERSION,
}
: undefined;

return {
port: env.PORT,
dataDir: env.DATA_DIR,
dbSchemaVersion: env.DB_SCHEMA_VERSION,
labelSet,
};
});

const configInput = envToConfigSchema.parse(env);
return ENSRainbowConfigSchema.parse(configInput);
} catch (error) {
if (error instanceof ZodError) {
throw new Error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`);
}

throw error;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trim label set env values before building labelSet.
You already use trim() for presence checks, but you forward raw values. Whitespace around env values can sneak through and cause confusing downstream validation failures. Trim once and reuse.

♻️ Proposed fix
       .transform((env) => {
-        const hasLabelSetId = hasValue(env.LABEL_SET_ID);
-        const hasLabelSetVersion = hasValue(env.LABEL_SET_VERSION);
+        const labelSetId = env.LABEL_SET_ID?.trim();
+        const labelSetVersion = env.LABEL_SET_VERSION?.trim();
+        const hasLabelSetId = hasValue(labelSetId);
+        const hasLabelSetVersion = hasValue(labelSetVersion);
 
         const labelSet =
           hasLabelSetId && hasLabelSetVersion
             ? {
-                labelSetId: env.LABEL_SET_ID,
-                labelSetVersion: env.LABEL_SET_VERSION,
+                labelSetId,
+                labelSetVersion,
               }
             : undefined;
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/config/config.schema.ts` around lines 56 - 120, The
LABEL_SET_ID and LABEL_SET_VERSION values are forwarded raw in
buildConfigFromEnvironment's envToConfigSchema.transform even though presence
checks use trimmed values; update the transform in envToConfigSchema (inside
buildConfigFromEnvironment) to trim LABEL_SET_ID and LABEL_SET_VERSION before
constructing the labelSet object (e.g., compute const id =
env.LABEL_SET_ID?.trim(); const ver = env.LABEL_SET_VERSION?.trim(); and use
those when setting labelSet), so downstream validation sees cleaned values; keep
existing hasValue checks as-is and only change the transform that builds
labelSet/returns the config.

Copilot AI review requested due to automatic review settings February 3, 2026 21:46
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 3, 2026 21:46 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 3, 2026 21:46 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 3, 2026 21:46 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

apps/ensrainbow/src/config/env-config.test.ts:286

  • There are two test files with nearly identical content: env-config.test.ts (286 lines) and config.schema.test.ts (316 lines). Both files test buildConfigFromEnvironment with the same test cases. The only difference is that config.schema.test.ts also includes tests for buildENSRainbowPublicConfig. This appears to be unintentional duplication. Consider removing env-config.test.ts and keeping only config.schema.test.ts which has the more complete test coverage.
import { isAbsolute, resolve } from "node:path";

import { describe, expect, it, vi } from "vitest";

import { DB_SCHEMA_VERSION } from "@/lib/database";

import { buildConfigFromEnvironment } from "./config.schema";
import { ENSRAINBOW_DEFAULT_PORT, getDefaultDataDir } from "./defaults";
import type { ENSRainbowEnvironment } from "./environment";

vi.mock("@/utils/logger", () => ({
  logger: {
    error: vi.fn(),
  },
}));

describe("buildConfigFromEnvironment", () => {
  describe("Success cases", () => {
    it("returns a valid config with all defaults when environment is empty", () => {
      const env: ENSRainbowEnvironment = {};

      const config = buildConfigFromEnvironment(env);

      expect(config).toStrictEqual({
        port: ENSRAINBOW_DEFAULT_PORT,
        dataDir: getDefaultDataDir(),
        dbSchemaVersion: DB_SCHEMA_VERSION,
      });
    });

    it("applies custom port when PORT is set", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "5000",
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.port).toBe(5000);
      expect(config.dataDir).toBe(getDefaultDataDir());
    });

    it("applies custom DATA_DIR when set", () => {
      const customDataDir = "/var/lib/ensrainbow/data";
      const env: ENSRainbowEnvironment = {
        DATA_DIR: customDataDir,
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.dataDir).toBe(customDataDir);
    });

    it("normalizes relative DATA_DIR to absolute path", () => {
      const relativeDataDir = "my-data";
      const env: ENSRainbowEnvironment = {
        DATA_DIR: relativeDataDir,
      };

      const config = buildConfigFromEnvironment(env);

      expect(isAbsolute(config.dataDir)).toBe(true);
      expect(config.dataDir).toBe(resolve(process.cwd(), relativeDataDir));
    });

    it("resolves nested relative DATA_DIR correctly", () => {
      const relativeDataDir = "./data/ensrainbow/db";
      const env: ENSRainbowEnvironment = {
        DATA_DIR: relativeDataDir,
      };

      const config = buildConfigFromEnvironment(env);

      expect(isAbsolute(config.dataDir)).toBe(true);
      expect(config.dataDir).toBe(resolve(process.cwd(), relativeDataDir));
    });

    it("preserves absolute DATA_DIR", () => {
      const absoluteDataDir = "/absolute/path/to/data";
      const env: ENSRainbowEnvironment = {
        DATA_DIR: absoluteDataDir,
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.dataDir).toBe(absoluteDataDir);
    });

    it("applies DB_SCHEMA_VERSION when set and matches code version", () => {
      const env: ENSRainbowEnvironment = {
        DB_SCHEMA_VERSION: DB_SCHEMA_VERSION.toString(),
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.dbSchemaVersion).toBe(DB_SCHEMA_VERSION);
    });

    it("defaults DB_SCHEMA_VERSION to code version when not set", () => {
      const env: ENSRainbowEnvironment = {};

      const config = buildConfigFromEnvironment(env);

      expect(config.dbSchemaVersion).toBe(DB_SCHEMA_VERSION);
    });

    it("handles all valid configuration options together", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "4444",
        DATA_DIR: "/opt/ensrainbow/data",
        DB_SCHEMA_VERSION: DB_SCHEMA_VERSION.toString(),
      };

      const config = buildConfigFromEnvironment(env);

      expect(config).toStrictEqual({
        port: 4444,
        dataDir: "/opt/ensrainbow/data",
        dbSchemaVersion: DB_SCHEMA_VERSION,
      });
    });
  });

  describe("Validation errors", () => {
    it("fails when PORT is not a number", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "not-a-number",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when PORT is a float", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "3000.5",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when PORT is less than 1", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "0",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when PORT is negative", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "-100",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when PORT is greater than 65535", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "65536",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when DATA_DIR is empty string", () => {
      const env: ENSRainbowEnvironment = {
        DATA_DIR: "",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when DATA_DIR is only whitespace", () => {
      const env: ENSRainbowEnvironment = {
        DATA_DIR: "   ",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when DB_SCHEMA_VERSION is not a number", () => {
      const env: ENSRainbowEnvironment = {
        DB_SCHEMA_VERSION: "not-a-number",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });

    it("fails when DB_SCHEMA_VERSION is a float", () => {
      const env: ENSRainbowEnvironment = {
        DB_SCHEMA_VERSION: "3.5",
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow();
    });
  });

  describe("Invariant: DB_SCHEMA_VERSION must match code version", () => {
    it("fails when DB_SCHEMA_VERSION does not match code version", () => {
      const wrongVersion = DB_SCHEMA_VERSION + 1;
      const env: ENSRainbowEnvironment = {
        DB_SCHEMA_VERSION: wrongVersion.toString(),
      };

      expect(() => buildConfigFromEnvironment(env)).toThrow(/DB_SCHEMA_VERSION mismatch/);
    });

    it("passes when DB_SCHEMA_VERSION matches code version", () => {
      const env: ENSRainbowEnvironment = {
        DB_SCHEMA_VERSION: DB_SCHEMA_VERSION.toString(),
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.dbSchemaVersion).toBe(DB_SCHEMA_VERSION);
    });

    it("passes when DB_SCHEMA_VERSION defaults to code version", () => {
      const env: ENSRainbowEnvironment = {};

      const config = buildConfigFromEnvironment(env);

      expect(config.dbSchemaVersion).toBe(DB_SCHEMA_VERSION);
    });
  });

  describe("Edge cases", () => {
    it("handles PORT at minimum valid value (1)", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "1",
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.port).toBe(1);
    });

    it("handles PORT at maximum valid value (65535)", () => {
      const env: ENSRainbowEnvironment = {
        PORT: "65535",
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.port).toBe(65535);
    });

    it("trims whitespace from DATA_DIR", () => {
      const dataDir = "/my/path/to/data";
      const env: ENSRainbowEnvironment = {
        DATA_DIR: `  ${dataDir}  `,
      };

      const config = buildConfigFromEnvironment(env);

      expect(config.dataDir).toBe(dataDir);
    });

    it("handles DATA_DIR with .. (parent directory)", () => {
      const relativeDataDir = "../data";
      const env: ENSRainbowEnvironment = {
        DATA_DIR: relativeDataDir,
      };

      const config = buildConfigFromEnvironment(env);

      expect(isAbsolute(config.dataDir)).toBe(true);
      expect(config.dataDir).toBe(resolve(process.cwd(), relativeDataDir));
    });

    it("handles DATA_DIR with ~ (not expanded, treated as relative)", () => {
      // Note: The config schema does NOT expand ~ to home directory
      // It would be treated as a relative path
      const tildeDataDir = "~/data";
      const env: ENSRainbowEnvironment = {
        DATA_DIR: tildeDataDir,
      };

      const config = buildConfigFromEnvironment(env);

      expect(isAbsolute(config.dataDir)).toBe(true);
      // ~ is treated as a directory name, not home expansion
      expect(config.dataDir).toBe(resolve(process.cwd(), tildeDataDir));
    });
  });
});


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/ensrainbow/src/config/types.ts`:
- Around line 1-17: The import of AbsolutePathSchemaBase and
DbSchemaVersionSchemaBase is currently type-only which breaks the z.infer<typeof
...> uses because typeof requires runtime values; update the import so those two
symbols are imported as real values (remove the `type`-only import for
AbsolutePathSchemaBase and DbSchemaVersionSchemaBase) while keeping other purely
type imports (e.g., z, PortNumber) as type imports, so that AbsolutePath and
DbSchemaVersion can use z.infer<typeof AbsolutePathSchemaBase> and
z.infer<typeof DbSchemaVersionSchemaBase> successfully.

Comment on lines +1 to +17
import type { z } from "zod/v4";

import type { PortNumber } from "@ensnode/ensnode-sdk/internal";

import type { AbsolutePathSchemaBase, DbSchemaVersionSchemaBase } from "./config.schema";

/**
* Absolute filesystem path.
* Inferred from {@link AbsolutePathSchemaBase} - see that schema for invariants.
*/
export type AbsolutePath = z.infer<typeof AbsolutePathSchemaBase>;

/**
* Database schema version number.
* Inferred from {@link DbSchemaVersionSchemaBase} - see that schema for invariants.
*/
export type DbSchemaVersion = z.infer<typeof DbSchemaVersionSchemaBase>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the config.schema file
find . -name "config.schema*" -type f

Repository: namehash/ensnode

Length of output: 291


🏁 Script executed:

# Search for AbsolutePathSchemaBase and DbSchemaVersionSchemaBase definitions
rg "AbsolutePathSchemaBase|DbSchemaVersionSchemaBase" --type ts --type tsx -B 2 -A 2

Repository: namehash/ensnode

Length of output: 87


🏁 Script executed:

# Read the config.schema.ts file to see exports
cat -n ./apps/ensrainbow/src/config/config.schema.ts | head -100

Repository: namehash/ensnode

Length of output: 3345


🏁 Script executed:

# Search for the schema definitions more carefully
rg "AbsolutePathSchemaBase|DbSchemaVersionSchemaBase" -B 2 -A 2

Repository: namehash/ensnode

Length of output: 2499


🏁 Script executed:

# Check if the types.ts file has any TypeScript compilation errors
# Look for build configuration
find . -name "tsconfig.json" -o -name "tsconfig*.json" | head -5

Repository: namehash/ensnode

Length of output: 218


🏁 Script executed:

# Check if there's any build or type-check output that might show errors
# Also verify by looking at the actual import/export statements carefully
cat -n ./apps/ensrainbow/src/config/types.ts

Repository: namehash/ensnode

Length of output: 1019


Type-only import breaks z.infer type queries.

AbsolutePathSchemaBase and DbSchemaVersionSchemaBase are runtime values exported as const from config.schema.ts, but imported with import type in line 5. Using typeof on type-only imports is invalid—TypeScript will fail to compile. Import them as values instead:

Fix
-import type { AbsolutePathSchemaBase, DbSchemaVersionSchemaBase } from "./config.schema";
+import { AbsolutePathSchemaBase, DbSchemaVersionSchemaBase } from "./config.schema";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { z } from "zod/v4";
import type { PortNumber } from "@ensnode/ensnode-sdk/internal";
import type { AbsolutePathSchemaBase, DbSchemaVersionSchemaBase } from "./config.schema";
/**
* Absolute filesystem path.
* Inferred from {@link AbsolutePathSchemaBase} - see that schema for invariants.
*/
export type AbsolutePath = z.infer<typeof AbsolutePathSchemaBase>;
/**
* Database schema version number.
* Inferred from {@link DbSchemaVersionSchemaBase} - see that schema for invariants.
*/
export type DbSchemaVersion = z.infer<typeof DbSchemaVersionSchemaBase>;
import type { z } from "zod/v4";
import type { PortNumber } from "@ensnode/ensnode-sdk/internal";
import { AbsolutePathSchemaBase, DbSchemaVersionSchemaBase } from "./config.schema";
/**
* Absolute filesystem path.
* Inferred from {`@link` AbsolutePathSchemaBase} - see that schema for invariants.
*/
export type AbsolutePath = z.infer<typeof AbsolutePathSchemaBase>;
/**
* Database schema version number.
* Inferred from {`@link` DbSchemaVersionSchemaBase} - see that schema for invariants.
*/
export type DbSchemaVersion = z.infer<typeof DbSchemaVersionSchemaBase>;
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/config/types.ts` around lines 1 - 17, The import of
AbsolutePathSchemaBase and DbSchemaVersionSchemaBase is currently type-only
which breaks the z.infer<typeof ...> uses because typeof requires runtime
values; update the import so those two symbols are imported as real values
(remove the `type`-only import for AbsolutePathSchemaBase and
DbSchemaVersionSchemaBase) while keeping other purely type imports (e.g., z,
PortNumber) as type imports, so that AbsolutePath and DbSchemaVersion can use
z.infer<typeof AbsolutePathSchemaBase> and z.infer<typeof
DbSchemaVersionSchemaBase> successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build ENSRainbow config

3 participants