npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ctroenv/core

v1.5.0

Published

Schema engine for CtroEnv — define, validate, and infer types for environment variables

Readme

@ctroenv/core

npm version npm downloads bundle size license

Define, validate, and infer types for environment variables. Zero runtime dependencies — 4 KB gzip.

Features

  • Zero dependencies — no runtime deps, not even as peer dependencies
  • Full TypeScript inference — exact literal types inferred from your schema
  • Rich error messages — grouped by category with actionable suggestions
  • Chainable APIstring().url().min(1).optional()
  • Secret masking — sensitive values masked in logs and console output
  • Schema composition — reusable schemas with defineSchema / extendSchema
  • Custom validators — build your own with createValidator / applyChain
  • Framework adapters — Node, Vite, Next.js (separate packages)
  • CLI tooling — validate, generate, check, docs, init (separate package)

Installation

npm install @ctroenv/core

Quick Start

import { defineEnv, string, number, pick } from "@ctroenv/core"

const env = defineEnv({
  DATABASE_URL: string().url().describe("Primary database connection"),
  PORT: number().port().default(3000),
  NODE_ENV: pick(["development", "staging", "production"]),
  JWT_SECRET: string().secret().min(32),
})

// TypeScript infers exact types:
env.DATABASE_URL // string
env.PORT         // number
env.NODE_ENV     // "development" | "staging" | "production"

Validators

| Validator | Type | Refinements | |-----------|------|-------------| | string() | string | .url(), .email(), .port(), .hostname(), .min(), .max(), .regex() | | number() | number | .int(), .positive(), .port(), .min(), .max() | | boolean() | boolean | Coerces true/false, "true"/"false", "yes"/"no", "on"/"off", 1/0, "y"/"n", "t"/"f" | | pick([...]) | union literal | Enum validation from a string list | | semver() | string | Strict semver (no ranges, no v prefix) | | ip() | string | Accepts IPv4 or IPv6 | | ipv4() | string | Strict IPv4 | | ipv6() | string | Strict IPv6 (rejects zone indices like %eth0) | | uuid() | string | RFC 9562 UUID (any version, lowercase) | | guid() | string | Permissive GUID (case-insensitive, with/without braces) |

Chainable Methods

Every validator supports: .optional(), .default(v), .describe(text), .secret(), .validate(fn)

Chain order matters: Validator-specific refinements (.url(), .min(), .email(), etc.) must come before .secret(), .optional(), or .describe(). Reason: .secret() returns a generic wrapper that loses type-specific methods.

// ✅ Correct
string().url().min(1).secret()

// ❌ Wrong — .min() won't exist
string().secret().url().min(1)

API

defineEnv(schema, opts?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | source | EnvSource \| Record<string, string \| undefined> | Auto-detected | Where to read env values from | | prefix | string | — | Prepended to key lookups in the source | | maskWith | string | "********" | Custom mask string for secret values |

const env = defineEnv(
  { DATABASE_URL: string().url() },
  { source: loadEnv(), prefix: "MY_APP_", maskWith: "***" },
)

watchEnv(schema, opts?)

Re-validates environment variables when the source changes. Useful for long-running processes (servers, CLIs, watchers).

| Option | Type | Default | Description | |--------|------|---------|-------------| | source | EnvSource | Auto-detected | Env source to watch | | pollInterval | number | 2000 | Poll interval in ms | | onChange | (key, oldVal, nextVal) => void | — | Called when a value changes | | signal | AbortSignal | — | Stop watching on abort |

import { watchEnv, string, number } from "@ctroenv/core"

const env = watchEnv({
  DATABASE_URL: string().url(),
  PORT: number().port().default(3000),
}, {
  onChange: (key, old, next) => console.log(`${key} changed`),
})

// env refreshes automatically when source changes

Source Functions

| Function | Description | |----------|-------------| | detectSource() | Auto-detect source (process.env, import.meta.env, etc.) | | objectSource(obj) | Create an EnvSource from a plain object | | workersSource(env) | Create an EnvSource from a Cloudflare Workers env binding |

Schema Composition

import { defineSchema, extendSchema } from "@ctroenv/core"

const base = defineSchema({
  DATABASE_URL: string().url(),
  PORT: number().port().default(3000),
})

const schema = extendSchema(base, {
  JWT_SECRET: string().secret(),
})

const env = defineEnv(schema)

Custom Validators

import { createValidator, applyChain, parseOk, singleError, errInvalid, errType } from "@ctroenv/core"

function semver() {
  const base = createValidator<string>(
    (input, ctx) =>
      typeof input !== "string"
        ? singleError(errType(ctx.key, typeof input, "semver"))
        : /^\d+\.\d+\.\d+$/.test(input)
          ? parseOk(input)
          : singleError(errInvalid(ctx.key, input, "not valid semver")),
    { typeLabel: "semver" },
  )
  return applyChain(base)
}

Error Handling

| Class / Function | Description | |-----------------|-------------| | CtroEnvError | Error class containing all validation errors | | formatErrors(errors) | Format errors for CLI output (grouped, colorized) | | ValidationError | Single validation error: { key, message, code, value?, originalValue?, suggestion? } | | ErrorCode | Enum: missing_required, type_mismatch, invalid_value, validation_failed |

import { CtroEnvError, formatErrors } from "@ctroenv/core"

try {
  const env = defineEnv(schema)
} catch (e) {
  if (e instanceof CtroEnvError) {
    console.error(formatErrors(e.errors))
  }
}

Types

| Type | Description | |------|-------------| | SchemaDefinition | Object mapping keys to validators | | Validator<T> | Generic validator type with metadata | | EnvResult<T> | Read-only env object with inferred types | | EnvMeta | Metadata accessor: .get(), .has(), .keys(), .toJSON() | | EnvSource | Source interface: { get(key: string): string \| undefined } | | ClientServerSchema | { client: SchemaDefinition, server: SchemaDefinition } | | InferredClientServerEnv<T> | Merged inferred type from client/server schemas | | DefineEnvOptions | Options for defineEnv() | | ParseResult | { success: true, value: T, errors: [] } \| { success: false, errors: ValidationError[] } | | ParseContext | Context passed to validator parse functions |

Documentation

Full documentation at ctroenv.vercel.app

License

MIT