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

@nano133/signin

v0.5.0

Published

Sign in with Nano (XNO): a passkey purse, Berry Wallet, Nautilus or NanChat, or any wallet that signs. No payment, no password, no email.

Readme

@nano133/signin

Stage: Alpha. What this means: https://nano133.com/stages

Sign in with Nano (XNO). No payment, no password and no email: every way is a signature from a key the person holds, and your server checks it strictly. Your server holds no key. nano133.com runs this package for its own sign-in.

| Way | Who signs | Entry points | |---|---|---| | Passkey purse | A Nano account made from the person's passkey (Face ID, Touch ID, a PIN), in the browser | /passkey, /purse (browser), checkSignatureSignin | | A key in the page | Any wallet in the page that signs a message | /purse (browser), checkSignatureSignin | | Nautilus | The Nautilus app, by its nanoauth: request | startNautilus, answerNautilus, checkNautilus | | NanChat | The NanChat app, by its nanauth request | startNanchat, answerNanchat, checkNanchat | | Berry Wallet | Berry Wallet (or any wallet that reads the same request), by nano-signin-v1 | startNanoSignin, readNanoSignin, answerNanoSignin, checkNanoSignin |

Nautilus publishes no guide for its request: the format here was read in the app's source. NanChat's is on its developer page. Both have worked with the real apps on nano133.com. Berry Wallet's nano-signin-v1 has no guide either: it was read from the real app against a test site, and the package's tests check the exact text the app signs.

A passkey purse (browser)

import { firstUse, makePasskey, openPasskey, type PasskeyScheme } from "@nano133/signin/passkey";
import { signinBody } from "@nano133/signin/purse";

// FROZEN: a person's account lies at the address these texts give. Never edit; test with fixed vectors.
const SCHEME: PasskeyScheme = { input: "example.com purse v1", salt: "example.com", info: "example.com purse seed v1", versionByte: 1 };
const setup = { rpId: "example.com", rpName: "Example", scheme: SCHEME, exclude: madeHere() };

const made = await makePasskey(setup);          // remember made.credentialId now
const { seed } = await firstUse(setup, made);   // the seed comes from a use, never from the answer at create
// later visits: const { seed } = await openPasskey(setup);
// show the seed as a spare key before any XNO goes in: a lost passkey is a lost purse
await fetch("/signin/signature", { method: "POST", body: JSON.stringify(signinBody("example.com", seed)) });
  • A passkey belongs to one domain, so each site gets its own account for a person.
  • It needs the passkey PRF extension: passkeySupport() says "yes", "no" or "unknown".
  • Errors are PasskeyError with a code; the words are yours.

The signature route (server)

import { checkSignatureSignin } from "@nano133/signin";
import { firestoreOnce } from "@nano133/signin/firestore";

// POST /signin/signature  { address, at, signature }   (rate-limited per visitor: required)
const account = await checkSignatureSignin({ domain: "example.com", useOnce: firestoreOnce(db, "signin_once") }, body);
createYourSession(account);

The message names your domain, is valid for 5 minutes either way, and counts once (by the hash of the signed message, not the signature, which can have two forms). Small-order keys (the burn address) and an address with a wrong checksum are refused.

Nautilus and NanChat (server)

import { answerNanchat, answerNautilus, checkNanchat, checkNautilus, nanchatReply, nautilusReply, startNanchat, startNautilus, WalletRefused } from "@nano133/signin";
import { firestoreRequests } from "@nano133/signin/firestore";

const nautilus = { site: "example.com", siteAddress: SITE_ADDRESS, store: firestoreRequests(db, "signin_nautilus") };
const nanchat = { host: "example.com", store: firestoreRequests(db, "signin_nanchat") };

// start (rate-limited): set the starter cookie, pass its sha256
const n = await startNautilus(nautilus, { starter, answerUrl: (id) => `https://example.com/signin/nautilus?id=${id}` });
const c = await startNanchat(nanchat, { starter, answerUrl: "https://example.com/signin/nanchat" });

// the apps post here (no Origin check); always HTTP 200
try { await answerNautilus(nautilus, id, body); reply(nautilusReply("example.com")); }
catch (e) { if (e instanceof WalletRefused) reply(nautilusReply("example.com", e.message)); else throw e; }

// the browser asks every 2 seconds
const r = await checkNautilus(nautilus, id, starter);
if (r.status === "signedin" && r.mine) createYourSession(r.account!);
  • Only the browser that started a request is signed in by it (mine).
  • NanChat's answers all come from its relay: limit them by total and per nonce (nanchatNonce), not per IP address. NanChat shows your reply's text whatever it says: nanchatReply starts a refusal with "NOT signed in".
  • A wallet app's request can be sent to someone else: do not take such a sign-in as proof for an action that cannot be undone.

Berry Wallet (server)

nano-signin-v1: the browser shows a link, the wallet reads the request from your route (GET) and posts its signature to the same route (POST).

import { answerNanoSignin, checkNanoSignin, NANO_SIGNIN_ACCEPTED, nanoSigninRefusal, readNanoSignin, startNanoSignin, WalletRefused } from "@nano133/signin";
import { firestoreRequests } from "@nano133/signin/firestore";

const berry = { domain: "example.com", uri: "https://example.com/", store: firestoreRequests(db, "signin_nano") };
const requestUrl = (id: string) => `https://example.com/signin/nano/${id}`;

// start (rate-limited): set the starter cookie, pass its sha256
const s = await startNanoSignin(berry, { starter, requestUrl });
// s.link: berrypay://signin?request_uri=… (a button on a phone, a QR code on a computer)
// s.walletLink: nano://signin?request_uri=… (another wallet)

// GET /signin/nano/:id (the wallet reads it; no cookies, no Origin)
const request = await readNanoSignin(berry, id, requestUrl);
request ? reply(200, request) : reply(404, nanoSigninRefusal("This sign-in expired."));

// POST /signin/nano/:id (the wallet's signature): 204 and no body, the only reply Berry Wallet shows as a success
try { await answerNanoSignin(berry, id, body, requestUrl); reply(NANO_SIGNIN_ACCEPTED); }
catch (e) { if (e instanceof WalletRefused) reply(401, nanoSigninRefusal(e.message)); else throw e; }

// the browser asks every 2 seconds
const r = await checkNanoSignin(berry, id, starter);
if (r.status === "signedin" && r.mine) createYourSession(r.account!);
  • A request lives 5 minutes at most: Berry Wallet refuses a longer one.
  • The wallet signs the SHA-256 of an exact text (nanoSigninMessage) that names your domain, the page, the nonce, the times and the request's id and address. Any other text, another type of signature or a second answer is refused.
  • Rate-limit the GET and POST by IP address: the wallet's own phone sends them.

Sessions

  • At sign-in, create a new session id, and show "Signed in as nano_…" (required).
  • Refuse a sign-in call that another site sent (login CSRF): check that Origin is exactly your site, except on the routes the apps call.
  • Firestore rows carry deleteAt: set a TTL policy on each collection. Every row names its account, for a "delete my data".

Test

npm test
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 npm test

The spec for AI coding tools: ai/SPEC.md.

MIT. From nano133.com.