惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
N
Netflix TechBlog - Medium
WordPress大学
WordPress大学
罗磊的独立博客
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
A
About on SuperTechFans
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Docker
云风的 BLOG
云风的 BLOG
Microsoft Security Blog
Microsoft Security Blog
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
J
Java Code Geeks
博客园 - 聂微东
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
美团技术团队
小众软件
小众软件
Stack Overflow Blog
Stack Overflow Blog
C
Check Point Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Secrets at Rest, Schemas at Scale — What's New in CtroEnv...
Odejobi Abiola Samuel · 2026-06-18 · via DEV Community

I accidentally logged a JWT secret to the console while debugging a test. Twice. The first time I thought "eh, it's just dev." The second time I realized the problem wasn't me — it was that my env tool treated secrets and config the same way. No distinction. No protection. Just a flat bag of strings where a console.log(env) dumped everything.

That's the gap v1.1.0 tries to close.

Runtime Secret Masking

The API is simple — add .secret() to any validator:

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

const env = defineEnv({
  DATABASE_URL: string().url(),
  JWT_SECRET: string().min(32).secret(),
})

Now env.JWT_SECRET returns "********" instead of the real value:

console.log(env.JWT_SECRET)      // "********"
console.log(env.DATABASE_URL)    // "postgresql://..." — visible as usual

// Real value when you need it:
env.meta.get("JWT_SECRET")       // "supersecretkey..."
env.meta.keys()                  // ["JWT_SECRET"]
env.meta.has("JWT_SECRET")       // true

This means console.log(env), error reports, and JSON.stringify are safe by default:

JSON.stringify(env)
// {"DATABASE_URL":"postgresql://...","JWT_SECRET":"********"}

.meta is non-enumerable — it won't show up in Object.keys(), for...in, or spreads. You have to reach for it explicitly.

Why a Proxy?

Object.freeze can't intercept reads. The ES Proxy spec prevents a get trap from returning different values for non-configurable target properties. So instead of freezing, we use a Proxy with set/deleteProperty traps. The result feels identical — you can't mutate it, but reads on secret keys get masked.

Error Messages Are Safe Too

Secret values are redacted in error output automatically:

try {
  defineEnv({ KEY: string().secret() }, { source: { KEY: "exposed" } })
} catch (e) {
  if (e instanceof CtroEnvError) {
    console.log(e.errors[0].value) // "********" — not "exposed"
  }
}

Schema Composition for Monorepos

The second feature comes from working on a monorepo ourselves. We had three packages (shared, api, worker) all needing DATABASE_URL and JWT_SECRET, but each needing their own specific vars too. Before, we copy-pasted. After, we composed:

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

// Define shared vars once
const base = defineSchema({
  DATABASE_URL: string().url(),
  REDIS_URL: string().url().optional(),
  LOG_LEVEL: string().default("info"),
})

// API service extends
const apiSchema = extendSchema(base, {
  PORT: number().port().default(4000),
  API_KEY: string().secret(),
})

// Worker extends
const workerSchema = extendSchema(base, {
  QUEUE_CONCURRENCY: number().positive().default(5),
  JOB_TIMEOUT: number().positive().default(30000),
})

const env = defineEnv(apiSchema)
env.DATABASE_URL // string — inherited
env.API_KEY      // "********" — secret + inherited

Merge Semantics

Extension keys override base keys. Dev mode (NODE_ENV=development) logs a warning on conflict so you don't accidentally shadow a shared var.

Chaining works naturally:

const stagingSchema = extendSchema(base, { /* overrides */ })
const prodSchema = extendSchema(stagingSchema, { /* overrides */ })

Real Example

We shipped a monorepo example with the repo:

monorepo/
  .env                          # Root env file
  packages/
    shared/src/index.ts         # defineSchema({ DATABASE_URL, REDIS_URL, LOG_LEVEL })
    api/src/env.ts              # extendSchema(base, { PORT, API_KEY })
    api/src/index.ts            # defineEnv(apiSchema)
    worker/src/env.ts           # extendSchema(base, { QUEUE_CONCURRENCY, JOB_TIMEOUT })
    worker/src/index.ts         # defineEnv(workerSchema)

Each package loads env from root via loadEnv({ path: "../.." }). One .env file, three schemas.

AGENTS.md

CtroEnv now ships an AGENTS.md at the repo root. It covers the API, chain order rules, error handling, CLI commands, and anti-patterns. Any AI agent that lands on the repo reads it automatically — opencode, Cursor, Copilot, Claude Code, Cline.

There's also a skill file at .opencode/skills/ctroenv/SKILL.md with worked examples.

What Else Changed

Package Version What
@ctroenv/core 1.1.0 Secret masking, schema composition, EnvMeta API
@ctroenv/cli 1.1.0 Config-level secrets masking
@ctroenv/{node,vite,nextjs,shared} 1.0.2 README + LICENSE polish

No breaking changes. Existing schemas keep working.

npm install @ctroenv/core@^1.1.0 @ctroenv/cli@^1.1.0

The v1.2.0 roadmap: a zodToCtroEnv bridge, branded types (Email, URL), and deeper Next.js RSC integration.

Try string().secret() and see if it catches anything you'd rather keep quiet.