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

@neuraiproject/neurai-auth

v0.0.2

Published

Verify Sign-in-with-Neurai logins on a Node backend: login transactions bound to the browser pre-session, atomic CACAO verification with neurai-message.

Readme

@neuraiproject/neurai-auth

Backend verification of Sign in with Neurai for Node: the user signs a CAIP-122 message with their Neurai wallet, the browser posts the resulting CACAO to your site, and this package decides whether that is a login.

It does two things:

  1. The login transaction (spec/auth.md section 7). Every attempt is a transaction bound to the browser's pre-session cookie, with its own nonce, pairing topic, domain, URI, chains and a 10-minute window. It is consumed atomically and exactly once.
  2. The CACAO verification (section 6). The canonical text is rebuilt from the payload, the chain and the signature type are checked against the address, and the signature is verified with @neuraiproject/neurai-message — the same primitive the wallet and the browser extension use.

Nothing that comes back from the browser is used without being contrasted with the stored transaction.

npm install @neuraiproject/neurai-auth

Frontend (5 lines)

The pairing is created before /auth/begin, because its topic is the requestId that binds the signature to this QR code. The authPayload comes from the backend, so the frontend cannot drift from what will be verified.

const pairing = await nc.createPairing();                                     // topic P = requestId
const { loginId, authPayload } = await api.post("/auth/begin", { requestId: pairing.topic });
const { uri, response } = await nc.authenticate({ pairing, ...authPayload }); // show `uri` as the QR code
const { cacaos } = await response;                                            // resolves when the user approves
await api.post("/auth/complete", { loginId, cacao: cacaos[0] });              // same pre-session cookie

Backend (10 lines)

import { NeuraiAuth, neuraiAuthRoutes } from "@neuraiproject/neurai-auth";

const auth = new NeuraiAuth({
  domain: "example.com",                                    // fixed here, never taken from the browser
  uri: "https://example.com/login",
  chains: ["bip122:00000044d33c0c0ba019be5c02497304"],
  ttlSeconds: 600,
});
const routes = neuraiAuthRoutes(auth, { onLogin: (login, req) => { req.session.user = login.address; } });
app.post("/auth/begin", routes.begin);
app.post("/auth/complete", routes.complete);

Or without the adapters, if you already have your own session middleware:

app.post("/auth/begin", async (req, res) => {
  const begun = await auth.begin(req.preSession.id, { requestId: req.body.requestId, addressPolicy: "wallet" });
  res.json({ loginId: begun.loginId, nonce: begun.nonce, authPayload: auth.buildAuthPayload(begun) });
});

app.post("/auth/complete", async (req, res) => {
  // Consumes the transaction and checks nonce, domain, aud, requestId, chain, window and signature.
  const { address, chainId } = await auth.complete(req.preSession.id, req.body.loginId, req.body.cacao);
  req.session.user = address;                    // issue your session
  req.preSession.destroy();                      // and destroy the pre-session
  res.sendStatus(204);
});

Mounting the HTTP adapters

Both adapters are optional and depend on no framework: req and res are typed structurally, so nothing is installed on your behalf. They are the same handlers with different plumbing.

// Express (cookie-parser optional: the `Cookie` header is parsed when `req.cookies` is absent)
import express, { type Request, type Response } from "express";
import { NeuraiAuth, neuraiAuthRoutes } from "@neuraiproject/neurai-auth";

const app = express();
app.use(express.json());

// The type arguments are optional; give them and `onLogin` sees your own Request/Response (`req.session`, …).
const routes = neuraiAuthRoutes<Request, Response>(auth, {
  preSessionCookie: "nc_presession",
  cookieOptions: { httpOnly: true, sameSite: "Lax", secure: true, path: "/" },  // the defaults
  onLogin: async (login, req) => { req.session.user = login.address; },         // issue your session here
  onError: (error) => logger.error(error),
});
app.post("/auth/begin", routes.begin);
app.post("/auth/complete", routes.complete);
// Fastify (the same factory, exported as `fastifyNeuraiAuthRoutes` from the package root)
import type { FastifyReply, FastifyRequest } from "fastify";
import { fastifyNeuraiAuthRoutes } from "@neuraiproject/neurai-auth";

const routes = fastifyNeuraiAuthRoutes<FastifyRequest, FastifyReply>(auth, {
  onLogin: (login, req) => { req.session.user = login.address; },
});
fastify.post("/auth/begin", routes.begin);
fastify.post("/auth/complete", routes.complete);

The pre-session cookie:

  • HttpOnly, so no script can read it; SameSite=Lax, so a third-party page cannot drive the callback; Secure, so it never travels in the clear (drop it only on http://localhost during development); Path=/ and Max-Age equal to the transaction TTL.
  • begin mints one (16 random bytes in hex) when the browser has none, and keeps the existing one otherwise. If your app already has a pre-session, pass its id to auth.begin directly instead of using the adapters.
  • The pre-session is destroyed on success: complete answers with Set-Cookie: nc_presession=; Max-Age=0. Your onLogin runs before the response is written, which is where you issue the real session cookie, so the browser leaves the exchange with a user session and no pre-session.

Responses: begin answers 200 {loginId, nonce, authPayload} or 400 {error: {code, message}}; complete answers 200 {address, chainId} or 401 {error: {code, message}} with the codes below.

Security model

  • The login transaction is bound to the pre-session that started it. An attacker who obtains a valid CACAO for their own account cannot make a victim's browser submit it (login-CSRF): the transaction belongs to the attacker's cookie, not the victim's.
  • Atomic single use. complete consumes the transaction in one indivisible operation before any other check, so a second callback with the same loginId fails with unknown_transaction even when its CACAO is perfectly valid, and two concurrent callbacks cannot both win. MemoryTransactionStore does it without awaiting between read and mark; RedisTransactionStore does it with a Lua GET+DEL script (or the native GETDEL), which is atomic across processes.
  • Domain, URI, chains and nonce are the backend's, never the browser's. requestId is the pairing topic the frontend registered at begin, so a signature made for another QR code does not authenticate here.
  • addressPolicy is a hint, not a condition (section 7.2). From a CACAO the backend sees a valid address but knows neither the seed nor the derivation path, so it cannot tell an account-0 wallet address from an account-101 per-domain identity address. It is stored as informational data and never causes a rejection.
  • Balance and asset checks happen on-chain, after login, with @neuraiproject/neurai-lock or Blockbook, against whatever address signed. Do not try to infer holdings from the login itself.
  • A relayed QR code (QRLjacking) remains a residual risk (section 9): the wallet warns that the browser showing the QR will be connected, transactions expire in 10 minutes, and sensitive actions should ask the already-authenticated browser to confirm.
  • CAIP-122 version "1" only. A future "2" must be accepted alongside "1" for at least six months (ACCEPTED_CAIP122_VERSIONS).

Without a framework

handleBegin and handleComplete are the whole surface; the Express and Fastify files are thin wrappers over them, so plain node:http works too:

import { NeuraiAuth, handleBegin, handleComplete, parseCookieHeader } from "@neuraiproject/neurai-auth";

const auth = new NeuraiAuth({ domain: "example.com", uri: "https://example.com/login", chains });

if (req.url === "/auth/begin" && req.method === "POST") {
  const preSessionId = parseCookieHeader(req.headers.cookie).nc_presession;   // undefined on a first visit
  const outcome = await handleBegin(auth, { preSessionCookie: "nc_presession" }, { preSessionId, body: await readJson(req) });
  if (outcome.setCookie) res.setHeader("set-cookie", outcome.setCookie);      // mints the pre-session when absent
  res.writeHead(outcome.status, { "content-type": "application/json" }).end(JSON.stringify(outcome.body));
}

The outcome carries { status, body }; on success at /auth/complete it also carries the verified address, and the caller destroys the pre-session cookie and issues its own session. A complete, runnable example lives in apps/demo-login/server.mjs.

Error codes

Every rejection is an AuthError with a stable code (isAuthError(error) narrows it). The message is for humans; the code is the contract.

| code | Where | Meaning | | --- | --- | --- | | invalid_config | constructor, verifyCacao | Missing domain/uri, empty or non-CAIP-2 chains, bad ttlSeconds/nonceBytes. | | invalid_pre_session | begin, complete | No pre-session identifier (no nc_presession cookie). | | invalid_request_id | begin | requestId is not a pairing topic: 64 lowercase hex characters. | | statement_newline | begin, complete | The statement contains \n, which could forge canonical lines. | | unknown_transaction | complete | No such loginId, or it was already used: single use. | | wrong_pre_session | complete | The transaction belongs to another browser. The transaction is burnt anyway. | | expired | complete | The 10-minute window elapsed. | | nonce_mismatch | complete | The signed nonce is not this transaction's (a CACAO from another attempt). | | domain_mismatch | complete | The signed domain is not the configured one. | | aud_mismatch | complete | The signed aud is not the configured login URI. | | request_id_mismatch | complete | The signed requestId is not the registered pairing topic. | | timestamps_out_of_window | complete | iat/exp/nbf are unparseable or inconsistent with the window. | | invalid_shape | complete, verifyCacao | Not a CACAO: header, payload fields or iss are malformed. | | version_unsupported | verifyCacao | CAIP-122 version other than "1". | | unsupported_chain | verifyCacao | The chain of iss is not among the accepted ones. | | signature_type_mismatch | verifyCacao | s.t does not match the address (neurai-secp256k1-compact for legacy N…/t… and ECDSA witness v3 nq1r…/tnq1r…, neurai-ml-dsa-44 for AuthScript witness v1 nc1p…/tnc1p… and PQ witness v2 pq1z…/tpq1z…), or the address is not one that can sign messages (P2SH, the retired nq1p…/tnq1p… encoding). | | invalid_signature | verifyCacao | neurai-message did not validate the signature over the canonical text. |

Stores

MemoryTransactionStore (the default) is a Map with periodic pruning: fine for one process, useless for two, since a login begun on one would not be completable on the other. For several processes inject RedisTransactionStore with a small adapter over your client — ioredis is not a dependency of this package:

import Redis from "ioredis";
import { NeuraiAuth, RedisTransactionStore } from "@neuraiproject/neurai-auth";

const redis = new Redis(process.env.REDIS_URL);
const store = new RedisTransactionStore({
  set: (key, value, opts) => (opts?.px ? redis.set(key, value, "PX", opts.px) : redis.set(key, value)),
  get: (key) => redis.get(key),
  del: (key) => redis.del(key),
  eval: (script, keys, args) => redis.eval(script, keys.length, ...keys, ...args),
});
const auth = new NeuraiAuth({ domain, uri, chains, store });

consume is one round trip: the Lua script does GET and DEL inside a single script (Redis runs a script to completion before any other command), so exactly one of two concurrent callbacks gets the transaction. A client that offers neither eval nor getdel is rejected in the constructor instead of silently downgraded to a racy GET + DEL. Entries expire with SET … PX, so prune has nothing to do. Any storage with an atomic read-and-mark works: implement TransactionStore and keep consume indivisible.

Verifying a CACAO on its own

verifyCacao is pure — no clock, no state, no I/O — so a third party can validate a Neurai login signature without knowing anything about this package's transactions:

import { verifyCacao } from "@neuraiproject/neurai-auth";

const { address, chainId, message } = verifyCacao(cacao, {
  chains: ["bip122:00000044d33c0c0ba019be5c02497304"],
});

It checks shape, version, iss, chain, signature type, the canonical reconstruction and the signature. It does not check nonce, domain, aud, requestId or expiry: that is what the login transaction is for.

Public API

NeuraiAuth (begin, buildAuthPayload, complete), verifyCacao, AuthError / isAuthError / AuthErrorCode, ACCEPTED_CAIP122_VERSIONS, toRfc3339, the stores (TransactionStore, MemoryTransactionStore, RedisTransactionStore, MinimalRedisClient, LoginTransaction) and the adapters (neuraiAuthRoutes for Express, fastifyNeuraiAuthRoutes for Fastify, plus the cookie helpers parseCookieHeader / serializeCookie).

Specification: spec/auth.md (sections 3 to 8) and spec/session.md section 4 for the chain identifiers. Test vectors: spec/vectors/auth/neurai-message-0.11.0.json.