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

@srcabc/notabot-next

v0.3.1

Published

Drop-in Notabot protection for Next.js App Router (CAPTCHA + durable passkey step-up)

Readme

@srcabc/notabot-next

Official Next.js App Router integration for Notabot protected actions.

Install

npm install @srcabc/notabot-next @srcabc/notabot-server

Client Gate

"use client"

import { useRouter } from "next/navigation"
import { NotabotGate } from "@srcabc/notabot-next/client"

export function ArticleGate() {
  const router = useRouter()

  return (
    <NotabotGate
      siteKey={process.env.NEXT_PUBLIC_NOTABOT_SITE_KEY!}
      apiBase={process.env.NEXT_PUBLIC_NOTABOT_API_BASE}
      action="article_unlock"
      unlockUrl="/api/notabot/unlock"
      subjectTokenUrl="/api/notabot/subject-token"
      onVerified={() => router.refresh()}
    />
  )
}

NotabotGate loads https://<notabot-origin>/widget/v2/loader.js, mounts the widget, listens for notabot:verified, and sends only proof_token plus optional correlation_id to your unlock route. It does not trust browser state as final authorization. Omit subjectTokenUrl for CAPTCHA-only protection.

Unlock Route

import { createNotabotUnlockRoute } from "@srcabc/notabot-next/server"

export const POST = createNotabotUnlockRoute({
  siteKey: process.env.NOTABOT_SITE_KEY!,
  signingSecret: process.env.NOTABOT_SIGNING_SECRET!,
  apiBase: process.env.NOTABOT_API_BASE ?? "https://notabot.srcabc.com/api/v1",
  unlockCookieSecret: process.env.NOTABOT_UNLOCK_COOKIE_SECRET!,
  cookieName: "article_unlock",
  action: "article_unlock",
  scope: "example.com/article",
  cookiePath: "/article",
  cookieTtlSeconds: 3600,
})

The route validates the one-use proof server-to-server, sets a signed HttpOnly unlock cookie on allow, and fails closed on every error.

For partner apps that need site policy checks or a stable response contract, keep the orchestration in the package and configure hooks instead of reimplementing validation:

import { NextResponse } from "next/server"
import { createNotabotUnlockRoute } from "@srcabc/notabot-next/server"

export const POST = createNotabotUnlockRoute({
  siteKey: process.env.NOTABOT_SITE_KEY,
  signingSecret: process.env.NOTABOT_SIGNING_SECRET,
  apiBase: process.env.NOTABOT_API_BASE ?? "https://notabot.srcabc.com/api/v1",
  unlockCookieSecret: process.env.NOTABOT_UNLOCK_COOKIE_SECRET,
  cookieName: "article_unlock",
  action: "article_unlock",
  scope: "example.com/article",
  cookiePath: "/article",
  beforeValidate: async (request) => {
    if (request.headers.get("host") !== "example.com") {
      return NextResponse.json({ error: "not_found" }, { status: 404 })
    }
  },
  mapError: (failure) => ({
    status: failure.reason === "timeout" ? 504 : 403,
    code: failure.reason === "timeout" ? "captcha_validation_timeout" : failure.reason,
    message: "Verification failed.",
  }),
  onSuccess: ({ result }) => ({
    unlocked: true,
    correlationId: result.correlationId ?? null,
    expiresAt: result.expiresAt ?? null,
  }),
})

beforeValidate runs before body parsing and token validation, mapError shapes validation/configuration failures, and onSuccess shapes the JSON body without taking over cookie creation.

Page Guard

import { redirect } from "next/navigation"
import { verifyNotabotUnlockCookie } from "@srcabc/notabot-next/server"

export default async function ProtectedPage() {
  const unlocked = await verifyNotabotUnlockCookie({
    cookieName: "article_unlock",
    scope: "example.com/article",
    secret: process.env.NOTABOT_UNLOCK_COOKIE_SECRET!,
  })

  if (!unlocked) redirect("/article/locked")

  return <Article />
}

The protected content should be rendered only after the server-side guard accepts the signed unlock cookie.

deriveRequestOrigin(request) is exported from @srcabc/notabot-next/server for apps that need the same origin resolution in their own policy wrappers.

Subject-Token Route (durable passkey step-up)

To enable durable passkey step-up, add a same-origin subject-token route. The widget calls it to obtain a server-minted identity token bound to your site, the widget session, and the request origin.

import { createNotabotSubjectTokenRoute } from "@srcabc/notabot-next/server"

export const POST = createNotabotSubjectTokenRoute({
  siteKey: process.env.NOTABOT_SITE_KEY!,
  signingSecret: process.env.NOTABOT_SIGNING_SECRET!,
  subjectCookieName: "notabot_subject",
  allowedScopes: ["challenge", "verify", "register"],
  defaultScopes: ["challenge", "verify"],
})
  • The subject identity is server-authoritative: it lives in a long-lived HttpOnly first-party cookie and is never taken from the request body. Don't send your own subject_id from the client.
  • requested_scopes are allowlisted; unknown scopes are rejected. register enables passkey enrolment; manage_credentials is not granted by default.
  • The endpoint must be same-origin with the page (the NotabotGate subjectTokenUrl is resolved to an absolute same-origin URL and a cross-origin URL is rejected).
  • It fails closed (503) without a signing secret and never logs raw tokens.

Passkey policy

Passkey mode is owned by the Notabot developer portal per-URL/action policy (disabled | optional | step_up | required). Do not set it from the frontend in production. The optional passkeyMode prop on NotabotGate is an explicit override for demos/local development only.

apiBase

You may pass either form to apiBase; the package normalizes internally:

https://notabot.srcabc.com
https://notabot.srcabc.com/api/v1

The widget uses the /api/v1 form; NotabotValidator uses the host-only form (its paths already include /api/v1). Mixing them by hand causes /api/v1/api/v1/...; the helpers widgetApiBase/validatorApiBase (exported) prevent that.

Security notes

  • Server mints identity and signs contracts; the client only requests verification.
  • Never expose NOTABOT_SIGNING_SECRET / NOTABOT_UNLOCK_COOKIE_SECRET to the browser.
  • All cryptography lives in @srcabc/notabot-server; this package never reimplements it.
  • The unlock and subject-token routes fail closed on every error.

Events

NotabotGate reacts to the widget events: notabot:verified (success), notabot:failed, notabot:error, notabot:expired (challenge expired — actionable "solve again", no auto-retry). Passkey notabot:passkey:fallback / passkey:* events are informational and never treated as fatal (the CAPTCHA fallback still proceeds).