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

@nice-code/util

v0.63.0

Published

Readme

@nice-code/util

Docs: nicecode.io — guides, integrations, and the full API surface. Working with an AI assistant? Point it at nicecode.io/llms-util.txt (just this package) or nicecode.io/llms.txt (the whole stack) — the complete, current docs flattened into plain text.

Typed storage adapters (browser, Cloudflare Durable Objects, in-memory) and WebCrypto utilities (Ed25519 signing, X25519 key exchange, AES-GCM encryption).

Install

bun add @nice-code/util

Typed storage

ITypedStorage<T> gives you fully typed, async, key-prefixed storage over any backend.

import { createTypedWebLocalStorage } from "@nice-code/util";

interface IAppStorage {
  user_id: string;
  theme: "light" | "dark";
  recent_searches: string[];
}

const storage = createTypedWebLocalStorage<IAppStorage>({
  localStorage,
  keyPrefix: "app:",
});

// All keys autocomplete; values are typed
await storage.setJson("theme", "dark");
const theme = await storage.getJson("theme"); // "light" | "dark" | undefined
const userId = await storage.getJsonOrDef("user_id", "guest"); // string

// Read-modify-write in one call
await storage.updateJsonWithDef("recent_searches", [], (cur) => [...cur, "query"]);

await storage.removeItem("theme");
await storage.clearAll(); // removes only keys this storage has written

Adapters

import {
  createTypedWebLocalStorage,
  createTypedWebSessionStorage,
  createDurableObjectTypedStorage,
  createTypedMemoryStorage_string,
  createTypedMemoryStorage_json,
} from "@nice-code/util";

// Browser
const local = createTypedWebLocalStorage<IAppStorage>({ localStorage, keyPrefix: "app:" });
const session = createTypedWebSessionStorage<IAppStorage>({ sessionStorage });

// Cloudflare Durable Objects (inside a DO class)
const doStorage = createDurableObjectTypedStorage<IDOStorage>({
  durableObjectStorage: ctx.storage,
  keyPrefix: "do:",
});

// In-memory (testing / SSR) — string-serialized or JSON-native
const mem = createTypedMemoryStorage_string<IAppStorage>();
const memJson = createTypedMemoryStorage_json<IAppStorage>();

// Share state between instances by passing the same Map
const shared = new Map<string, string>();
const a = createTypedMemoryStorage_string<IAppStorage>({ memoryStorageMap: shared });
const b = createTypedMemoryStorage_string<IAppStorage>({ memoryStorageMap: shared });

ITypedStorage<T> interface

interface ITypedStorage<T extends Record<string, any>> {
  getJson<K>(key: K): Promise<T[K] | undefined>;
  getJsonOrDef<K>(key: K, defVal: T[K]): Promise<T[K]>;
  setJson<K>(key: K, val: T[K]): Promise<void>;
  updateJson<K>(key: K, updater: (cur: T[K] | undefined) => T[K]): Promise<void>;
  updateJsonWithDef<K>(key: K, defVal: T[K], updater: (cur: T[K]) => T[K]): Promise<void>;
  removeItem<K>(key: K): Promise<void>;
  clearAll(): Promise<void>;
}

Custom backend

Implement the methods interface to wrap any storage (Redis, KV, …):

import {
  createTypedStorage,
  EStorageAdapterType,
  StorageAdapter,
  type IStorageAdapterMethods_String,
} from "@nice-code/util";

const redisMethods: IStorageAdapterMethods_String = {
  type: EStorageAdapterType.string,
  getItem: async (key) => redis.get(key),
  setItem: async (key, value) => { await redis.set(key, value); },
  removeItem: async (key) => { await redis.del(key); },
};

const storage = createTypedStorage<IMySchema>({
  storageAdapter: new StorageAdapter({ methods: redisMethods, keyPrefix: "app:" }),
});

The lower-level StorageAdapter can also be used directly (untyped keys), including createJsonGetterSetter<T>(key) for a single-key { get, set } pair.


Crypto

WebCrypto-based helpers — work in browsers, Workers / Durable Objects, Bun, and Node.

Ed25519 — sign & verify

import {
  generateEd25519KeyPair,
  importEd25519Key,
  serializeEd25519Key_Raw,
  signTextDataWithKeyEd25519,
  verifyWithKeyEd25519,
} from "@nice-code/util";
import { base64 } from "@scure/base";

const keyPair = await generateEd25519KeyPair();

// Sign
const signature = await signTextDataWithKeyEd25519("challenge-text", keyPair.privateKey);
const signatureBase64 = base64.encode(signature);

// Serialize the public key for transport — "ed25519::raw_base64::<data>"
const { prefixed } = await serializeEd25519Key_Raw(keyPair.publicKey);

// Other side: import + verify
const publicKey = await importEd25519Key.public.fromFormattedString.extractable(prefixed);
const isValid = await verifyWithKeyEd25519({
  challenge: "challenge-text",
  signatureBase64,
  publicKey,
});

Keys serialize to self-describing prefixed strings (<algo>::<format>::<data>), so a stored or transported key always knows how to re-import itself. Private keys serialize via serializeEd25519Key_Jwk / serializeX25519Key_Jwk; public keys via the _Raw variants.

X25519 + AES-GCM — shared-key encryption

Derive a shared AES-GCM key from two X25519 key pairs (ECDH + HKDF), then encrypt/decrypt:

import {
  generateX25519KeyPair,
  createAesGcmKeyFromX25519Keys,
  encryptTextDataWithAesGcmKey,
  decryptTextDataWithAesGcmKey,
} from "@nice-code/util";

const alice = await generateX25519KeyPair();
const bob = await generateX25519KeyPair();

// Both sides derive the same key from their private + the other's public key
const aliceKey = await createAesGcmKeyFromX25519Keys({
  internalX25519PrivateKey: alice.privateKey,
  externalX25519PublicKey: bob.publicKey,
  saltString: "optional-session-salt",
  infoString: "optional-context",
});
const bobKey = await createAesGcmKeyFromX25519Keys({
  internalX25519PrivateKey: bob.privateKey,
  externalX25519PublicKey: alice.publicKey,
  saltString: "optional-session-salt",
  infoString: "optional-context",
});

const payload = await encryptTextDataWithAesGcmKey({
  aesGcmKey: aliceKey,
  dataToEncrypt: "secret message",
}); // { nonce, ciphertext } — both base64

const plaintext = await decryptTextDataWithAesGcmKey({
  aesGcmKey: bobKey,
  dataToDecrypt: payload,
});

ClientCryptoKeyLink — full client-to-client crypto

High-level class managing a local identity (Ed25519 verify pair + X25519 exchange pair) and links to other clients, with optional persistence through any StorageAdapter.

import { ClientCryptoKeyLink, createWebLocalStorageAdapter } from "@nice-code/util";

// Anything identity-bearing wants a DURABLE adapter: a peer pins this identity's verify key the
// first time it connects (trust-on-first-use), so an identity that regenerates on reload is
// rejected from the second load on. Memory adapters (`createMemoryStorageAdapter_json()`) are for
// tests — or omit `storageAdapter` entirely for a deliberately ephemeral, in-memory identity.
const link = new ClientCryptoKeyLink({
  storageAdapter: createWebLocalStorageAdapter({ localStorage, keyPrefix: "crypto:" }),
});
await link.initialize();

// Share these with the other side (serialized prefixed strings)
const { verifyPublicKey, exchangePublicKey } = await link.getLocalPublicKeys();

// Register the other side's keys
await link.linkClient({
  linkedClientId: "client::partner-1",
  verifyPublicKey: theirVerifyKey,
  exchangePublicKey: theirExchangeKey,
  // Optionally fold both verify keys into key derivation, so a
  // tampered relayed key makes the first decryption fail:
  bindVerifyKeysIntoDerivation: true,
});
// linkClientAndStore(...) persists the link across reloads

// Sign + encrypt for the linked client (shared key derived & cached automatically)
const { encryptedData, signatureBase64 } = await link.signAndEncryptDataForLinkedClient({
  linkedClientId: "client::partner-1",
  dataToEncrypt: "hello",
});

// Other side: decrypt + verify in one call
const { data, isValid } = await otherLink.decryptAndVerifyDataFromLinkedClient({
  linkedClientId: "client::me",
  dataToDecrypt: encryptedData,
  signatureBase64,
});

// Also: signChallenge, verifyChallengeFromLinkedClient, encryptDataForLinkedClient,
// decryptDataFromLinkedClient, unlinkClient, unlinkAllClients, reset

Crypto helpers require the @scure/base peer dependency.


TypeScript utilities

import type { StringKeys } from "@nice-code/util";

// Extracts string keys from a type
type Keys = StringKeys<{ a: string; b: number; 0: boolean }>;
// → "a" | "b"