Skip to content

Conversation

@djstrong
Copy link
Contributor

@djstrong djstrong commented Dec 22, 2025

Related to #1407

@vercel
Copy link

vercel bot commented Dec 22, 2025

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

3 Skipped Deployments
Project Deployment Review Updated (UTC)
admin.ensnode.io Skipped Skipped Jan 23, 2026 3:29pm
ensnode.io Skipped Skipped Jan 23, 2026 3:29pm
ensrainbow.io Skipped Skipped Jan 23, 2026 3:29pm

@changeset-bot
Copy link

changeset-bot bot commented Dec 22, 2025

🦋 Changeset detected

Latest commit: e3a6c90

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/ensnode-sdk Patch
@ensnode/ensrainbow-sdk Patch
ensadmin Patch
ensapi Patch
ensindexer Patch
fallback-ensapi Patch
@namehash/ens-referrals Patch
@ensnode/ensnode-react Patch
@namehash/namehash-ui Patch
@ensnode/ponder-metadata Patch
@ensnode/datasources Patch
@ensnode/ensnode-schema Patch
@ensnode/ponder-subgraph Patch
@ensnode/shared-configs Patch
@docs/ensnode Patch
@docs/ensrainbow Patch
@docs/mintlify 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

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Centralizes ENSRainbow configuration with Zod schemas and defaults, validates environment-to-config mapping (including DB schema invariant), moves CLI defaults to the new config, tightens port validation, adds a GET /v1/config endpoint and SDK client method, and expands tests for config and CLI isolation.

Changes

Cohort / File(s) Summary
Config schema & validation
apps/ensrainbow/src/config/config.schema.ts, apps/ensrainbow/src/config/validations.ts, apps/ensrainbow/src/config/environment.ts
Add Zod-backed ENSRainbowConfig schema, raw ENSRainbowEnvironment type, buildConfigFromEnvironment, buildENSRainbowPublicConfig, and invariant invariant_dbSchemaVersionMatch enforcing DB schema version.
Defaults & config surface
apps/ensrainbow/src/config/defaults.ts, apps/ensrainbow/src/config/index.ts, apps/ensrainbow/src/config/types.ts
Introduce ENSRAINBOW_DEFAULT_PORT and getDefaultDataDir(), re-export types/builders, and initialize default runtime config from process.env (logs and exits on parse error).
CLI & tests
apps/ensrainbow/src/cli.ts, apps/ensrainbow/src/cli.test.ts
CLI now sources defaults from central config/getDefaultDataDir(); port validation only enforced when PORT is set. Tests updated for module isolation, fresh imports, env stubbing, and async flows.
Env helper simplification
apps/ensrainbow/src/lib/env.ts
Remove DEFAULT_PORT and getDefaultDataSubDir; getEnvPort() now returns config.port (parsing/validation moved to config schema).
API endpoint
apps/ensrainbow/src/lib/api.ts
Add GET /v1/config endpoint: fetches server label set/count and returns public config via buildENSRainbowPublicConfig (500 on retrieval errors).
SDK client addition
packages/ensrainbow-sdk/src/client.ts
Add config(): Promise<ENSRainbowPublicConfig> to client, define ENSRainbowPublicConfig type, implement endpoint call and validation; deprecate older version endpoint guidance.
Shared schema tightening
packages/ensnode-sdk/src/shared/config/zod-schemas.ts
Strengthen PortSchema to require integer ports (.int()), adjust min/max error messages to inclusive bounds.
Config tests
apps/ensrainbow/src/config/config.schema.test.ts
Add comprehensive tests covering defaults, normalization, validation errors, invariants, edge cases, and process.exit behavior (logger/process.exit mocked).
Dependency update
apps/ensrainbow/package.json
Add zod dependency to ENSRainbow package.
Misc tests & changeset
apps/ensrainbow/src/cli.test.ts, .changeset/young-carrots-cheer.md
CLI tests adjusted for isolation; changeset added documenting patch bumps.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI as "CLI"
  participant ConfigBuilder as "buildConfigFromEnvironment"
  participant Zod as "Zod schema"
  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/validate)
  else invalid
    ConfigBuilder->>Process: log prettified errors
    Process->>Process: process.exit(1)
  end
Loading
sequenceDiagram
  autonumber
  participant SDK as "SDK Client"
  participant Server as "ENSRainbow API"
  participant DB as "Label store / DB"
  participant Builder as "buildENSRainbowPublicConfig"

  SDK->>Server: GET /v1/config
  Server->>DB: fetch server label set & count
  DB-->>Server: labelSet, count
  Server->>Builder: assemble public config (config, labelSet, count)
  Builder-->>Server: ENSRainbowPublicConfig
  Server-->>SDK: 200 + ENSRainbowPublicConfig
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

ensnode-sdk

Poem

🐰 I nibbled env vars, trimmed stray spaces wide,

Zod stitched defaults and kept the rules inside,
Ports now count proper, endpoints hum in tune,
Tests wake fresh each run beneath the moon,
A rabbit's happy hop — config snug and plied 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete and does not follow the required template. It only contains a single issue reference without the required Summary, Why, Testing, Notes, or Pre-Review Checklist sections. Complete the PR description by adding all template sections: Summary (1-3 bullets of changes), Why (rationale and issue link), Testing (test approach), Notes for Reviewer (if applicable), and Pre-Review Checklist with required acknowledgments.
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Build ENSRainbow config' accurately reflects the primary objective of this PR, which introduces a comprehensive configuration system for ENSRainbow including schema validation, environment mapping, and a new config endpoint.

✏️ 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

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).

Comment on lines +33 to +36
const ENSRainbowConfigSchema = z
.object({
port: PortSchema.default(ENSRAINBOW_DEFAULT_PORT),
dataDir: DataDirSchema.default(getDefaultDataDir()),
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Default data directory is evaluated at module load time.

getDefaultDataDir() is called once when the module loads, capturing process.cwd() at that moment. If the working directory changes before buildConfigFromEnvironment is called, the default will be stale.

Consider using a getter function for lazy evaluation:

♻️ Suggested lazy evaluation
 const ENSRainbowConfigSchema = z
   .object({
     port: PortSchema.default(ENSRAINBOW_DEFAULT_PORT),
-    dataDir: DataDirSchema.default(getDefaultDataDir()),
+    dataDir: DataDirSchema.optional(),
     dbSchemaVersion: DbSchemaVersionSchema,
     labelSet: LabelSetSchema.optional(),
   })

Then handle the default in buildConfigFromEnvironment:

return ENSRainbowConfigSchema.parse({
  port: env.PORT,
  dataDir: env.DATA_DIR ?? getDefaultDataDir(),
  // ...
});
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/config/config.schema.ts` around lines 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.

Comment on lines 73 to 83
} catch (error) {
if (error instanceof ZodError) {
logger.error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`);
} else if (error instanceof Error) {
logger.error(error, `Failed to build ENSRainbowConfig`);
} else {
logger.error(`Unknown Error`);
}

process.exit(1);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

process.exit(1) prevents testability and graceful error handling.

Calling process.exit(1) terminates the process immediately, making this function difficult to test and preventing callers from handling errors gracefully. Consider throwing a custom error and letting the caller decide how to handle it.

🔧 Suggested refactor for better error handling
+export class ConfigurationError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = "ConfigurationError";
+  }
+}
+
 export function buildConfigFromEnvironment(env: ENSRainbowEnvironment): ENSRainbowConfig {
   try {
     return ENSRainbowConfigSchema.parse({
       // ... parsing logic
     });
   } catch (error) {
     if (error instanceof ZodError) {
       logger.error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`);
+      throw new ConfigurationError(`Invalid configuration: ${prettifyError(error)}`);
     } else if (error instanceof Error) {
       logger.error(error, `Failed to build ENSRainbowConfig`);
+      throw error;
     } else {
       logger.error(`Unknown Error`);
+      throw new ConfigurationError("Unknown configuration error");
     }
-
-    process.exit(1);
   }
 }

Then handle the exit at the call site (e.g., in CLI entry points):

try {
  const config = buildConfigFromEnvironment(process.env as ENSRainbowEnvironment);
} catch (error) {
  process.exit(1);
}
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/config/config.schema.ts` around lines 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.

Comment on lines 7 to 10
export function getEnvPort(): number {
const envPort = process.env.PORT;
if (!envPort) {
return DEFAULT_PORT;
}

try {
const port = parseNonNegativeInteger(envPort);
return port;
} catch (_error: unknown) {
const errorMessage = `Invalid PORT value "${envPort}": must be a non-negative integer`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
const config = buildConfigFromEnvironment(process.env as ENSRainbowEnvironment);
return config.port;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unsafe type assertion may mask configuration errors.

Casting process.env as ENSRainbowEnvironment bypasses TypeScript's type checking. While buildConfigFromEnvironment validates at runtime, the assertion could hide type mismatches during development.

Additionally, this function rebuilds the entire configuration on each call. If called frequently, consider memoizing or caching the result.

♻️ Suggested improvement
+let cachedConfig: ENSRainbowConfig | null = null;
+
 /**
  * Gets the port from environment variables.
  */
 export function getEnvPort(): number {
-  const config = buildConfigFromEnvironment(process.env as ENSRainbowEnvironment);
-  return config.port;
+  if (!cachedConfig) {
+    cachedConfig = buildConfigFromEnvironment(process.env as ENSRainbowEnvironment);
+  }
+  return cachedConfig.port;
 }

You'll also need to import the config type:

import type { ENSRainbowConfig } from "@/config/config.schema";
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/lib/env.ts` around lines 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.

Comment on lines 20 to 23
if (config.dbSchemaVersion !== undefined && config.dbSchemaVersion !== DB_SCHEMA_VERSION) {
throw new Error(
`DB_SCHEMA_VERSION mismatch! Expected version ${DB_SCHEMA_VERSION} from code, but found ${config.dbSchemaVersion} in environment variables.`,
);
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

This .check() invariant throws an Error directly. In most config invariants in this repo, the check adds a custom issue to ctx.issues so the failure is reported via ZodError and formatted by prettifyError (see apps/ensapi/src/config/validations.ts:18-24). Consider pushing a custom issue (with path: ["dbSchemaVersion"]) instead of throwing, so users get consistent, nicely formatted config errors.

Copilot uses AI. Check for mistakes.
@@ -1,24 +1,10 @@
import { join } from "node:path";
import { buildConfigFromEnvironment } from "@/config/config.schema";
Copy link

@vercel vercel bot Jan 21, 2026

Choose a reason for hiding this comment

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

The refactored getEnvPort() function now calls process.exit(1) on validation failures instead of throwing an error, which breaks error handling in code paths that expect a catchable error. This causes the application to exit unexpectedly when validatePortConfiguration() is called with an invalid PORT environment variable.

View Details
📝 Patch Details
diff --git a/apps/ensrainbow/src/cli.test.ts b/apps/ensrainbow/src/cli.test.ts
index a8d674a4..b0159c00 100644
--- a/apps/ensrainbow/src/cli.test.ts
+++ b/apps/ensrainbow/src/cli.test.ts
@@ -51,20 +51,12 @@ describe("CLI", () => {
 
     it("should throw error for invalid port number", () => {
       process.env.PORT = "invalid";
-      const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
-        throw new Error("process.exit called");
-      }) as never);
       expect(() => getEnvPort()).toThrow();
-      expect(exitSpy).toHaveBeenCalledWith(1);
     });
 
     it("should throw error for negative port number", () => {
       process.env.PORT = "-1";
-      const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
-        throw new Error("process.exit called");
-      }) as never);
       expect(() => getEnvPort()).toThrow();
-      expect(exitSpy).toHaveBeenCalledWith(1);
     });
   });
 
diff --git a/apps/ensrainbow/src/config/config.schema.ts b/apps/ensrainbow/src/config/config.schema.ts
index e74f166c..6e079dc5 100644
--- a/apps/ensrainbow/src/config/config.schema.ts
+++ b/apps/ensrainbow/src/config/config.schema.ts
@@ -71,14 +71,15 @@ export function buildConfigFromEnvironment(env: ENSRainbowEnvironment): ENSRainb
           : undefined,
     });
   } catch (error) {
+    let errorMessage = `Failed to parse environment configuration`;
+
     if (error instanceof ZodError) {
-      logger.error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`);
+      errorMessage = `Failed to parse environment configuration: \n${prettifyError(error)}\n`;
     } else if (error instanceof Error) {
-      logger.error(error, `Failed to build ENSRainbowConfig`);
-    } else {
-      logger.error(`Unknown Error`);
+      errorMessage = error.message;
     }
 
-    process.exit(1);
+    logger.error(errorMessage);
+    throw new Error(errorMessage);
   }
 }

Analysis

Invalid PORT environment variable prevents CLI port override

What fails: The refactored getEnvPort() function calls buildConfigFromEnvironment() which invokes process.exit(1) on validation failure. When an invalid PORT environment variable is set, the process exits immediately during CLI setup (via yargs default: option evaluation), preventing users from overriding it with --port CLI flag.

How to reproduce:

PORT=invalid node cli.ts serve --port 5000

The process exits with validation error instead of using the provided --port 5000.

Expected vs Actual:

  • Expected: Accept the --port 5000 CLI argument and use that port, either ignoring or properly validating the invalid PORT env var
  • Actual: Process exits immediately during yargs option setup before any command validation occurs

Root cause: In commit c34c0bd, getEnvPort() was refactored to call buildConfigFromEnvironment(), which catches validation errors and calls process.exit(1) instead of throwing a catchable error. The old implementation (commit b20b546) threw Error on validation failure, allowing calling code to catch and handle the error.

Fix applied: Changed buildConfigFromEnvironment() to throw Error instead of calling process.exit(1), allowing validation errors to propagate and be caught by calling code in validatePortConfiguration() and yargs error handlers.

@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
Copilot AI review requested due to automatic review settings January 23, 2026 14:07
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 23, 2026 14:08 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 23, 2026 14:08 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 23, 2026 14:08 Inactive
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: 2

🤖 Fix all issues with AI agents
In `@apps/ensrainbow/src/config/config.schema.ts`:
- Around line 98-107: The parameter `config: ENSRainbowConfig` on the function
`buildENSRainbowPublicConfig` is unused; either remove it from the function
signature and update any callers that pass it, or rename it to `_config` to
signal intentional omission and satisfy linters; adjust the declaration of
`buildENSRainbowPublicConfig` accordingly and ensure imports/types referencing
`ENSRainbowConfig` remain consistent.

In `@apps/ensrainbow/src/lib/api.ts`:
- Around line 106-118: The /v1/config handler always returns HTTP 500 on
labelCount failures; change it to preserve the upstream error code from
server.labelCount(): when countResult.status === StatusCode.Error, call
c.json(...) with the HTTP status set to countResult.errorCode (falling back to
500 if errorCode is undefined) so the response aligns with /v1/labels/count;
refer to api.get("/v1/config", async (c: HonoContext)), server.labelCount(),
countResult.status, countResult.errorCode and c.json when making the change.
♻️ Duplicate comments (3)
apps/ensrainbow/src/config/config.schema.ts (3)

37-41: Avoid eager getDefaultDataDir() evaluation at module load.
This captures process.cwd() too early; prefer lazy evaluation at build time.

♻️ Suggested refactor
 const ENSRainbowConfigSchema = z
   .object({
     port: PortSchema.default(ENSRAINBOW_DEFAULT_PORT),
-    dataDir: DataDirSchema.default(getDefaultDataDir()),
+    dataDir: DataDirSchema.optional(),
     dbSchemaVersion: DbSchemaVersionSchema,
     labelSet: LabelSetSchema.optional(),
   })
     return ENSRainbowConfigSchema.parse({
       port: env.PORT,
-      dataDir: env.DATA_DIR,
+      dataDir: env.DATA_DIR ?? getDefaultDataDir(),
       dbSchemaVersion: env.DB_SCHEMA_VERSION,
       labelSet:
         env.LABEL_SET_ID || env.LABEL_SET_VERSION
           ? {

69-75: Treat empty label-set env vars as provided values.
Using a truthy check skips validation when LABEL_SET_ID="". Switch to explicit !== undefined checks.

🔧 Suggested fix
-      labelSet:
-        env.LABEL_SET_ID || env.LABEL_SET_VERSION
+      labelSet:
+        env.LABEL_SET_ID !== undefined || env.LABEL_SET_VERSION !== undefined
           ? {
               labelSetId: env.LABEL_SET_ID,
               labelSetVersion: env.LABEL_SET_VERSION,
             }
           : undefined,

77-86: Avoid process.exit(1) in library code paths.
This makes callers/tests harder to handle; prefer throwing a typed error and let the entrypoint decide.

🔧 Suggested refactor
-    process.exit(1);
+    throw error instanceof Error ? error : new Error("Unknown configuration error");

Comment on lines +98 to +107
export function buildENSRainbowPublicConfig(
config: ENSRainbowConfig,
labelSet: EnsRainbowServerLabelSet,
recordsCount: number,
): EnsRainbow.ENSRainbowPublicConfig {
return {
version: packageJson.version,
labelSet,
recordsCount,
};
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unused config parameter in buildENSRainbowPublicConfig.
If it’s intentionally unused, prefix with _ or remove to avoid confusion/lint noise.

♻️ Suggested tweak
-export function buildENSRainbowPublicConfig(
-  config: ENSRainbowConfig,
+export function buildENSRainbowPublicConfig(
+  _config: ENSRainbowConfig,
   labelSet: EnsRainbowServerLabelSet,
   recordsCount: number,
): EnsRainbow.ENSRainbowPublicConfig {
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/config/config.schema.ts` around lines 98 - 107, The
parameter `config: ENSRainbowConfig` on the function
`buildENSRainbowPublicConfig` is unused; either remove it from the function
signature and update any callers that pass it, or rename it to `_config` to
signal intentional omission and satisfy linters; adjust the declaration of
`buildENSRainbowPublicConfig` accordingly and ensure imports/types referencing
`ENSRainbowConfig` remain consistent.

Comment on lines +106 to +118
api.get("/v1/config", async (c: HonoContext) => {
logger.debug("Config request");
const countResult = await server.labelCount();
if (countResult.status === StatusCode.Error) {
logger.error("Failed to get records count for config endpoint");
return c.json(
{
status: StatusCode.Error,
error: countResult.error,
errorCode: countResult.errorCode,
},
500,
);
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

Preserve upstream error code for config failures.
Right now the handler always returns HTTP 500, even if countResult.errorCode is more specific. Align with /v1/labels/count by returning the upstream code.

🔧 Suggested fix
-        500,
+        countResult.errorCode ?? 500,
📝 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
api.get("/v1/config", async (c: HonoContext) => {
logger.debug("Config request");
const countResult = await server.labelCount();
if (countResult.status === StatusCode.Error) {
logger.error("Failed to get records count for config endpoint");
return c.json(
{
status: StatusCode.Error,
error: countResult.error,
errorCode: countResult.errorCode,
},
500,
);
api.get("/v1/config", async (c: HonoContext) => {
logger.debug("Config request");
const countResult = await server.labelCount();
if (countResult.status === StatusCode.Error) {
logger.error("Failed to get records count for config endpoint");
return c.json(
{
status: StatusCode.Error,
error: countResult.error,
errorCode: countResult.errorCode,
},
countResult.errorCode ?? 500,
);
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/lib/api.ts` around lines 106 - 118, The /v1/config
handler always returns HTTP 500 on labelCount failures; change it to preserve
the upstream error code from server.labelCount(): when countResult.status ===
StatusCode.Error, call c.json(...) with the HTTP status set to
countResult.errorCode (falling back to 500 if errorCode is undefined) so the
response aligns with /v1/labels/count; refer to api.get("/v1/config", async (c:
HonoContext)), server.labelCount(), countResult.status, countResult.errorCode
and c.json when making the change.

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 14 out of 15 changed files in this pull request and generated 2 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.

Comment on lines +98 to +108
export function buildENSRainbowPublicConfig(
config: ENSRainbowConfig,
labelSet: EnsRainbowServerLabelSet,
recordsCount: number,
): EnsRainbow.ENSRainbowPublicConfig {
return {
version: packageJson.version,
labelSet,
recordsCount,
};
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The config parameter in buildENSRainbowPublicConfig is declared but never used in the function body. The function only uses packageJson.version, labelSet, and recordsCount parameters. Either remove the unused config parameter or use it if there's a future need for it. Based on the similar pattern in ENSApi (buildEnsApiPublicConfig), the config parameter is used to access configuration values. Consider whether any config values should be included in the public config.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +108
export function buildENSRainbowPublicConfig(
config: ENSRainbowConfig,
labelSet: EnsRainbowServerLabelSet,
recordsCount: number,
): EnsRainbow.ENSRainbowPublicConfig {
return {
version: packageJson.version,
labelSet,
recordsCount,
};
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The buildENSRainbowPublicConfig function is not tested. Consider adding tests to verify that it correctly builds the public config from the provided parameters (config, labelSet, recordsCount). This would ensure the function works as expected and maintains consistency with similar functions in ENSApi and ENSIndexer.

Copilot uses AI. Check for mistakes.
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.test.ts`:
- Around line 3-28: The test suite currently replaces process.exit with a spy
(mockExit) but never restores it, which can leak into other tests; add an
afterAll hook that calls mockExit.mockRestore() to restore the original
process.exit. Locate the mock created with vi.spyOn(process, "exit") assigned to
mockExit in this file and add an afterAll(() => mockExit.mockRestore()) to
ensure the spy is removed after the suite finishes.

Comment on lines 3 to 28
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

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

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", () => {
// Mock process.exit to prevent actual exit
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);

beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
mockExit.mockClear();
});
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

Restore the process.exit spy after the suite completes.

The spy is never restored, so other test files in the same worker can inherit the mocked process.exit and mask real exits. Add an afterAll restore.

🧹 Proposed fix
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@
   afterEach(() => {
     mockExit.mockClear();
   });
+
+  afterAll(() => {
+    mockExit.mockRestore();
+  });
📝 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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DB_SCHEMA_VERSION } from "@/lib/database";
import { logger } from "@/utils/logger";
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", () => {
// Mock process.exit to prevent actual exit
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
mockExit.mockClear();
});
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DB_SCHEMA_VERSION } from "@/lib/database";
import { logger } from "@/utils/logger";
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", () => {
// Mock process.exit to prevent actual exit
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
mockExit.mockClear();
});
afterAll(() => {
mockExit.mockRestore();
});
🤖 Prompt for AI Agents
In `@apps/ensrainbow/src/config/config.schema.test.ts` around lines 3 - 28, The
test suite currently replaces process.exit with a spy (mockExit) but never
restores it, which can leak into other tests; add an afterAll hook that calls
mockExit.mockRestore() to restore the original process.exit. Locate the mock
created with vi.spyOn(process, "exit") assigned to mockExit in this file and add
an afterAll(() => mockExit.mockRestore()) to ensure the spy is removed after the
suite finishes.

@vercel vercel bot temporarily deployed to Preview – ensnode.io January 23, 2026 15:00 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 23, 2026 15:00 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 23, 2026 15:00 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 23, 2026 15:29 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 23, 2026 15:29 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 23, 2026 15:29 Inactive
@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/namehash/ensnode/issues/comments/3778117110","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> \n> \n> ```ascii\n>  _______________________________________________________________\n> < Making your code shine like the top of the Chrysler Building. >\n>  ---------------------------------------------------------------\n>   \\\n>    \\   \\\n>         \\ /\\\n>         ( )\n>       .( o ).\n> ```\n> \n> <sub>✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.</sub>\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- other_code_reviewer_warning_start -->\n\n> [!NOTE]\n> ## Other AI code review bot(s) detected\n> \n> CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.\n\n<!-- other_code_reviewer_warning_end -->\n\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nCentralizes ENSRainbow configuration with Zod schemas and defaults, validates environment-to-config mapping (including DB schema invariant), moves CLI defaults to the new config, tightens port validation, adds a GET /v1/config endpoint and SDK client method, and expands tests for config and CLI isolation.\n\n## Changes\n\n| Cohort / File(s) | Summary |\n|---|---|\n| **Config schema & validation** <br> `apps/ensrainbow/src/config/config.schema.ts`, `apps/ensrainbow/src/config/validations.ts`, `apps/ensrainbow/src/config/environment.ts` | Add Zod-backed `ENSRainbowConfig` schema, raw env type `ENSRainbowEnvironment`, `buildConfigFromEnvironment`, `buildENSRainbowPublicConfig`, and invariant `invariant_dbSchemaVersionMatch` validating DB schema version. |\n| **Defaults & config surface** <br> `apps/ensrainbow/src/config/defaults.ts`, `apps/ensrainbow/src/config/index.ts`, `apps/ensrainbow/src/config/types.ts` | Introduce `ENSRAINBOW_DEFAULT_PORT` and `getDefaultDataDir()`, re-export types/builders, and initialize default runtime config from `process.env` (exits on config parse error). |\n| **CLI & tests** <br> `apps/ensrainbow/src/cli.ts`, `apps/ensrainbow/src/cli.test.ts` | CLI now sources defaults from central `config`/`getDefaultDataDir()`; port validation only enforced when `PORT` is set. Tests updated for module isolation, fresh imports, env stubbing, and async flows. |\n| **Env helper simplification** <br> `apps/ensrainbow/src/lib/env.ts` | Remove `DEFAULT_PORT` and `getDefaultDataSubDir`; `getEnvPort()` now returns `config.port` (parsing/validation moved to config schema). |\n| **API endpoint** <br> `apps/ensrainbow/src/lib/api.ts` | Add GET `/v1/config` endpoint: fetches server label set/count and returns public config via `buildENSRainbowPublicConfig` (500 on retrieval errors). |\n| **SDK client addition** <br> `packages/ensrainbow-sdk/src/client.ts` | Add `config(): Promise<ENSRainbowPublicConfig>` to client, define `ENSRainbowPublicConfig` type, implement endpoint call and validation; deprecate older version endpoint guidance. |\n| **Shared schema tightening** <br> `packages/ensnode-sdk/src/shared/config/zod-schemas.ts` | Strengthen `PortSchema` to require integer ports (`.int()`), adjust min/max error messages to inclusive bounds. |\n| **Config tests** <br> `apps/ensrainbow/src/config/config.schema.test.ts` | Add comprehensive tests covering defaults, normalization, validation errors, invariants, edge cases, and process.exit behavior (logger/process.exit mocked). |\n| **Dependency update** <br> `apps/ensrainbow/package.json` | Add `zod` dependency to ENSRainbow package. |\n| **Misc tests & changeset** <br> `apps/ensrainbow/src/cli.test.ts`, `.changeset/young-carrots-cheer.md` | CLI tests adjusted for isolation; changeset added documenting patch bumps. |\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n  autonumber\n  participant CLI as \"CLI\"\n  participant ConfigBuilder as \"buildConfigFromEnvironment\"\n  participant Zod as \"Zod schema\"\n  participant Process as \"process\"\n\n  CLI->>ConfigBuilder: request config from env\n  ConfigBuilder->>Zod: parse & validate environment -> ENSRainbowConfig\n  Zod-->>ConfigBuilder: validation result\n  alt valid\n    ConfigBuilder-->>CLI: return ENSRainbowConfig\n    CLI->>CLI: continue startup (serve/ingest/validate)\n  else invalid\n    ConfigBuilder->>Process: log prettified errors\n    Process->>Process: process.exit(1)\n  end\n```\n\n```mermaid\nsequenceDiagram\n  autonumber\n  participant SDK as \"SDK Client\"\n  participant Server as \"ENSRainbow API\"\n  participant DB as \"Label store / DB\"\n  participant Builder as \"buildENSRainbowPublicConfig\"\n\n  SDK->>Server: GET /v1/config\n  Server->>DB: fetch server label set & count\n  DB-->>Server: labelSet, count\n  Server->>Builder: assemble public config (config, labelSet, count)\n  Builder-->>Server: ENSRainbowPublicConfig\n  Server-->>SDK: 200 + ENSRainbowPublicConfig\n```\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~45 minutes\n\n## Suggested labels\n\n`ensnode-sdk`\n\n## Poem\n\n> 🐰 I hopped through env vars, trimmed each stray,  \n> > Zod stitched defaults and chased the noise away,  \n> > Ports now count proper, endpoints sing anew,  \n> > Tests wake fresh each run — imports reborn true,  \n> > A rabbit's nibble — configs snug and blue 🥕\n\n</details>\n\n<!-- walkthrough_end -->\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 1 | ❌ 2</summary>\n\n<details>\n<summary>❌ Failed checks (2 warnings)</summary>\n\n|     Check name     | Status     | Explanation                                                                                                                                          | Resolution                                                                                                                                             |\n| :----------------: | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |\n|  Description check | ⚠️ Warning | The pull request description is largely incomplete, missing the required Summary, Why, Testing, and Pre-Review Checklist sections from the template. | Complete the description using the template: add Summary (1-3 bullets), Why section with link to `#1407`, Testing details, and the Pre-Review Checklist. |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%.                                                                | Write docstrings for the functions missing them to satisfy the coverage threshold.                                                                     |\n\n</details>\n<details>\n<summary>✅ Passed checks (1 passed)</summary>\n\n|  Check name | Status   | Explanation                                                                                                                                                                        |\n| :---------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Title check | ✅ Passed | The title 'Build ENSRainbow config' directly describes the main objective of this changeset, which introduces a comprehensive configuration system for the ENSRainbow application. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `1407-build-ensrainbow-config`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/namehash/ensnode/issues/comments/3778117110","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Fri, 23 Jan 2026 15:30:07 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"B03B:396F35:C9D287:38FA775:697393FE","x-xss-protection":"0"},"data":{"message":"Requires authentication","documentation_url":"https://docs.github.com/rest","status":"401"}}}

Comment on lines +97 to +98
labelSetId: env.LABEL_SET_ID,
labelSetVersion: env.LABEL_SET_VERSION,
Copy link

Choose a reason for hiding this comment

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

Suggested change
labelSetId: env.LABEL_SET_ID,
labelSetVersion: env.LABEL_SET_VERSION,
labelSetId: env.LABEL_SET_ID!.trim(),
labelSetVersion: env.LABEL_SET_VERSION!.trim(),

The LABEL_SET_ID and LABEL_SET_VERSION values are not trimmed before being returned from the transform, even though the code checks that the trimmed values are non-empty. This will cause validation to fail if either value has leading or trailing whitespace.

View Details

Analysis

Untrimmed LABEL_SET_ID and LABEL_SET_VERSION values fail regex validation

What fails: The buildConfigFromEnvironment() function validates that trimmed LABEL_SET_ID and LABEL_SET_VERSION are non-empty, but then passes the untrimmed values to LabelSetSchema. Since makeLabelSetIdSchema validates against /^[a-z-]+$/, strings with leading/trailing whitespace fail validation despite passing the trim check.

How to reproduce:

const env = {
  LABEL_SET_ID: " subgraph ",
  LABEL_SET_VERSION: "0"
};
buildConfigFromEnvironment(env); // Throws ZodError with "LABEL_SET_ID can only contain lowercase letters (a-z) and hyphens (-)"

Result: Validation fails with regex format error: code: "invalid_format", pattern: "/^[a-z-]+$/"

Expected: Should accept the environment variables and return config with trimmed values: {labelSetId: "subgraph", labelSetVersion: 0}

Fix: Added .trim() calls when constructing the labelSet object to match the trimmed values already validated earlier in the transform function (lines 97-98 in apps/ensrainbow/src/config/config.schema.ts). This aligns with how DATA_DIR is handled in the schema.

@djstrong djstrong marked this pull request as ready for review January 23, 2026 15:46
@djstrong djstrong requested a review from a team as a code owner January 23, 2026 15:46
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 23, 2026

Greptile Overview

Greptile Summary

This PR implements a comprehensive configuration system for ENSRainbow using Zod validation, addressing issue #1407.

Key Changes:

  • Created new /config module with schema validation, type safety, and environment variable parsing
  • Added new GET /v1/config API endpoint exposing version, label set info, and record count
  • Deprecated GET /v1/version endpoint in favor of the more comprehensive config endpoint
  • Enhanced PortSchema validation to enforce integer-only ports (rejecting floats)
  • Refactored CLI and tests to use centralized config instead of scattered defaults
  • Added 364 lines of comprehensive test coverage for config validation

Architecture:

  • Config is built and validated at module load time from process.env, exiting immediately on errors
  • Centralized defaults (ENSRAINBOW_DEFAULT_PORT, getDefaultDataDir()) prevent inconsistencies
  • Zod schemas enforce strict validation with custom invariants (e.g., DB schema version matching)
  • Label set configuration requires both ID and version to be set together

Testing:

  • Tests properly isolate environment variables using vi.stubEnv() and vi.resetModules()
  • Comprehensive edge cases covered: boundary values, path resolution, whitespace handling

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Excellent implementation with comprehensive test coverage (364 lines), proper error handling, and good architectural patterns. The config system follows best practices with Zod validation, centralized defaults, and proper module isolation in tests. All changes are backward compatible with deprecation warnings.
  • No files require special attention

Important Files Changed

Filename Overview
apps/ensrainbow/src/config/config.schema.ts Implemented comprehensive config schema with Zod validation, environment parsing, and public config builder
apps/ensrainbow/src/config/config.schema.test.ts Added comprehensive test coverage for config schema validation with 364 lines of tests
apps/ensrainbow/src/config/index.ts Module entry point that builds and exports config from process.env at module load time, exits on error
apps/ensrainbow/src/cli.ts Updated to use centralized config module for port and data directory defaults
apps/ensrainbow/src/lib/api.ts Added new /v1/config endpoint with public configuration data, deprecated /v1/version
packages/ensrainbow-sdk/src/client.ts Added config() method to fetch ENSRainbowPublicConfig, deprecated version() method

Sequence Diagram

sequenceDiagram
    participant ENV as Environment Variables
    participant CI as config/index.ts
    participant CS as config/config.schema.ts
    participant ZOD as Zod Validation
    participant CLI as cli.ts
    participant API as lib/api.ts
    participant Client as ensrainbow-sdk

    Note over ENV,CI: Module Load Time (Startup)
    CI->>CS: buildConfigFromEnvironment(process.env)
    CS->>ZOD: Parse PORT, DATA_DIR, DB_SCHEMA_VERSION, LABEL_SET_*
    ZOD->>ZOD: Validate schema & invariants
    alt Validation Success
        ZOD-->>CS: Validated ENSRainbowConfig
        CS-->>CI: config object
        CI->>CI: export default config
    else Validation Error
        ZOD-->>CS: ZodError / Error
        CS-->>CI: throw error
        CI->>CI: console.error + process.exit(1)
    end

    Note over CLI,API: Runtime (Server/CLI Operations)
    CLI->>CI: import config
    CLI->>CLI: Use config.port for defaults
    CLI->>API: serverCommand(port, dataDir)
    
    Note over API,Client: New Config Endpoint
    Client->>API: GET /v1/config
    API->>API: server.labelCount()
    API->>CS: buildENSRainbowPublicConfig(config, labelSet, count)
    CS-->>API: ENSRainbowPublicConfig
    API-->>Client: { version, labelSet, recordsCount }
    
    Note over API,Client: Deprecated Version Endpoint
    Client->>API: GET /v1/version (deprecated)
    API-->>Client: { status, versionInfo }
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

config = buildConfigFromEnvironment(process.env as ENSRainbowEnvironment);
} catch (error) {
// For CLI applications, invalid configuration should exit the process
console.error("Configuration error:", error instanceof Error ? error.message : String(error));
Copy link
Contributor

Choose a reason for hiding this comment

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

console.error doesn't output structured error details from Zod validation. Consider using logger.error and formatting ZodError for better debugging

Suggested change
console.error("Configuration error:", error instanceof Error ? error.message : String(error));
logger.error("Configuration error:", error instanceof ZodError ? error.format() : error);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ensrainbow/src/config/index.ts
Line: 15:15

Comment:
`console.error` doesn't output structured error details from Zod validation. Consider using `logger.error` and formatting ZodError for better debugging

```suggestion
  logger.error("Configuration error:", error instanceof ZodError ? error.format() : error);
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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