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

@grahak/react

v0.0.7

Published

React bindings for the Grahak customer-conversation widget — <GrahakProvider> + hooks over @grahak/js.

Readme

@grahak/react

React bindings for the Grahak customer-conversation widget — a thin convenience layer over @grahak/js. A provider that mounts the widget once, plus hooks to read its state and drive it.

The widget runs in an iframe on Grahak's origin; your page never holds Grahak tokens — the session is established and kept inside the iframe.

Contents

Install

npm install @grahak/react

react >= 18 is a peer dependency.

How it works

Two pieces:

  1. Backend — an authed endpoint that signs a short-lived identity JWT for the logged-in user with your project's private key. This is the security boundary; the JWT is what proves who the user is.
  2. Frontend — wrap your app in <GrahakProvider> once; use hooks to drive it.

The widget runs in an iframe on Grahak's origin and runs the token exchange itself, so Grahak tokens never touch your page.

Backend — mint an identity JWT

Identical to @grahak/js — the JWT contract is the same regardless of which SDK renders the widget.

1. Generate your keypair

You sign identity JWTs with an RS256 private key and upload only the public half to Grahak — the GitHub-SSH-keys model. A leak of what Grahak stores grants no signing power. Any RS256 keypair works; with openssl:

# Private key — backend secret, never sent to the browser (PKCS#8 PEM)
openssl genpkey -algorithm RSA -pkcs8 -out grahak_private.pem -pkeyopt rsa_keygen_bits:2048
# Public key — upload to Console → project → Identity keys (SPKI PEM)
openssl rsa -in grahak_private.pem -pubout -out grahak_public.pem

Paste grahak_public.pem into Console → project → Identity keys, and store the private key as a backend secret (e.g. GRAHAK_PRIVATE_KEY). You can register multiple public keys per project and rotate freely — old keys keep verifying until you revoke them.

2. The JWT claims

| Part | Field | Required | Notes | |------|-------|----------|-------| | header | alg | yes | must be RS256 | | header | typ | — | JWT (conventional) | | header | kid | optional | RFC 7638 JWK thumbprint of your public key. If set, Grahak goes straight to it; if omitted, it tries all the project's active keys. Omitting it is fine. | | claim | aud | yes | must equal your projectId (in Console: your project → Settings → Project ID) | | claim | sub | yes | your app's stable, immutable user id (the host externalId) — never an email | | claim | iat | yes | issued-at; the token must be < 10 min old (30s clock tolerance) | | claim | exp | yes | set short, e.g. now + 10 min | | claim | email | optional | display only | | claim | name | optional | display only | | claim | avatar_url | optional | display only — snake_case | | claim | metadata | optional | arbitrary JSON object, stored on the end user |

email/name/avatar_url/metadata refresh on every boot; sub is the fixed identity (Grahak upserts the end user by (projectId, sub)).

The JWT is single-use and short-lived — mint a fresh one per widget boot. That's why the provider takes a userJwt getter, not a string.

3. Mint it on your backend

Any RS256 JWT library works — the claim contract above is all that matters.

TypeScript / Node (jose):

// GET /api/me/grahak-identity  ->  { jwt }   (an authed route — behind your login)
import { SignJWT, importPKCS8 } from "jose";

const key = await importPKCS8(process.env.GRAHAK_PRIVATE_KEY, "RS256");
const jwt = await new SignJWT({
  name: user.name,
  email: user.email,
  avatar_url: user.avatarUrl, // snake_case
})
  .setProtectedHeader({ alg: "RS256", typ: "JWT" }) // kid optional
  .setSubject(user.id) // your stable user id → externalId
  .setAudience(process.env.GRAHAK_PROJECT_ID)
  .setIssuedAt()
  .setExpirationTime("10m")
  .sign(key);

Go (golang-jwt/jwt v5):

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

claims := jwt.MapClaims{
    "sub":        user.ID,         // stable user id → externalId
    "aud":        grahakProjectID,
    "iat":        time.Now().Unix(),
    "exp":        time.Now().Add(10 * time.Minute).Unix(),
    "name":       user.Name,
    "email":      user.Email,
    "avatar_url": user.AvatarURL,  // snake_case
}
tok := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// kid optional: tok.Header["kid"] = grahakKeyThumbprint
signed, err := tok.SignedString(privateKey) // *rsa.PrivateKey (parse the PEM)

Python (PyJWT):

# pip install "pyjwt[crypto]"
import time
import jwt  # PyJWT

with open("grahak_private.pem") as f:
    private_key = f.read()

now = int(time.time())
token = jwt.encode(
    {
        "sub": user.id,                 # stable user id → externalId
        "aud": GRAHAK_PROJECT_ID,
        "iat": now,
        "exp": now + 600,               # 10 min
        "name": user.name,
        "email": user.email,
        "avatar_url": user.avatar_url,  # snake_case
    },
    private_key,
    algorithm="RS256",
    # headers={"kid": GRAHAK_KEY_THUMBPRINT},  # optional
)
# PyJWT >= 2.0 returns a str

Frontend — wrap your app

Get started

Wrap your app once. A GETTER, not a string — the identity JWT is single-use + short-lived, so the SDK fetches a fresh one on every (re)connect:

import { GrahakProvider } from "@grahak/react";

function App() {
  return (
    <GrahakProvider
      projectId={GRAHAK_PROJECT_ID}
      userJwt={async () => {
        const res = await fetch("/api/me/grahak-identity");
        return (await res.json()).jwt;
      }}
    >
      <YourApp />
    </GrahakProvider>
  );
}

That's it — Grahak renders a floating launcher (with the unread badge) and owns the chat panel. The iframe stays mounted across navigation, so the badge keeps updating even while the widget is closed.

Mount <GrahakProvider> only for authenticated users — the userJwt getter needs a session. For logged-out users, don't render it.

Hooks

Read state or drive the widget from anywhere below the provider:

import { useGrahak, useGrahakUnread } from "@grahak/react";

function HelpButton() {
  const { open, isOpen, unreadCount } = useGrahak();
  return (
    <button onClick={open}>
      Help {unreadCount > 0 && <span className="badge">{unreadCount}</span>}
    </button>
  );
}

// Or just the count, for a badge elsewhere in your nav:
const count = useGrahakUnread();

useGrahak() returns:

{
  widget,        // the underlying @grahak/js controller (advanced; may be null pre-mount)
  unreadCount,   // number — re-renders on change
  isOpen,        // boolean
  isReady,       // handshake complete
  open, close, toggle,
  setTheme,      // (theme: "light" | "dark") => void
}

Common patterns

Match your app's theme (live, no remount):

<GrahakProvider projectId={...} userJwt={getJwt} theme={isDark ? "dark" : "light"}>

Use your own launcher button — the launcher render prop:

Want a fully custom floating button instead of Grahak's? Pass a launcher render prop. Grahak suppresses its own FAB (so you don't also touch overlay.launcher), docks your element in the same corner, and re-renders it with live state — you just describe the button:

<GrahakProvider
  projectId={...}
  userJwt={getJwt}
  launcher={({ isOpen, unreadCount, toggle }) => (
    <button onClick={toggle} data-open={isOpen}>
      {isOpen ? <CloseIcon /> : <ChatIcon />}
      {unreadCount > 0 && <span className="dot" />}
    </button>
  )}
>
  <YourApp />
</GrahakProvider>

The renderer receives { isOpen, unreadCount, isReady, open, close, toggle } and re-runs whenever they change. This is the idiomatic React path.

Place your own trigger anywhere instead — if you'd rather put the button somewhere in your own layout (not docked in the corner), turn Grahak's off with overlay={{ launcher: false }} and drive it from useGrahak():

function MyTrigger() {
  const { toggle, unreadCount } = useGrahak();
  return <button onClick={toggle}>Support {unreadCount || ""}</button>;
}

(overlay.launcher also accepts a raw HTMLElement, but that's for the vanilla @grahak/js SDK — in React use the launcher prop or useGrahak() above.)

Position / size the launcher + paneloverlay forwards to mountOverlay():

<GrahakProvider
  projectId={...}
  userJwt={getJwt}
  overlay={{ position: "bottom-left", accentColor: "#4f46e5" }}
>

Custom placement (attach mode)

Want the widget inside your own popover / drawer / panel instead of Grahak's floating overlay? Set mode="attach" and render a <GrahakWidget> where you want it:

import { GrahakProvider, GrahakWidget, useGrahak } from "@grahak/react";

<GrahakProvider projectId={...} userJwt={getJwt} mode="attach">
  <HelpPopover />
</GrahakProvider>;

function HelpPopover() {
  const { isOpen, open, close, unreadCount } = useGrahak();
  return (
    <Popover open={isOpen} onOpenChange={(o) => (o ? open() : close())}>
      <PopoverTrigger>Help {unreadCount || ""}</PopoverTrigger>
      {/* forceMount keeps the iframe alive when the popover closes */}
      <PopoverContent forceMount>
        <GrahakWidget style={{ width: 380, height: 560 }} />
      </PopoverContent>
    </Popover>
  );
}

⚠️ Keep <GrahakWidget> mounted. An iframe reloads if it's unmounted or moved in the DOM — dropping the session, the open thread + draft, and the unread polling. With Radix Popover/Dialog that means forceMount on the content + hiding via CSS; don't let it unmount on close. (Overlay mode avoids this entirely — prefer it unless you specifically need in-popover placement.)

Notes

  • Mount the provider once, high in your tree. It creates + tears down the widget on (un)mount; remounting it on every route would reload the iframe and drop the session + in-iframe state.
  • <GrahakProvider> is client-only. It uses effects + postMessage, so it can't render on the server — in Next.js App Router, render it from a "use client" component (or wrap it in one).
  • Mount <GrahakProvider> only for authenticated users — the userJwt getter needs a session. For logged-out users, don't render it (and gate any useGrahak() consumers behind your auth check).
  • You don't set a widget URL. The widget always loads from widget.grahak.dev in dev and prod alike; to test, use a dev-mode project's projectId. (widgetBaseUrl exists only for Grahak's own dev / self-hosting.)
  • Changing userJwt (e.g. a new getter each render) does not remount — the latest getter is always used on reconnect. Changing projectId does.
  • A rejecting userJwt getter (or a stale/invalid token) surfaces on the underlying controller's error event — const { widget } = useGrahak(); then widget?.on("error", …).

For the lower-level controller API, see @grahak/js.

Troubleshooting

| Symptom | Likely cause | Fix | |---------|--------------|-----| | Widget shows "We don't recognize who you are" | Identity exchange failed: the JWT is > 10 min old, already used (it's single-use), a static string that went stale, aud ≠ your projectId, or the public key isn't registered (or was revoked) on the project. | Pass userJwt as a getter, not a string. Confirm aud == projectId, the registered public key matches your private key, and iat is fresh (≤ 10 min, ~30s clock tolerance). | | Open thread / draft / unread badge resets every time the panel closes | <GrahakWidget> is being unmounted on close (e.g. a Radix Popover/Dialog without forceMount). | forceMount + a CSS hide, or use overlay mode. (See the attach-mode callout above.) | | "Invalid hook call" / two copies of React | Two React instances — only happens when the package is npm link/symlinked for local dev. | Ensure a single React (Vite: resolve.dedupe: ["react","react-dom"]). A normal published npm install never hits this. | | One transient postMessage "target origin … does not match" on load | Benign load-timing race; the SDK ignores mismatched-origin inbound messages. | No action unless it's persistently noisy. |