@hyperfrontend/immutable-api-utils§

A shelf of built-ins, Object.keys and JSON.parse, from which copies drop into a sealed vault at start-up; a third-party analytics.js card slides in and rewrites both shelf tiles; two questions are then asked of the shelf and of the vault, the shelf answering wrongly in red and the vault correctly in greenA shelf of built-ins, Object.keys and JSON.parse, from which copies drop into a sealed vault at start-up; a third-party analytics.js card slides in and rewrites both shelf tiles; two questions are then asked of the shelf and of the vault, the shelf answering wrongly in red and the vault correctly in green

Decorators and utilities for creating immutable, tamper-proof object APIs with built-in prototype pollution defense.

What is @hyperfrontend/immutable-api-utils?

@hyperfrontend/immutable-api-utils provides low-level utilities for locking object properties and methods to prevent modification. Using JavaScript's property descriptors (Object.defineProperty), it creates truly immutable APIs where neither values nor method bindings can be altered after definition.

The library offers three approaches: a TypeScript decorator (@locked()) for class methods, a functional API (lockedProps()) for bulk property locking, and descriptor builders (lockedPropertyDescriptors()) for granular control. All utilities enforce non-writable, non-configurable descriptors while maintaining correct this binding through per-instance caching.

Additionally, the library provides safe built-in copies: pre-captured references to JavaScript built-in methods that mitigate prototype pollution attacks when loaded early.

Key Features

  • @locked() decoratorfor TypeScript classes

    prevents method overwriting and ensures correct this binding

  • Bulk property locking

    via lockedProps() for multiple properties in one call

  • Property descriptor creation

    with lockedPropertyDescriptors() for custom locking patterns

  • Safe built-in copiesvia secondary entrypoints

    captured at module load time before any pollution can occur

  • Per-instance binding cache

    to avoid repeated .bind() calls

  • Zero runtime dependencies

    pure JavaScript property descriptor manipulation

Why Use @hyperfrontend/immutable-api-utils?

Prevents Accidental API Tampering in Shared Contexts

When exposing objects to third-party code, plugin systems, or sandboxed environments, you need guarantees that critical methods won't be overwritten. @locked() makes methods truly immutable: attempting reassignment throws a TypeError. This protects public APIs from accidental or malicious modification.

Eliminates this Binding Bugs Without Arrow Functions

Arrow functions in class fields break inheritance and bloat bundle sizes due to per-instance function creation. The @locked() decorator provides correct this binding (like arrow functions) while using efficient prototype methods. Methods are bound once per instance and cached with Symbol keys.

Simplifies Immutable Object Construction

Object.freeze() is all or nothing: every own property goes read-only, including the ones you meant to keep mutable. lockedProps() locks only the properties you name, non-writable and non-configurable, so they cannot be reassigned, redefined or deleted while the rest of the object stays ordinary. It is shallow, like Object.freeze(): a locked property holding an object still hands out an object you can mutate.

TypeScript-First with Runtime Enforcement

Unlike TypeScript readonly (compile-time only), these utilities enforce immutability at runtime. This catches bugs in JavaScript-land, during deserialization, or when interfacing with dynamically typed code. Type safety and runtime safety in one decorator.

Installation

npm install @hyperfrontend/immutable-api-utils

Quick Start

import { locked, lockedProps, lockedPropertyDescriptors } from '@hyperfrontend/immutable-api-utils'

// Decorator usage: lock methods in classes
class Counter {
  private count = 0

  @locked()
  increment() {
    this.count++
    return this.count
  }

  @locked()
  getValue() {
    return this.count
  }
}

const counter = new Counter()
counter.increment() // Works: 1
counter.increment = () => 0 // Throws: Cannot overwrite locked method

// Ensure correct `this` binding even when method is extracted
const { increment } = counter
increment() // Still works correctly, `this` remains bound

// Functional API: lock multiple properties
const config = {}
lockedProps(config, [
  ['apiKey', 'secret-key-12345'],
  ['timeout', 5000],
  ['retries', 3],
])

config.apiKey = 'hacked' // Silent fail in non-strict mode, throws in strict mode
// Throws: cannot redefine
Object.defineProperty(config, 'apiKey', { writable: true })

// Low-level descriptor creation
const obj = {}
Object.defineProperty(obj, 'version', lockedPropertyDescriptors('1.0.0', true))
// Property is non-writable, non-configurable, but enumerable

API Overview

Two jobs share one mechanism, the property descriptor. One half locks the properties of an object you own; the other half captures the built-ins before anything else can swap them. Every module under built-in-copy/ reads its global exactly once, while it is being evaluated, and re-exports what it found as a named binding: built-in-copy/object reads globalThis.Object, so its keys is the function value Object.keys held at that instant, and a later write to Object.keys lands on the global while the binding goes on answering correctly. parse does the same for JSON.parse.

The limit is worth stating plainly: this mitigates rather than prevents, and import order decides whether it works at all. A module graph evaluates in source order, so a copy is honest only when its import sits above the code that tampers. Put the untrusted import first and the capture reads an already poisoned global, returning exactly the wrong answer the global would. Treat these as entry-point imports:

// these two lines have to be evaluated first
import { keys, hasOwn } from '@hyperfrontend/immutable-api-utils/built-in-copy/object'
import { parse } from '@hyperfrontend/immutable-api-utils/built-in-copy/json'

import './vendor/analytics.js' // whatever this does to Object.keys, keys() is unaffected

There are 24 of these subpaths, each named after the global or family of globals it copies, so the one you want is spelled like the thing you were reaching for: built-in-copy/timers holds setTimeout and queueMicrotask, built-in-copy/console holds log and warn. Members that need a receiver come through as wrappers (hasOwn applies the captured Object.prototype.hasOwnProperty through a captured Reflect.apply), and constructors as create* factories such as createMap. The package root re-exports frozen namespace objects for many of these globals, which reads nicely but pulls in the whole namespace; named bindings from a subpath are what keep the rest out of your bundle.

Three entries do the locking, and they stack. lockedPropertyDescriptors is the primitive: hand it a value and it returns { value, writable: false, configurable: false, enumerable } for you to pass to Object.defineProperty. lockedProps applies that across a list, taking the target object and an array of [key, value] pairs, and returns nothing: it mutates the object you handed it. locked is the class-method decorator and the odd one out, installing an accessor whose getter binds the method to the instance once and caches it under a symbol, and whose setter throws a TypeError.

Every export, on every subpath, is in the API reference.

Use Cases

  • Plugin APIs: Prevent plugins from modifying core library methods
  • Sandboxed execution: Expose safe APIs to untrusted code
  • Configuration objects: Lock critical config values after initialization
  • Public library interfaces: Protect exported classes from mutation
  • Event emitters: Prevent handler list manipulation
  • Prototype pollution mitigation: Safe built-in copies reduce attack surface when loaded early
  • Secure logging: Use safe console copies to prevent tampered log output
  • Safe timers: Prevent timer functions from being hijacked
  • Cross-origin messaging: Secure postMessage wrappers with captured references

Compatibility

Runs on

  • Node.js>=18.0.0supported
  • Browserssupported
  • Web Workerssupported

built-in-copy/websocket loads without a WebSocket global (Node.js before 22); only construction throws there.

Ships as

  • ESMTree-shakeable
  • CJSNode and older bundlers
  • IIFEHyperfrontendImmutableApiUtils
  • UMDHyperfrontendImmutableApiUtils

CDN Usage

<!-- unpkg -->
<script src="https://unpkg.com/@hyperfrontend/immutable-api-utils"></script>

<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@hyperfrontend/immutable-api-utils"></script>

<script>
  const { locked, lockedProps } = HyperfrontendImmutableApiUtils
  const { lockedPropertyDescriptors } = HyperfrontendImmutableApiUtils
</script>

Global variable: HyperfrontendImmutableApiUtils

Dependencies

None: zero external dependencies.

Architecture Highlights§

A locked property is defined writable: false and configurable: false, so it cannot be reassigned, deleted, or redefined afterwards. Reassignment fails silently in sloppy mode and throws a TypeError in strict mode, which includes every ES module.

The safe built-in copies are captured at module initialization time. Important: This only works if the module loads before any malicious code runs; it mitigates pollution, not prevents it retroactively.

API Reference§

View:
Organized by entry point

Module Structure

|

28 modules · 288 total exports

Tamper-proof copies of the JavaScript built-ins, and the decorators and helpers that lock an object's own properties and descriptors.

1 fn3 type19 var

Safe copies of Array built-in static methods. These references are captured at module initialization time to protect against prototype pollution attacks. Import only what you need for tree-shaking.

4 var

Safe copies of Console built-in methods. These references are captured at module initialization time to protect against prototype pollution attacks. Import only what you need for tree-shaking.

19 var

Safe copies of Date built-in via factory function and static methods.

1 fn4 var

Safe copies of encoding built-ins for TextEncoder, TextDecoder, atob, and btoa.

4 fn1 var

Safe copies of the Error built-ins, captured at load so later prototype pollution cannot reach them. Constructors arrive as factory functions, which is the only way to capture a constructor safely.

8 fn1 var

Safe copies of Function constructor and prototype methods.

1 fn5 var

Safe copies of JSON built-in methods. These references are captured at module initialization time to protect against prototype pollution attacks. Import only what you need for tree-shaking.

3 var

Safe Map factory with optional groupBy for ES2024+.

1 fn2 var

Safe copies of Math built-in methods. These references are captured at module initialization time to protect against prototype pollution attacks. Import only what you need for tree-shaking.

44 var

Safe copies of the messaging built-ins, captured at load so later prototype pollution cannot reach them: factories for MessageChannel and BroadcastChannel, whose instances carry a safe `postMessage`.

4 fn1 int4 var

Safe copies of the Number built-in methods and constants, captured at load so later prototype pollution cannot reach them.

17 var

Safe copies of Object built-in methods. These references are captured at module initialization time to protect against prototype pollution attacks. Import only what you need for tree-shaking.

2 fn22 var

Safe Promise factory and bound static methods.

1 fn8 var

Safe copies of Reflect built-in methods. These references are captured at module initialization time to protect against prototype pollution attacks. Import only what you need for tree-shaking.

14 var

Safe RegExp factory for protected regex construction.

1 fn1 var

Safe Set factory for protected set construction.

1 fn1 var

Safe copies of String built-in static methods. These references are captured at module initialization time to protect against prototype pollution attacks. Import only what you need for tree-shaking.

4 var

Safe Symbol factory and well-known symbols.

1 fn16 var

Safe copies of the timer and scheduling built-ins, captured at load so later prototype pollution cannot reach them.

5 fn3 var

Safe TypedArray and ArrayBuffer factories for protected array construction.

14 fn24 var

Safe factories for the URL built-ins, URL and URLSearchParams, captured at load so later prototype pollution cannot reach them.

2 fn5 var

Safe WeakMap factory for protected weak map construction.

1 fn1 var

Safe WeakSet factory for protected weak set construction.

1 fn1 var

Safe factory for the WebSocket constructor, captured at load so later prototype pollution cannot reach it.

1 fn5 var

Decorator for binding methods with immutable this context.

1 fn1 type

Non-writable, non-configurable property descriptor utilities.

1 type1 var

Decorator for locking object properties to prevent modification.

1 type1 var

Browse guides filtered to this packageSuggest a guide