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

@userkit/js

v0.7.0

Published

The UserKit client for the browser: contact sessions, hosted sign-in, social sign-in, and the short-lived JWT your backend verifies.

Readme

@userkit/js

O cliente do UserKit para o navegador. Sessão de contato, autenticação hospedada, login social, magic link, boot e o JWT curto que o backend do desenvolvedor verifica sozinho.

Framework-agnóstico de propósito: o @userkit/react é construído sobre este pacote, e um projeto em Svelte, Vue ou nenhum framework usa exatamente o mesmo objeto.

npm install @userkit/js

Versão 0.3.0. Pré-1.0 de propósito: a superfície ainda cresce, então um minor pode quebrar compatibilidade. Fixe a versão se isso importar para você. O pacote é ESM-onlyimport, não require(); um Node antigo ou um Jest em CJS puro precisam de transformação. As mudanças estão em CHANGELOG.md.

Uso

import { createClient } from "@userkit/js";

const userkit = createClient({ publishableKey: "uk_pk_live_…" });

// Uma vez, no carregamento: restaura a sessão da visita anterior.
await userkit.load();

// A UI segue o estado. O listener é chamado na hora da inscrição também.
userkit.subscribe((state) => {
  if (state.status === "loading") return renderSkeleton();
  state.contact ? renderApp(state.contact) : renderSignIn();
});

status começa em loading de propósito. Um botão de entrar renderizado como "deslogado" antes do load() resolver pisca a cada recarga da página — que é o defeito mais visível de um SDK de autenticação.

Entrar

await userkit.signUp({ email, password, name });   // sempre resolve: 202
await userkit.signIn({ email, password });         // lança invalid_credentials
await userkit.verifyEmail(tokenDaURL);             // já entra com a pessoa
await userkit.signOut();

Login social

Duas chamadas, porque o redirecionamento sai do processo — mas o state fica guardado aqui, então a sua página de callback não precisa carregar nada:

// Na tela de login:
await userkit.signInWithOAuth({
  provider: "google",
  redirectUri: "https://app.example.com/auth/callback",
});

// Na página de callback:
await userkit.completeOAuth();

completeOAuth()code e state da URL atual e confere o state contra o que este navegador guardou: um callback que não veio de um fluxo iniciado aqui é recusado antes do código ser gasto.

O JWT

const token = await userkit.getToken();
await fetch("/api/minha-rota", { headers: { Authorization: `Bearer ${token}` } });

Cinco minutos, renovado trinta segundos antes de vencer e compartilhado entre chamadas concorrentes — chamar isso a cada requisição é o uso pretendido. Seu backend valida contra GET /v1/jwks/{publishable_key}, sem nos chamar.

Os times do contato

const teams = await userkit.listCustomerMemberships();
userkit.setActiveCustomer(teams[1].id);   // não escreve nada
const { members, invitations } = await userkit.listCustomerMembers();

Trocar de time é navegação: nada é gravado aqui nem no servidor — a próxima requisição leva outro X-Customer-Id, e a associação por trás dele é quem decide o que ela pode fazer. A escolha fica em memória de propósito: guardada, duas abas passariam a dividir um cursor só, que é exatamente o que a API abandonou ao não guardar um active_customer_id. Se você quer que ela sobreviva a um recarregamento, coloque o time na sua rota e chame setActiveCustomer no carregamento.

Sem escolha nenhuma, a API responde pela associação mais antiga do contato — o que faz a primeira renderização estar certa antes de alguém escolher.

O que o plano do time inclui

const grant = await userkit.getEntitlements();
if (grant.features["api_access"]?.enabled) { … }

Resolve para o time ativo, exatamente como as outras chamadas de time. Três respostas valem um branch: 404 é sessão anônima (não pertence a nenhum customer — ofereça criar um time), 403 unverified_session é identidade nunca provada, e 503 entitlements_unavailable é "pergunte de novo" — nunca trate como um plano vazio.

Eventos

userkit.track("checkout_started", { plan: "pro" });

Síncrono, e nunca uma requisição: os eventos entram numa fila durável (localStorage) e saem em lote — por intervalo, quando o navegador volta a ficar online, e no pagehide com keepalive. Um evento só sai da fila depois do 202 da API, e o id (UUIDv7, cunhado na chamada) é o que faz uma reentrega contar uma vez só. A fila tem teto de 500: estourou, os mais antigos são descartados — comportamento recente vale mais do que o que uma indisponibilidade perdeu horas atrás.

Nomes são [a-z0-9_.:-]{1,64}; um nome fora disso, ou com o prefixo $ (reservado à plataforma), é recusado com um throw na hora — um evento descartado em silêncio só seria descoberto num dashboard, tarde demais. Visitante anônimo pode rastrear: o anonymous_id do dispositivo sempre acompanha o lote, e uma sessão ativa resolve os eventos para o contato.

Feature flags

await userkit.boot();               // decide as flags desta pessoa
if (userkit.isEnabled("new_checkout")) { … }

isEnabled é síncrono, e isso é o contrato: uma UI que esperasse uma promise renderizaria a ausência do recurso primeiro e o recurso um tick depois, e uma flag que pisca a cada montagem é pior do que uma flag desligada.

Quem decide é o boot() — segmento, rampa percentual, tudo — no round trip que a sessão já custou. O que mantém isso atual é um poll de GET /v1/flags/{publishable_key}, um documento público e cacheável que carrega só key e enabled, sem o alvo. Daí a assimetria que é o desenho inteiro: o documento pode desligar uma flag e nunca ligarenabled: true ali diz que a chave está ligada, não que esta pessoa está na audiência. Desligar chega à página em ~30 segundos; alargar uma rampa vale no próximo boot. A direção urgente é o desligar.

O poll roda só com a página visível, e volta a perguntar assim que a aba volta em vez de esperar o intervalo que dormiu. Falha de rede mantém o último estado: um kill switch que abre quando a rede cai é o oposto do que ele existe para fazer.

Onboarding, changelog, pesquisas e sugestões

const listas = await userkit.getChecklist();
const { unread, since } = await userkit.getChangelogUnread();
const [pendente] = await userkit.listPendingSurveys();
const { posts } = await userkit.getFeedbackBoard();

Quatro superfícies, e o que não existe em cada uma é a parte que importa. Não há como marcar um passo do checklist: um passo é satisfeito por um evento ter acontecido ou por um direito do plano estar valendo, então a única escrita ali é dismissChecklist(id) — que fecha uma lista, por lista, e não conclui nada. Um ambiente tem várias, e uma marca na pessoa faria a lista lançada no mês que vem nascer escondida para quem fechou a anterior. Não há como pedir uma pesquisa: quem decide é a API, a partir de um gatilho, uma audiência e um intervalo por pessoa. E não há como ler o peso de um voto por receita — isso existe no painel e em lugar nenhum mais.

O since do badge é quando este contato chegou: um badge que abre em 47 no primeiro dia é um badge que a pessoa dispensa uma vez e nunca mais olha. answerSurvey manda o score só quando ele foi escolhido — zero é uma nota real de NPS, a mais dura delas, e nunca deve virar "não respondeu".

As notificações dentro do produto

const { notifications, unread } = await userkit.listNotifications();
await userkit.listNotifications({ unreadOnly: true });
await userkit.markNotificationRead(notifications[0].id);
await userkit.markAllNotificationsRead();

O feed é lido dentro do time ativo: as mensagens daquele time mais as que foram endereçadas à própria pessoa (customer_id: null), que aparecem em qualquer time dela. setActiveCustomer muda a leitura, e markAllNotificationsRead limpa exatamente o que a tela mostrava — quem zera o badge num time não marca como lidas as mensagens de outro.

unread é a conta de tudo que não foi lido, nunca o tamanho da página — a página para em 50, então um badge tirado dela erra exatamente para quem tem mais. E não existe método que crie uma notificação: quem escreve é o backend do desenvolvedor, em POST /v1/notifications com a chave uk_sk_, porque uma rota alcançável pelo navegador deixaria qualquer página dizer qualquer coisa a qualquer usuário dele.

url é para onde a notificação leva, ou null quando ela não leva a lugar nenhum. Ou um caminho começando em /, ou um endereço http(s) completo — a API recusa qualquer outra forma na escrita, esquema que executa incluído, então dá para colocar num href sem sanitizar de novo.

Nada aqui passa por consentimento, e isso é construção e não decisão de tela: a categoria que a API grava é um valor que notification_preferences não consegue guardar, então não há linha que desligue um aviso de "sua fatura falhou".

Quais e-mails esta pessoa aceita receber

const preferencias = await userkit.getNotificationPreferences();
await userkit.updateNotificationPreferences([{ category: "marketing", opted_out: true }]);

A resposta é sempre o catálogo inteiro, não as decisões guardadas: a ausência de linha é o consentimento, então uma tela que mostrasse só o que existe estaria vazia para todo mundo que nunca a abriu.

E-mail transacional não está na lista, e não é omissão — não existe categoria que o nomeie. Confirmação de endereço, redefinição de senha, recibo e aviso de segurança continuam chegando depois de a pessoa desligar tudo aqui, e vale dizer isso na tela em vez de deixar a inferência acontecer. Todo e-mail não transacional carrega, além disso, um link de descadastro que funciona sem sessão nenhuma e não expira: quem não consegue sair marca como spam, e aí quem paga é o domínio.

Erros

import { isUserKitError } from "@userkit/js";

try {
  await userkit.signIn({ email, password });
} catch (error) {
  if (isUserKitError(error) && error.code === "invalid_credentials") { … }
}

Ramifique no code, nunca na message: o código é contrato estável, a mensagem é uma frase para gente e pode mudar. UserKitErrorCode é a união dos códigos conhecidos — error.code autocompleta — e continua aberta para os que a API ganhar depois. Uma chamada que não responde em 15 s falha com timeout (timeoutMs na criação do cliente muda o teto; 0 desliga).

Onde a sessão fica, e o modo proxy

No modo padrão (direct) o token uk_ct_… fica em localStorage, por trinta dias, e qualquer script na sua origem consegue lê-lo — é o custo de não precisar de servidor. Se você tem um servidor, prefira mode: "proxy": as rotas na sua própria origem guardam o token num cookie httpOnly, e o navegador nunca o vê. É o arranjo que @userkit/nextjs monta para você.

Um meio-termo sem servidor é trocar o storage: sessionStorage termina com a aba, e a interface é de três linhas.

const userkit = createClient({
  publishableKey: "uk_pk_live_…",
  storage: {
    get: (key) => sessionStorage.getItem(key),
    set: (key, value) => sessionStorage.setItem(key, value),
    remove: (key) => sessionStorage.removeItem(key),
  },
});

Ciclo de vida

Um cliente por página. Onde isso não vale — StrictMode, hot reload, um micro-frontend que monta e desmonta, um teste entre dois casos — destroy() desarma todo timer e listener que o cliente instalou; a sessão e a fila de eventos ficam no storage para o próximo. A sessão acompanha as outras abas da mesma origem: sair numa é sair em todas, na hora.

Testes

@userkit/js/testing traz o fakeFetch() e as fixtures (fakeContact, fakeAuthResult, …) que a própria suíte deste pacote usa — instale-o no beforeEach e responda o que cada chamada deve receber.

O que não está aqui

Um método de SDK para uma rota que não existe é promessa, não funcionalidade. Por essa regra não há nada aqui que edite nome, e-mail ou avatar de um contato: /v1/contact/* não tem esse endpoint.