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

invisiblejs

v2.0.0

Published

Embed hidden UTF-8 strings inside any text using invisible Unicode characters. Two codecs: variation-selector (Invisible) and zero-width 2-bit packed (ZeroWidth).

Readme

invisiblejs

Embed hidden UTF-8 strings inside any text using invisible Unicode characters.

invisiblejs provides two steganography codecs that hide data in plain sight using different invisible Unicode character strategies:

| Codec | Characters | Density | Best For | |---|---|---|---| | Invisible | Variation Selectors (U+E0100–U+E01FF) | 1 char/byte | Maximum compatibility | | ZeroWidth | Zero-width chars (U+200B, U+200C, U+200D, U+2060) | 4 chars/byte | Platforms that strip variation selectors |

Both codecs share the same API, use XOR obfuscation with a seed-derived keystream, and include a MAGIC signature for integrity verification.

Features

  • 🔤 Invisible — payload characters are zero-width and visually silent
  • 🔑 Seed-keyed — different seeds produce different, incompatible encodings
  • Integrity check — MAGIC signature detects wrong-seed decodes immediately
  • 🌍 Full UTF-8 support — encode any Unicode string including emoji
  • 📦 Zero dependencies — pure TypeScript, no runtime deps
  • 🔄 Two codecs — choose between variation-selector and zero-width encoding

Installation

npm install invisiblejs

Quick Start

Invisible (Variation Selectors)

import { Invisible } from 'invisiblejs'

const iv = new Invisible('my-secret-seed')

// Encode a secret message into invisible characters
const hidden = iv.encode('top secret')

// Embed them into ordinary visible text
const post = `Check out this tweet! ${hidden} #invisible`

// Later — extract and decode
const result = iv.decodeFrom(post)
if ('ok' in result) {
  console.log(result.ok) // → "top secret"
}

ZeroWidth (Zero-Width Characters)

import { ZeroWidth } from 'invisiblejs'

const zw = new ZeroWidth('my-secret-seed')

// Same API — encode into zero-width characters
const hidden = zw.encode('top secret')

// Embed into visible text
const post = `Nothing to see here ${hidden} just a normal message`

// Extract and decode
const result = zw.decodeFrom(post)
if ('ok' in result) {
  console.log(result.ok) // → "top secret"
}

API

Both Invisible and ZeroWidth share an identical API:

new Invisible(seed?: string) / new ZeroWidth(seed?: string)

Creates a new instance. The seed string determines the XOR keystream and the MAGIC signature embedded at the start of every payload.

  • Default seed: 'default-seed'
  • Two instances sharing the same seed can encode/decode each other's output.
  • Instances with different seeds cannot decode each other's output (returns invalid_signature).
  • Invisible and ZeroWidth use different character sets and are not interchangeable, even with the same seed.

encode(input: string): string

Encodes input into a sequence of invisible Unicode characters.

const invisible = iv.encode('hello')
// Invisible:          string of chars in U+E0100–U+E01FF range
// ZeroWidth: string of U+200B, U+200C, U+200D, U+2060

decode(input: string): DecodeResult

Decodes a string that consists entirely of invisible characters (as produced by encode()).

const result = iv.decode(invisible)
if ('ok' in result) {
  console.log(result.ok) // decoded string
} else {
  console.error(result.error) // error code
}

extract(input: string): string | null

Extracts the first invisible payload from a mixed string (visible + invisible characters). Only payloads produced by an instance with the same seed will be found.

Returns null if no matching payload is found.

const mixed = `Normal text ${iv.encode('secret')} more text`
const payload = iv.extract(mixed) // → invisible string

decodeFrom(input: string): DecodeResult

Convenience method — calls extract() then decode() in one step.

const result = iv.decodeFrom(mixed)
// { ok: 'secret' }  or  { error: 'invisible_not_found' }

Error Types

decode() and decodeFrom() return a discriminated union:

type DecodeResult =
  | { ok: string }
  | { error:
      | 'invisible_not_found'        // decodeFrom: no payload in the string
      | 'invalid_invisible_character' // input contains unexpected characters
      | 'invalid_signature'           // MAGIC mismatch — wrong seed or corrupt data
      | 'invalid_utf8'                // payload bytes are not valid UTF-8
    }

How It Works

Invisible (Variation Selectors)

  1. Seed hashing — the seed is hashed with FNV-1a 32-bit into a 4-byte keystream.
  2. Payload constructionMAGIC (4 bytes) || UTF-8(input).
  3. XOR obfuscation — each byte is XORed with a rotating keystream byte.
  4. Mapping — obfuscated byte b → code point U+E0100 + b (range 0–255 → U+E0100–U+E01FF).
  5. Extraction — the encoded MAGIC prefix is used as a regex anchor.

ZeroWidth (Zero-Width 2-Bit Packing)

  1. Seed hashing — same FNV-1a 32-bit → 4-byte keystream.
  2. Payload constructionMAGIC (4 bytes) || UTF-8(input).
  3. XOR obfuscation — each byte is XORed with a rotating keystream byte.
  4. 2-bit packing — each byte is split into four 2-bit groups (MSB-first), mapped to zero-width characters:
    • 00U+200B (Zero Width Space)
    • 01U+200C (Zero Width Non-Joiner)
    • 10U+200D (Zero Width Joiner)
    • 11U+2060 (Word Joiner)
  5. Delimiter wrapping — payloads are wrapped in a unique 8-character delimiter sequence for reliable extraction from mixed text.
  6. Extraction — delimiter + seed-specific MAGIC prefix used as regex anchor.

Note: U+FEFF (BOM) is deliberately avoided — many systems strip it when it appears at the start of text.

Choosing a Codec

| Consideration | Invisible | ZeroWidth | |---|---|---| | Output size | 1 char per byte (most compact) | 4 chars per byte + delimiters | | Character range | Supplementary plane (U+E0100+) | BMP zero-width (U+200x) | | Platform support | Most environments | Better on platforms that strip variation selectors | | Natural occurrence | Variation selectors are rare in text | Zero-width chars appear in emoji/Arabic/Indic text | | Extraction | Regex on unique codepoint range | Delimiter-based boundary markers |

License

MIT