
Security News
PolinRider: North Korea-Linked Supply Chain Campaign Expands Across Open Source Ecosystems
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.
Zero-dependency immutability toolkit: freeze, view, snapshot, vault, and integrity verification
The most security-hardened immutability library for JavaScript and TypeScript.
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.
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.
npm install constancy
# or
yarn add constancy
# or
pnpm add constancy
Requires Node.js >= 20. Zero dependencies. ESM + CommonJS.
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
freezeShallow<T>(val: T) — Shallow FreezeFreezes 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 FreezeRecursively 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 poisoningconst 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 });
immutableView<T>(obj: T, options?) — Proxy-Based VIEWWraps 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 ViewReturns true if value is an immutable proxy.
isImmutableView(immutableView({})); // true
isImmutableView({}); // false
isImmutableView(deepFreeze({})); // false
assertImmutableView<T>(val: T) — Assert Immutable ViewThrows if value is not an immutable proxy.
assertImmutableView(immutableView({})); // OK
assertImmutableView({}); // TypeError
immutableMapView<K, V>(map: Map<K, V>) — Read-Only MapWraps 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 SetWraps 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<T>(value: T) — Clone + Deep FreezeCreates 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 SnapshotVault 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 SnapshotStores 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
vault<T>(val: T) — Copy-on-Read VaultStores 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
isDeepFrozen<T>(val: T) — Check if Deep FrozenVerifies 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 FrozenThrows if value is not deeply frozen.
assertDeepFrozen(deepFreeze({})); // OK
assertDeepFrozen({}); // TypeError
checkRuntimeIntegrity() — Detect Post-Import TamperingVerifies that Object.freeze and other builtins haven't been overridden post-import.
const { intact, compromised } = checkRuntimeIntegrity();
if (!intact) console.error('Environment compromised:', compromised);
DeepReadonly<T>Recursively readonly type for objects, arrays, Maps, Sets.
FreezableUnion 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 }.
| 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 |
Object.freeze override → cached builtins captured at module loadsecureSnapshot(), own-property precedenceimmutableView()immutableView()secureSnapshot()vault() / tamperEvident()tamperEvident()immutableView()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');
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.
| 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. |
| 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 |
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
MIT — DungGramer
FAQs
Zero-dependency immutability toolkit: freeze, view, snapshot, vault, and integrity verification
The npm package constancy receives a total of 28 weekly downloads. As such, constancy popularity was classified as not popular.
We found that constancy demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.

Security News
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.