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

constancy

v3.0.1

Published

Zero-dependency immutability toolkit: freeze, view, snapshot, vault, and integrity verification

Readme

Constancy

The most security-hardened immutability library for JavaScript and TypeScript.

npm version npm downloads zero dependencies Node.js CI CodeQL OSV Scanner OpenSSF Best Practices Quality Gate Status Fuzz Testing SLSA 3 Socket Badge OpenSSF Scorecard codecov license

Zero-dependency, TypeScript-first immutability toolkit with multi-layered defense against tampering. Deep freeze, immutable views, snapshots, vaults, and structural hashing — all under 6KB.

Why constancy? Most "freeze" libraries stop at Object.freeze. Constancy gives you 7 levels of protection — from shallow freeze to tamper-evident vaults with hash verification — and defends against prototype pollution, builtin override attacks, and reference extraction. Supply-chain safe: zero dependencies, SLSA 3 provenance, fuzz-tested, 228+ tests at 98% coverage.

Critical Distinction: VIEW vs SNAPSHOT

This is the most important concept in constancy.

| API | Model | Data frozen? | Reference severed? | Use | |-----|-------|-------------|-------------------|----| | immutableView() | VIEW — Proxy blocks mutations through this reference | No — original still mutable | No — if you keep original ref, you can mutate | When you want to prevent access through this specific reference | | snapshot() | SNAPSHOT — Clone + freeze creates true immutability | Yes — data itself is frozen | Yes — clone is independent | When you need true data immutability |

Example:

const original = { count: 0 };

// immutableView() is a VIEW — blocks mutations through proxy, but original still mutable
const view = immutableView(original);
view.count = 1;        // TypeError: object is immutable
original.count = 1;    // Works! original is still mutable

// snapshot() is a SNAPSHOT — independent frozen clone
const snapshot = snapshot(original);
snapshot.count = 1;    // TypeError: frozen
original.count = 1;    // Doesn't affect snapshot

Choose immutableView() for: Runtime mutation prevention when you control the original reference. Choose snapshot() for: True data immutability, or when untrusted code could retain the original reference.

Installation

npm install constancy
# or
yarn add constancy
# or
pnpm add constancy

Requires Node.js >= 20. Zero dependencies. ESM + CommonJS.

Quick Start

import { freezeShallow, deepFreeze, immutableView, snapshot, vault, tamperEvident } from 'constancy';

// Freeze: Shallow freeze
const config = freezeShallow({ host: 'localhost', port: 3000 });
config.port = 8080; // Ignored (non-strict), throws in strict mode

// Freeze: Deep freeze — recursive
const state = deepFreeze({ user: { name: 'Alice', roles: ['admin'] } });
state.user.roles.push('user'); // Throws in strict mode

// View: Immutable proxy — throws, but original still mutable if retained
const data = immutableView({ count: 0, items: [] });
data.count = 1; // TypeError: object is immutable
// ⚠️ Original reference is still mutable if you kept it!

// Snapshot: snapshot() — clone + freeze, true immutability
const locked = snapshot({ count: 0, items: [] });
locked.count = 1; // TypeError: frozen
// Original unaffected, reference severed

// Isolation: Vault — closure isolation + copy-on-read
const secret = vault({ apiKey: 'sk-123456' });
const copy = secret.get(); // Fresh frozen copy each call
secret.get() === secret.get(); // false — always new copy

// Snapshot: Tamper-evident — hash verification vault
const protected = tamperEvident({ version: '1.0.0', data: [1, 2, 3] });
protected.assertIntact(); // Throws if hash mismatch
const fingerprint = protected.fingerprint; // Original hash

API Reference

Freeze (In-Place)

freezeShallow<T>(val: T) — Shallow Freeze

Freezes only top-level properties using native Object.freeze().

const obj = freezeShallow({ a: 1, b: { c: 2 } });
obj.a = 2;      // Ignored
obj.b.c = 3;    // Works — nested not frozen

deepFreeze<T>(val: T, options?) — Recursive Freeze

Recursively freezes all nested objects. Handles circular refs, Symbol keys, TypedArrays, and accessor descriptors.

Options:

  • freezePrototypeChain?: boolean (default false) — Freeze prototype chain to defend against post-freeze poisoning
const obj = deepFreeze({ nested: { count: 0 }, tags: ['a'] });
obj.nested.count = 1; // Ignored
obj.tags.push('b');   // Ignored

// Opt-in: freeze prototype chain
const hardened = deepFreeze(MyClass.prototype, { freezePrototypeChain: true });

View (Proxy, No Clone)

immutableView<T>(obj: T, options?) — Proxy-Based VIEW

Wraps object in a Proxy that throws TypeError on ANY mutation through this reference. Does NOT freeze or clone the original.

This is a VIEW, not a snapshot. Original is still mutable if you retain the reference. Use snapshot() for true immutability.

Options:

  • blockToJSON?: boolean (default false) — Prevent JSON.stringify(view) from invoking target's toJSON()
const data = immutableView({ items: [], config: { theme: 'dark' } });
data.items.push(1);              // TypeError: object is immutable
data.config.theme = 'light';     // TypeError: object is immutable

const m = immutableView(new Map([['a', 1]]), { blockToJSON: true });
m.set('b', 2);                   // TypeError: Cannot set: object is immutable
m.get('a');                      // Works — read access allowed
JSON.stringify(m);               // Blocks toJSON() invocation

isImmutableView<T>(val: any) — Check Immutable View

Returns true if value is an immutable proxy.

isImmutableView(immutableView({}));  // true
isImmutableView({});                 // false
isImmutableView(deepFreeze({}));     // false

assertImmutableView<T>(val: T) — Assert Immutable View

Throws if value is not an immutable proxy.

assertImmutableView(immutableView({}));  // OK
assertImmutableView({});                 // TypeError

immutableMapView<K, V>(map: Map<K, V>) — Read-Only Map

Wraps Map in a Proxy. All mutator methods throw.

const m = immutableMapView(new Map([['a', 1], ['b', 2]]));
m.get('a');  // 1
m.set('c', 3); // TypeError
m.delete('a'); // TypeError

immutableSetView<T>(set: Set<T>) — Read-Only Set

Wraps Set in a Proxy. All mutator methods throw.

const s = immutableSetView(new Set([1, 2, 3]));
s.has(1);    // true
s.add(4);    // TypeError
s.delete(1); // TypeError

Snapshot (Clone + Freeze)

snapshot<T>(value: T) — Clone + Deep Freeze

Creates a deep clone and recursively freezes every object. True immutability — original unaffected.

const original = { user: { isVip: false } };
const snap = snapshot(original);

original.user.isVip = true;    // original mutated
snap.user.isVip;               // false — snapshot unaffected
snap.user.isVip = true;        // TypeError — frozen

lock<T>(value: T) — Alias for snapshot()

Alternate name for snapshot(). Both are identical.

const snap = lock({ count: 0 });  // Same as snapshot()

secureSnapshot<T>(obj: T) — Hardened Snapshot

Vault with null prototype + getter-only descriptors. Max protection for critical data. Throws on accessor properties to prevent silent data loss.

const cfg = secureSnapshot({ db: { host: 'localhost', port: 5432 } });
cfg.db.host;                              // 'localhost'
cfg.db.host = 'evil';                     // TypeError (strict)
Object.defineProperty(cfg, 'db', {value: null}); // TypeError — non-configurable

// Throws if object contains accessor properties
secureSnapshot({ get x() { return 1; } }); // TypeError

tamperEvident<T>(val: T) — Hash-Verified Snapshot

Stores value in vault + computes 64-bit structural hash (djb2+sdbm). Detects any internal corruption by reaching into Map/Set/Date/RegExp internal slots.

const protected = tamperEvident({ version: '1.0', data: [1, 2, 3] });
const fingerprint = protected.fingerprint; // Original hash (base-36)

// Safe access with automatic verification
const copy = protected.get(); // Fresh frozen copy

// Detect corruption
protected.verify();           // true if intact
protected.assertIntact();     // Throws TypeError if corrupted

Isolation (Closure + Copy-on-Read)

vault<T>(val: T) — Copy-on-Read Vault

Stores a value in a sealed closure. Each get() call returns a fresh frozen copy. Reference extraction impossible.

const secret = vault({ password: 'xyz', tokens: ['token1'] });
const copy1 = secret.get();
const copy2 = secret.get();
copy1 === copy2; // false — new copy each time
copy1.tokens.push('token2'); // Copy mutated, vault unchanged
secret.get().tokens; // ['token1'] — original preserved

Verification

isDeepFrozen<T>(val: T) — Check if Deep Frozen

Verifies object and all nested objects are frozen.

const obj = deepFreeze({ nested: { count: 0 } });
isDeepFrozen(obj);                          // true
isDeepFrozen(freezeShallow({}));            // false (shallow only)
const partial = { nested: deepFreeze({}) };
isDeepFrozen(partial);                      // false (root not frozen)

assertDeepFrozen<T>(val: T) — Assert Deep Frozen

Throws if value is not deeply frozen.

assertDeepFrozen(deepFreeze({}));  // OK
assertDeepFrozen({});              // TypeError

checkRuntimeIntegrity() — Detect Post-Import Tampering

Verifies that Object.freeze and other builtins haven't been overridden post-import.

const { intact, compromised } = checkRuntimeIntegrity();
if (!intact) console.error('Environment compromised:', compromised);

Types

DeepReadonly<T>

Recursively readonly type for objects, arrays, Maps, Sets.

Freezable

Union type: object | Function.

Vault<T>

Interface for vault values: { readonly get: () => DeepReadonly<T> }.

TamperProofVault<T>

Interface for tamper-proof vaults: { readonly get, verify, assertIntact, fingerprint }.

Security: Defense Levels

| Level | API | Type | Mechanism | Original Mutable? | Use Case | |-------|-----|------|-----------|-------------------|----------| | 0 | freezeShallow() | Freeze | Shallow Object.freeze() | If nested | Top-level freeze only | | 0 | deepFreeze() | Freeze | Recursive freeze + cached builtins | Only with retained ref | Full graph immutability | | 1 | immutableView() | View | Proxy traps | Yes, if you keep original | Runtime mutation blocking | | 1.5 | snapshot() | Snapshot | Clone + deep freeze | No — independent copy | True immutability | | 1.5 | immutableMapView/SetView() | View | Proxy mutator blocking | If you keep original | Collection safety | | 2 | vault() | Snapshot | Closure isolation + copy-on-read | No — sealed | Absolute reference isolation | | 2.5 | secureSnapshot() | Snapshot | Null proto + getter-only + non-configurable | No — sealed | Prototype pollution defense | | 3 | tamperEvident() | Snapshot | Vault + djb2 hash verification | No — sealed | Data tampering detection |

Attack Vectors Defended

  • Object.freeze override → cached builtins captured at module load
  • ✅ Prototype pollution → null proto in secureSnapshot(), own-property precedence
  • ✅ Internal slot mutations → Proxy method blocking in immutableView()
  • ✅ Array mutations → blocked via Proxy in immutableView()
  • ✅ Property descriptor manipulation → non-configurable in secureSnapshot()
  • ✅ Reference extraction → vault copy isolation in vault() / tamperEvident()
  • ✅ Data tampering → hash verification in tamperEvident()
  • ✅ Silent mutations → Proxy throws in strict + non-strict in immutableView()

TypeScript Usage

import { freezeShallow, deepFreeze, immutableView, snapshot, vault, tamperEvident } from 'constancy';
import type { DeepReadonly, Vault, TamperProofVault } from 'constancy';

CommonJS:

const { freezeShallow, deepFreeze, immutableView, snapshot, vault, tamperEvident } = require('constancy');

Performance

All operations run in sub-microsecond to single-digit microsecond range. No runtime overhead from dependencies — everything is native JS.

| Operation | Object size | Throughput | Latency | |-----------|------------|------------|---------| | deepFreeze() | 3 keys | 2.2M ops/s | < 1 us | | snapshot() | 3 keys | 900K ops/s | ~1 us | | immutableView() | 3 keys | 800K ops/s | ~1 us | | tamperEvident() | 3 keys | 400K ops/s | ~2 us | | deepFreeze() | nested (3 levels) | 300K ops/s | ~3 us | | immutableView() | nested (3 levels) | 300K ops/s | ~3 us |

Benchmarked on Node.js v20, Windows 11. immutableView() is near-zero cost on creation — Proxy wrapping is lazy on property access.

Why Constancy?

| Capability | Details | |------------------------|-----------------------------------------------------------------------------| | Zero dependencies | Eliminates supply chain risk. SLSA 3 provenance on every release. | | Tiny footprint | < 6KB minified (ESM + CJS). Fully tree-shakeable. | | Type-safe | Readonly<T>, DeepReadonly<T>, strict TypeScript with advanced conditional types. | | 7 defense levels | Ranges from shallow freeze to tamper-evident vaults with hash verification. | | Battle-tested | 228+ tests, 98% coverage, fuzz-tested with Jazzer.js. | | Tamper-resistant | Builtins cached at module load to mitigate post-import prototype poisoning. | | Clear mental model | Explicit VIEW vs SNAPSHOT semantics — no ambiguity in mutation guarantees. | | Dual format | Native ESM + CJS via conditional exports. Broad runtime compatibility. |

Compared to Alternatives

| Feature | Object.freeze | immer | immutable.js | constancy | |---------|-----------------|---------|----------------|---------------| | Deep freeze | No | No | N/A | Yes | | Proxy views | No | Drafts only | No | Yes | | Clone + freeze | No | No | No | Yes | | Vault isolation | No | No | No | Yes | | Hash verification | No | No | No | Yes | | Runtime integrity | No | No | No | Yes | | Zero dependencies | Yes | No | No | Yes | | Bundle size | 0KB | ~16KB | ~63KB | < 6KB |

Development

npm run build       # ESM + CJS bundles
npm test            # Run tests (228+ tests)
npm run test:watch  # Watch mode
npm run test:coverage # Coverage report (98%+ lines)
npm run typecheck   # Type check
npm run fuzz        # Fuzz testing with Jazzer.js

License

MIT — DungGramer