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

@fernclark/four-word-recovery

v0.1.0

Published

Four-word evocative recovery phrase generation, validation, and constant-time comparison for anonymous-first identity systems.

Readme

@fernclark/four-word-recovery

Four-word evocative recovery phrase generation, validation, and constant-time comparison for anonymous-first identity systems.

Built for miracles.live — a daily contemplative practice that is anonymous by default and never asks for an email address.

Why four words?

256^4 ≈ 4.3 billion combinations (32 bits of entropy). Combined with per-IP throttling on the claim endpoint, this is well above the practical brute-force ceiling for a low-stakes account-recovery surface.

The wordlist is deliberately tone-aligned — words like amber, forest, tide, and solace — so the phrase feels like part of a practice rather than a random password. No homophones. No offensive words. All unique, 4–7 letter lowercase.

Installation

npm install @fernclark/four-word-recovery

Usage

import {
  generateRecoveryPhrase,
  normalizePhrase,
  isValidPhrase,
  newSalt,
  hashPhrase,
  safeHashEqual,
} from "@fernclark/four-word-recovery";

// Generate a phrase
const phrase = generateRecoveryPhrase();
// => "amber-forest-stone-tide"

// Normalize user input (accepts spaces, commas, or dashes)
const canonical = normalizePhrase("Amber Forest Stone Tide");
// => "amber-forest-stone-tide"

// Validate before storing
if (!isValidPhrase(canonical)) throw new Error("Invalid phrase");

// Hash for storage (never store the canonical phrase)
const salt = newSalt();
const hash = hashPhrase(canonical, salt);
// Store { salt, hash } — never { canonical }

// Verify on claim
const match = safeHashEqual(hash, hashPhrase(normalizePhrase(userInput), salt));

API

generateRecoveryPhrase(): string

Generates a random four-word phrase using crypto.randomInt (no modulo bias). Returns words joined by -.

normalizePhrase(raw: string): string

Normalizes user input: lowercase, replaces spaces/commas with dashes, collapses repeated separators, trims. Call before isValidPhrase and hashPhrase.

isValidPhrase(canonical: string): boolean

Validates a normalized phrase: exactly four words, every word present in the wordlist.

newSalt(): string

Returns a 16-byte cryptographically random hex string for use as a hash salt.

hashPhrase(canonical: string, salt: string): string

SHA-256 hash of salt:canonical. Store this alongside the salt; never store the canonical phrase.

safeHashEqual(a: string, b: string): boolean

Constant-time comparison of two hex hash strings. Prevents timing oracle attacks.

RECOVERY_WORDLIST: readonly string[]

The 256-word wordlist, exported for inspection or extension.

Security notes

  • Always throttle the claim endpoint by IP (12 attempts / hour is a reasonable ceiling).
  • Never log or return the canonical phrase after it has been shown to the user once.
  • The hash is SHA-256 with a random salt — not bcrypt or argon2 — because the entropy in the wordlist (32 bits) is the rate-limiting factor, not the hash speed.
  • safeHashEqual uses timingSafeEqual to prevent timing side-channels.

License

MIT — FeRn Clark / miracles.live