🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

constancy

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

constancy

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

latest
Source
npmnpm
Version
3.0.1
Version published
Weekly downloads
27
22.73%
Maintainers
1
Weekly downloads
 
Created
Source

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.

APIModelData frozen?Reference severed?Use
immutableView()VIEW — Proxy blocks mutations through this referenceNo — original still mutableNo — if you keep original ref, you can mutateWhen you want to prevent access through this specific reference
snapshot()SNAPSHOT — Clone + freeze creates true immutabilityYes — data itself is frozenYes — clone is independentWhen 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

LevelAPITypeMechanismOriginal Mutable?Use Case
0freezeShallow()FreezeShallow Object.freeze()If nestedTop-level freeze only
0deepFreeze()FreezeRecursive freeze + cached builtinsOnly with retained refFull graph immutability
1immutableView()ViewProxy trapsYes, if you keep originalRuntime mutation blocking
1.5snapshot()SnapshotClone + deep freezeNo — independent copyTrue immutability
1.5immutableMapView/SetView()ViewProxy mutator blockingIf you keep originalCollection safety
2vault()SnapshotClosure isolation + copy-on-readNo — sealedAbsolute reference isolation
2.5secureSnapshot()SnapshotNull proto + getter-only + non-configurableNo — sealedPrototype pollution defense
3tamperEvident()SnapshotVault + djb2 hash verificationNo — sealedData 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.

OperationObject sizeThroughputLatency
deepFreeze()3 keys2.2M ops/s< 1 us
snapshot()3 keys900K ops/s~1 us
immutableView()3 keys800K ops/s~1 us
tamperEvident()3 keys400K 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?

CapabilityDetails
Zero dependenciesEliminates supply chain risk. SLSA 3 provenance on every release.
Tiny footprint< 6KB minified (ESM + CJS). Fully tree-shakeable.
Type-safeReadonly<T>, DeepReadonly<T>, strict TypeScript with advanced conditional types.
7 defense levelsRanges from shallow freeze to tamper-evident vaults with hash verification.
Battle-tested228+ tests, 98% coverage, fuzz-tested with Jazzer.js.
Tamper-resistantBuiltins cached at module load to mitigate post-import prototype poisoning.
Clear mental modelExplicit VIEW vs SNAPSHOT semantics — no ambiguity in mutation guarantees.
Dual formatNative ESM + CJS via conditional exports. Broad runtime compatibility.

Compared to Alternatives

FeatureObject.freezeimmerimmutable.jsconstancy
Deep freezeNoNoN/AYes
Proxy viewsNoDrafts onlyNoYes
Clone + freezeNoNoNoYes
Vault isolationNoNoNoYes
Hash verificationNoNoNoYes
Runtime integrityNoNoNoYes
Zero dependenciesYesNoNoYes
Bundle size0KB~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

Keywords

constancy

FAQs

Package last updated on 17 Apr 2026

Did you know?

Socket

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.

Install

Related posts