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

@pidgeon-ai/sign-in

v0.2.1

Published

Sign in with Pidgeon for your website: verified AI agents sign up and sign in with no password and no CAPTCHA, and you see who each acts for and what it may do. Next.js and Express.

Readme

@pidgeon-ai/sign-in

Sign in with Pidgeon for your website. Verified AI agents sign up and sign in with no password, no CAPTCHA and no fake human account, and you see who each one is, who it acts for and what it may do. People sign in too, as the one their agents act for.

Pidgeon is a standard OpenID Connect provider. This package is the four routes a site needs, for Next.js and Express.

npm install @pidgeon-ai/sign-in

1. Register your site

Once, with your callback URL:

curl -s https://post.pidgeon.email/api/post/oidc/register \
  -H 'content-type: application/json' \
  -d '{"client_name":"Shop","redirect_uris":["https://shop.example/api/auth/pidgeon/callback"]}'

Keep the client_id it answers.

2. Add the routes

// lib/pidgeon.ts
import { pidgeonSignIn } from '@pidgeon-ai/sign-in';

export const pidgeon = pidgeonSignIn({
  clientId: process.env.PIDGEON_CLIENT_ID!,
  url: 'https://shop.example',
  secret: process.env.PIDGEON_SESSION_SECRET!, // 32+ characters
  onSignIn: async (claims) => {
    // Create or find the account. Key it on claims.iss + claims.sub, never the email.
  },
});

Next.js:

// app/api/auth/pidgeon/[action]/route.ts
import { routeHandlers } from '@pidgeon-ai/sign-in/next';
import { pidgeon } from '@/lib/pidgeon';

export const { GET, POST } = routeHandlers(pidgeon);
// a page
import { headers } from 'next/headers';
import { buttonProps, sessionFrom } from '@pidgeon-ai/sign-in/next';

const session = await sessionFrom(pidgeon, await headers());
// …
<a {...buttonProps(pidgeon)} />;

Express:

import { pidgeonMiddleware, requireSession } from '@pidgeon-ai/sign-in/express';

app.use(pidgeonMiddleware(pidgeon)); // before body parsers
app.get('/account', requireSession(), (req, res) => res.json(req.pidgeon.claims));

A company's own issuer. A company that runs its own Pidgeon issuer (@pidgeon-ai/issuer, inside its own network) signs its agents in itself. To let them in, list it beside Pidgeon's:

pidgeonSignIn({ /* … */ issuers: ['https://post.acme.com'] });

Listing an issuer trusts its keys, found through its own discovery document on its own host, to say who its agents are. Nothing else is ever contacted: a token from an issuer you did not list is refused before anything is fetched, and a discovery document that names another issuer, or keeps its keys on another host, is refused too. People still sign in at issuer. Register with that issuer as you did with Pidgeon, and key accounts on iss + sub.

| Route | Who | What | | -------------------------------- | -------- | ------------------------------------------------------- | | GET /api/auth/pidgeon/login | a person | The button: off to Pidgeon, with PKCE | | GET /api/auth/pidgeon/callback | a person | Back from Pidgeon; sets the session cookie | | GET /api/auth/pidgeon/token | an agent | Your issuers and client_id, so an agent knows the way | | POST /api/auth/pidgeon/token | an agent | Signs the agent in; answers a bearer session |

What you learn

An agent:

{
  "sub": "8f0c…",
  "kind": "agent",
  "email": "[email protected]",
  "operator": { "domain": "acme.com", "verified": true, "shared": false },
  "standing": "established",
  "card": "https://pidgeon.ai/api/v1/cards/8f0c…",
  "grant": {
    "scope": { "name": "bookings.change", "label": "manage bookings" },
    "grantor": { "type": "person", "email": "[email protected]" },
    "subject": { "type": "customer", "id": "42" },
    "exp": 1767225600
  }
}

actsFor(claims) says who it acts for: the grant's grantor, else what its card says. operator.shared is true on a domain anyone can get an address on, and then verified is false.

A person: kind: "person", their verified email, and the grants they gave their agents for your site (never for anywhere else).

How an agent signs in

With no browser and no person present, in two standard steps:

  1. At Pidgeon, an RFC 8693 token exchange signed with the agent's own key (HTTP Message Signatures, Web Bot Auth's profile): its card, your client_id, and its grant. Pidgeon checks the card, the key and the grant, and issues an ID token for your site that names the agent's key (cnf).
  2. At your site, that ID token as an RFC 7523 JWT-bearer grant, signed with the same key. This package checks the signature against the key in the token, so a copied token is useless, and accepts each token once.

An agent built on @pidgeon-ai/id does both in one call:

import { loadIdentity, signInWithPidgeon } from '@pidgeon-ai/id';

const identity = await loadIdentity('acme.com');
const { site } = await signInWithPidgeon(identity, {
  agent: '[email protected]',
  site: 'https://shop.example/api/auth/pidgeon/token',
  grant, // the grant's JWS, when it acts under one
});
// site.access_token is your session for the agent.

An agent whose address is in a Pidgeon account has no signing key of its own, so it signs in with an API key that can send as the address. The key goes only to Pidgeon, never to an issuer a site names:

import { signInWithApiKey } from '@pidgeon-ai/id';

const { site } = await signInWithApiKey({
  agent: '[email protected]',
  apiKey: process.env.PIDGEON_API_KEY!,
  site: 'https://shop.example/api/auth/pidgeon/token',
});

For an API rather than a site, the agent asks for a token that says whom it acts for (delegation: true, with a grant). It is a JWT per RFC 8693: sub the person or organisation that granted, act the agent. Check it against Pidgeon's JWKS, and ask introspection_endpoint (RFC 7662) whether it still holds: one agent's tokens can be revoked without touching the grant.

Or from a terminal: pidgeon id sign-in https://shop.example/api/auth/pidgeon/token --as support-agent.

Several servers

Used ID tokens are remembered in memory. Behind more than one server, pass a shared replay store: { use(jti, expiresAt) } answering false for a jti it has seen.

Apache-2.0.