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

sour.js

v0.1.1

Published

ENS name resolution, decentralized content fetching (IPFS/IPNS/Arweave), and ENS domain registration (Citron) — core logic only, no UI.

Downloads

326

Readme

sour.js

Core logic for ENS name resolution, decentralized content fetching (IPFS / IPNS / Arweave), and ENS domain registration (Citron). No DOM, no UI, no wallet-connect modal — just functions you call from your own app, CLI, or server.

Ships as a real dual package: import gets you ESM, require gets you CommonJS, both from the same source.

Install

npm install sour.js ethers

A few features are optional and only need their package installed if you actually use them:

npm install @ensdomains/content-hash   # decodeContenthash / resolveENS
npm install @helia/verified-fetch      # fetchIPFS / fetchIPNS
npm install jszip                      # buildDirectoryZip

Usage

import { resolveENS, fetchRoot, resolveSiteRoot } from 'sour.js';
// or: const { resolveENS, fetchRoot, resolveSiteRoot } = require('sour.js');

const resolved = await resolveENS('vitalik.eth');
// { ens, resolver, contenthash, protocol: 'ipfs', codec, value, sourPath }

const { mode, result, sitePath } = await resolveSiteRoot(resolved);
// mode: "site" | "media" | "download"

Citron (ENS registration)

import { citron } from 'sour.js';
// or: import * as citron from 'sour.js/citron';

const provider = new ethers.JsonRpcProvider(citron.NETWORKS.mainnet.rpc[0]);
const { label, name } = citron.normalizeLabel('my-new-name');

const available = await citron.isAvailable(provider, 'mainnet', label);
if (available) {
  const price = await citron.getPrice(provider, 'mainnet', label, 31536000n); // 1 year
  // price: { base, premium, total, unit: "ETH", decimals: 18 }
}

Commit/reveal flow:

const secret = citron.generateSecret();
const built = citron.buildRegistration('mainnet', {
  label, owner: signerAddress, duration: 31536000n, secret
});

const commitment = await citron.makeCommitment(provider, 'mainnet', built);
await citron.commit(signer, 'mainnet', commitment);

// wait for the on-chain minAge window (see getCommitmentWindow) ...

const price = await citron.getPrice(provider, 'mainnet', label, 31536000n);
await citron.register(signer, 'mainnet', built, { value: price.total });

ENSv2 (Sepolia testnet) uses the same functions — pass 'sepolia' as the network — but pays in an ERC20 instead of ETH, so approve() it first:

const price = await citron.getPrice(provider, 'sepolia', label, duration);
await citron.approvePaymentToken(signer, 'sepolia', price.total);
await citron.register(signer, 'sepolia', built);

If a transaction reverts, citron.decodeRegistrarError(error) tries to turn it into a friendly message (ENSv1 only — ENSv2's custom errors aren't published yet); it returns null when it can't decode the revert, so fall back to error.shortMessage / error.reason.

API

ENS resolution (sour.js)

| Export | Description | |---|---| | normalizeENS(name) | Canonicalize a name to label.eth (ENSIP-15 normalized). | | resolveENS(name, opts?) | Full resolve: name → resolver → contenthash → decoded {protocol, value}. | | getResolver(node, opts?) | Low-level: resolver address for a namehash node. | | getContenthash(resolver, node, opts?) | Low-level: raw contenthash bytes. | | decodeContenthash(hex) | Decode raw contenthash into {protocol, codec, value}. | | ensToSourPath(name) / parseSourPath(path) | Convert between name.eth and Sour's /name/ URL convention. | | ethHostToSourPath(url) | Convert a https://name.eth/... URL into a Sour-style path. | | rpcCall(method, params, opts?) | Race a JSON-RPC call across multiple endpoints. |

opts accepts { rpcEndpoints, timeoutMs, registry }.

Content fetching (sour.js)

| Export | Description | |---|---| | fetchContent(resolved, path) | Fetch a path from whatever resolved.protocol is (ipfs/ipns/arweave). | | fetchRoot(resolved) | Fetch /, falling back to /index.html, /index.htm. | | resolveSiteRoot(resolved) | Figure out whether a resolved name is a site, a single media file, or a directory — returns {mode, result, sitePath?}. | | fetchIPFS(cid, path) / fetchIPNS(name, path) | Direct protocol-specific fetch via Helia verified-fetch. | | fetchArweave(txId, path) | Fetch from Arweave, following path-manifests. | | buildDirectoryZip(resolved, paths) | Zip a set of paths under a resolved directory (needs jszip). | | isHTML, isMediaMime, isMediaPath, isMediaResult, mimeType | Small content-sniffing helpers. |

Citron / ENS registration (sour.js/citron or sour.js's citron export)

| Export | Description | |---|---| | NETWORKS.mainnet / NETWORKS.sepolia | Registrar addresses, RPC lists, commit-reveal window. | | normalizeLabel(raw) | Parse+normalize a single .eth label. | | isAvailable(providerOrSigner, network, label) | Availability check. | | getPrice(providerOrSigner, network, label, durationSeconds) | Quote a price (ETH on mainnet, USD-pegged ERC20 on Sepolia/ENSv2). | | getCommitmentWindow(providerOrSigner, network) | Live minAge/maxAge from the contract. | | generateSecret() | Random 32-byte commit secret. | | buildRegistration(network, params) | Build the registration struct/args for either registrar version. | | makeCommitment / commit / getCommitmentTimestamp / register | The commit-reveal transaction steps. | | approvePaymentToken(signer, network, amount) | ERC20 approve step, ENSv2 only. | | decodeRegistrarError(error) | Best-effort friendly message from a revert (ENSv1 only). |

What's not included

This package is deliberately UI-free. It does not include: a wallet connect UI, an SPA router, HTML rewriting/site-mounting (for actually serving a resolved site in a browser), or countdown-timer polling for the commit-reveal wait — bring your own for all of that.

License

MIT