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

@mantequilla-soft/butrauth-client

v0.5.0

Published

Node.js client for Butter Auth — OAuth 2.0 + Hive identity bridge

Readme

@mantequilla-soft/butrauth-client

Tiny Node.js client for Butter Auth — OAuth 2.0 Authorization Code + PKCE, RS256 access tokens, Hive identity bridge.

Zero runtime dependencies. Node ≥ 18 (uses global fetch and node:crypto).

TL;DR

What this is. A small Node library so your server does not have to hand-roll an OAuth client. It builds the redirect URL, swaps the code for a token, and verifies tokens offline.

Why use it rather than raw fetch. Two of the fiddly parts are done for you and are easy to get subtly wrong by hand: PKCE (generating the verifier and challenge correctly) and token verification (checking an RS256 signature against the right key, rejecting the wrong issuer, and refusing a token with no expiry). Verification is local, so it costs no network call.

Where it runs. Your backend, never the browser — it uses your client secret.

sequenceDiagram
    participant A as Your backend<br/>(this SDK)
    participant U as User's browser
    participant B as Butter Auth

    A->>A: createAuthUrl()<br/><i>makes PKCE verifier + challenge</i>
    A->>U: redirect them
    U->>B: signs in, links or creates account
    B->>U: back to you with ?code=
    U->>A: hands you the code
    A->>B: exchangeCode(code, verifier)
    B->>A: access_token + username
    A->>A: verifyAccessToken(token)<br/><i>offline: RS256 + issuer + expiry</i>

Requirements

  • Node 18 or newer — the SDK uses global fetch and node:crypto.
  • Zero runtime dependencies.
  • A registered app: client_id, client_secret, and a redirect_uri that matches exactly what you registered.

Install

npm install @mantequilla-soft/butrauth-client

Usage

import { ButrAuthClient } from '@mantequilla-soft/butrauth-client'

const butr = new ButrAuthClient({
  baseUrl: 'https://butrauth.com',
  clientId: process.env.BUTRAUTH_CLIENT_ID,
  clientSecret: process.env.BUTRAUTH_CLIENT_SECRET
})

// 1. Build the redirect URL (in your /login handler)
const { url, codeVerifier, state } = butr.createAuthRequest({
  redirectUri: 'https://my-app.com/callback'
})
// → store `codeVerifier` in an httpOnly cookie or server session
// → res.redirect(url)

// 2. Exchange the code (in your /callback handler)
const tokens = await butr.exchangeCode({
  code: req.query.code,
  redirectUri: 'https://my-app.com/callback',
  codeVerifier // from the cookie/session
})
// → { accessToken, tokenType, expiresIn, refreshToken, refreshExpiresIn,
//     username, incubation, handle }
//
// ⚠️ `username` is NULL when `incubation` is true — the person has signed in and
//    picked a public name but has no Hive account yet. Every new signup starts
//    that way, so this is the common case, not an edge one. See "Users with no
//    Hive account" below.

// 3. Verify the token whenever you need to authenticate a request
const claims = await butr.verifyAccessToken(tokens.accessToken)
// → { userId, hiveUsername, clientId, issuedAt, expiresAt, incubation, handle }

// 4. Access tokens last one hour. When one expires, refresh instead of sending
//    the user back through the authorize flow. Store the NEW refresh token.
const renewed = await butr.refreshAccessToken(storedRefreshToken)

The full HTTP contract is documented in ../INTEGRATION.md.

A runnable Express example lives in example.js.

API

new ButrAuthClient({ baseUrl, clientId, clientSecret })

client.createAuthRequest({ redirectUri, scope?, state?, invite?, referrer? })

Returns { url, codeVerifier, state }. The caller must persist codeVerifier until exchangeCode runs. invite and referrer hand a referral to the signup screen; see Invite links below.

client.exchangeCode({ code, redirectUri, codeVerifier })

Returns { accessToken, tokenType, expiresIn, refreshToken, refreshExpiresIn, username }. Throws ButrAuthError on any failure.

client.refreshAccessToken(refreshToken)

Exchanges a refresh token for a fresh access token (RFC 6749 §6), so a session outlives the one-hour access token without another authorize round-trip. Returns the same shape as exchangeCode.

Refresh tokens are rotated: the token you pass in is spent, and the refreshToken on the response is the only one that will work next time — always persist it, replacing the old value. Replaying a spent token is treated as a stolen credential and revokes the entire rotation chain, so the user has to log in again. Store refresh tokens like passwords: server-side, never in the browser.

client.verifyAccessToken(token)

Verifies the RS256 JWT against Butter Auth's public key (cached for one hour) and returns the decoded claims. Throws ButrAuthError on bad signature, expiry, wrong audience, or wrong token type.

client.getPublicKey()

Returns the RSA public key (PEM). Mostly internal — call verifyAccessToken instead.

client.requireAuth({ optional?, requireHiveAccount? })

Returns Express-style middleware. Reads Authorization: Bearer <token>, verifies the JWT, and attaches the claims to req.butrAuth.

import express from 'express'
const app = express()

// Protect a route — 401 if the token is missing/invalid.
app.get('/me', butr.requireAuth(), (req, res) => {
  res.json(req.butrAuth) // { userId, hiveUsername, clientId, incubation, handle, ... }
})

// Optional: lets anonymous requests through with req.butrAuth = null.
app.get('/feed', butr.requireAuth({ optional: true }), (req, res) => {
  res.json({ user: req.butrAuth, posts: [] })
})

// Mount this on EVERY route that signs or broadcasts. 403 (not 401) for someone
// with no Hive account: they are correctly authenticated, they just have no
// account to act as yet.
app.post('/api/broadcast', butr.requireAuth({ requireHiveAccount: true }), handler)

client.resolveHandles(handles)

Resolve incubation handles to their current identity, auto-chunked at 100. Returns [{ handle, userId, status, hiveUsername }] — order is not guaranteed, so key off handle.

client.reportIncubationProgress(userId, percent)

Tell Butter Auth how far one of your users is towards whatever you ask of them, so their app owner sees a progress bar in the activation queue. Opaque and advisory: it grants nothing and approves nobody.

client.getIncubationStatus(accessToken)

The caller's own incubation state — handle, approval, whether they can create an account yet — for rendering a "you're nearly there" prompt.

client.getReferrer(username)

Who referred this user. Pass a Hive username or a warm-up handle — they are interchangeable, and after graduation the same person answers to both, so a referral recorded under the handle is still found under the account.

const { referredBy } = await butrauth.getReferrer('alice')
// { username: 'alice', referredBy: 'bob', referredAt: '2026-09-14T…' }

referredBy is null both for someone who named nobody and for a username nobody here has ever used. That is deliberate: neither is a person you can pay.

⚠️ The referrer is a name the user typed, not a verified identity. Butter Auth checks it has the shape of a Hive name, refuses a self-referral, and stores only the first answer ever given — it does not check the account exists. Confirm it before you pay it.

client.getReferrers(usernames)

getReferrer for many users at once, auto-chunked at 100. Settling referral credit means asking about a page of users, and the single-name call is one HTTP request each. Every requested name comes back, in order, so you can zip the results against your input instead of diffing two lists.

Referral links

Referrals are recorded when a user names themselves — picking a warm-up handle or creating an account — on whichever path they take. To credit a link, put a referrer on the authorize URL you send them to:

https://butrauth.com/authorize?client_id=…&screen_hint=signup&referrer=alice

The naming screen then shows that name already filled in and locked, so the person who followed the link cannot quietly change or clear the credit. 3Speak does this from a ?ref=alice link on any of its own URLs.

Invite links

An app owner can activate trusted people (a business, a big creator) as referrers on butrauth's Referrals tab. Each referrer gets an invite link; a new user who signs up through it gets a real Hive account on day one instead of the warm-up, up to the referrer's monthly limit and the providers' budget. When either runs out they join through the warm-up, still credited.

Set your app's invite link format on the same tab, e.g. https://yourapp.com/invite/{code}, so the links point at your site (the host must be one your redirect URIs already use). Then:

  1. Your invite page reads the code and shows who invited them: client.getInvite(code) returns { valid, referrer, fastTrack, app }. Check app.clientId === yourClientId.
  2. Keep the code until they sign up (a cookie or localStorage), and pass it on: client.createAuthRequest({ redirectUri, invite: code }). butrauth binds it after sign-in and, if the link can fast-track them, goes straight to account creation. Send screen_hint=signup as usual for a new user.
  3. A screen for your referrers: client.getReferralLinks(account) lists the links your app gave that Hive account, with url, limit, usedThisMonth, leftThisMonth, resetsAt and totals, and client.getReferralPeople(account, link.id) lists who came in. Both are confidential (server side) and take the account YOUR session verified, so they also work for users who signed in with a wallet rather than butrauth. They only ever return your own app's links.

Users with no Hive account

Butter Auth can hold an identity that has signed in, picked a public name, and has no Hive account yet. The point is cost: a free account spends a real, money-backed creation token and providers cap at roughly 25 a day, so the token should follow evidence that someone will stay.

Every new signup starts this way. It is not an opt-in, and there is no per-app switch — so if your integration ignores this, it is broken for every new user, not for a rare few.

Such a token carries:

{ username: null, incubation: true, handle: 'newbie-name' }

That null is deliberate, and it is the safety property. An integration written before this existed reads username, fails its own Hive-username check, and broadcasts nothing.

Never substitute handle for hiveUsername. A handle is a display pseudonym for an account that does not exist; signing an operation authored by it produces a post from an account that is not there. Keep it in its own variable, well away from wherever you store the signed-in Hive account.

Three things to do:

  1. Guard every signing or broadcasting route with requireAuth({ requireHiveAccount: true }).
  2. Store your content rows against userId, never the handle — a handle changes while incubating and again at graduation. Resolve for display with resolveHandles().
  3. Give them somewhere to put content in the meantime. That is what @mantequilla-soft/incubation-client is for: off-chain posts, replies, votes and follows, replayed under a real account at graduation.

The full contract is in ../INTEGRATION.md §3b.

TypeScript

Type definitions ship in the package. No @types/... install needed.

import { ButrAuthClient, type AccessTokenClaims } from '@mantequilla-soft/butrauth-client'

Broadcasting on the user's behalf

Butter Auth does not broadcast for you. The user grants posting authority to your service account on chain during the auth flow, and your backend uses its own posting WIF to sign and broadcast operations. See ../INTEGRATION.md.