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

@credninja/oauth

v1.0.0

Published

Zero-dependency OAuth2 client for AI agents. 5 provider adapters, PKCE, Express middleware

Readme

@credninja/oauth

Standalone OAuth2 middleware toolkit for Node.js. Zero runtime dependencies. TypeScript-first. Works standalone with zero cloud dependency.

Seven battle-tested provider adapters: Google, GitHub, Slack, Notion, Salesforce, Linear, HubSpot. Each with provider-specific quirks handled correctly out of the box.


Installation

npm install @credninja/oauth

Requirements: Node.js ≥ 18 (uses built-in fetch and crypto).


5-Minute Quickstart

import { OAuthClient, GoogleAdapter } from '@credninja/oauth';

const google = new OAuthClient({
  adapter: new GoogleAdapter(),
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  redirectUri: 'http://localhost:3000/callback',
});

// Step 1: Generate auth URL (PKCE + CSRF state auto-generated)
const { url, state, codeVerifier } = await google.getAuthorizationUrl({
  scopes: ['calendar.readonly', 'gmail.readonly'],
});

// Store `state` and `codeVerifier` in session, then redirect user to `url`

// Step 2: Handle callback
const tokens = await google.exchangeCode({
  code: req.query.code as string,
  codeVerifier,            // required for PKCE
});

// Step 3: Refresh when expired
const refreshed = await google.refreshToken(tokens.refresh_token!);

// Step 4: Revoke
await google.revokeToken(tokens.access_token);

Provider Reference

Google

import { OAuthClient, GoogleAdapter } from '@credninja/oauth';

const client = new OAuthClient({
  adapter: new GoogleAdapter(),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

// Short scope names are auto-prefixed
const { url } = await client.getAuthorizationUrl({
  scopes: ['calendar.readonly', 'drive', 'gmail.readonly'],
  // Becomes: https://www.googleapis.com/auth/calendar.readonly, etc.
});

Quirks handled:

  • access_type=offline and prompt=consent added automatically (required for refresh tokens)
  • Short scope names prefixed with https://www.googleapis.com/auth/
  • Full PKCE support (S256)

GitHub

import { OAuthClient, GitHubAdapter } from '@credninja/oauth';

const client = new OAuthClient({
  adapter: new GitHubAdapter(),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

const { url } = await client.getAuthorizationUrl({
  scopes: ['repo', 'read:user'],
});

Quirks handled:

  • Accept: application/json header sent automatically (GitHub returns plain text by default)
  • Scopes are comma-separated
  • Token revocation via DELETE /applications/{clientId}/token with Basic auth
  • 404 on revoke treated as success (already revoked)
  • No PKCE (GitHub doesn't support it)

Slack

import { OAuthClient, SlackAdapter } from '@credninja/oauth';

const client = new OAuthClient({
  adapter: new SlackAdapter(),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

const { url } = await client.getAuthorizationUrl({
  scopes: ['channels:read', 'chat:write'],
});

Quirks handled:

  • Scopes are comma-separated
  • authed_user.access_token extracted from nested response (user tokens)
  • Tokens don't expire → refreshToken() throws Error('Slack tokens do not expire')
  • Revocation uses Bearer {token} auth (not client credentials)
  • token_revoked error treated as success on revoke

Notion

import { OAuthClient, NotionAdapter } from '@credninja/oauth';

const client = new OAuthClient({
  adapter: new NotionAdapter(),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

Quirks handled:

  • Token exchange uses Authorization: Basic base64(clientId:clientSecret). client_secret is NOT in the body
  • Notion-Version header sent automatically
  • Tokens don't expire → refreshToken() throws
  • No revocation endpoint → revokeToken() is a no-op
  • No PKCE support

Salesforce

import { OAuthClient, SalesforceAdapter, SALESFORCE_SANDBOX } from '@credninja/oauth';

// Production (default)
const client = new OAuthClient({
  adapter: new SalesforceAdapter(),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

// Sandbox
const sandboxClient = new OAuthClient({
  adapter: new SalesforceAdapter(SALESFORCE_SANDBOX),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

const tokens = await client.exchangeCode({ code: '...' });
// tokens.instance_url → 'https://na1.salesforce.com' (use for API calls)

Quirks handled:

  • instance_url returned in token response (required for Salesforce API calls)
  • Full PKCE support (S256)
  • Sandbox config available via SALESFORCE_SANDBOX

Linear

import { OAuthClient, LinearAdapter } from '@credninja/oauth';

// Default: user actor
const client = new OAuthClient({
  adapter: new LinearAdapter(),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

// Agent/service account mode
const agentClient = new OAuthClient({
  adapter: new LinearAdapter('app'),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

const { url } = await client.getAuthorizationUrl({
  scopes: ['read', 'write', 'issues:create'],
});

Quirks handled:

  • Scopes are comma-separated
  • Full PKCE support (S256)
  • Refresh tokens supported (access tokens expire in 24 hours)
  • actor=app param added for agent/service account flows

HubSpot

import { OAuthClient, HubSpotAdapter } from '@credninja/oauth';

// Standard (any portal)
const client = new OAuthClient({
  adapter: new HubSpotAdapter(),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

// Scoped to a specific HubSpot portal/account
const portalClient = new OAuthClient({
  adapter: new HubSpotAdapter('12345678'),
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
});

const { url } = await client.getAuthorizationUrl({
  scopes: ['crm.objects.contacts.read', 'crm.objects.contacts.write'],
  optionalScope: ['automation', 'content'], // HubSpot-specific: nice-to-have scopes
});

const tokens = await client.exchangeCode({ code: '...' });
// tokens.expires_in: 1800 (30 minutes)

// Refresh before expiry
const refreshed = await client.refreshToken(tokens.refresh_token!);

Quirks handled:

  • Scopes are space-separated
  • No PKCE support
  • Refresh tokens supported (access tokens expire in 30 minutes)
  • Account-specific auth URLs via optional accountId constructor param
  • optional_scope parameter for nice-to-have permissions
  • Revocation via DELETE /oauth/v1/refresh-tokens/:token (not POST)

PKCE

PKCE (Proof Key for Code Exchange, RFC 7636) is generated automatically for adapters that support it (Google, Salesforce, Linear). You can also use the helpers directly:

import { generatePKCE, generateVerifier, computeChallenge } from '@credninja/oauth';

const { verifier, challenge } = generatePKCE();
// verifier: 64-char URL-safe random string
// challenge: base64url(sha256(verifier)), no padding

// Or separately:
const verifier2 = generateVerifier(96);           // 43–128 chars
const challenge2 = computeChallenge(verifier2);   // S256 method

The challenge method is always S256. Plain challenge (code_challenge_method=plain) is not supported.


Express Middleware

Optional import. Keeps your bundle lean if you don't need it:

import express from 'express';
import session from 'express-session';
import { credOAuth } from '@credninja/oauth/express';
import { GoogleAdapter, GitHubAdapter } from '@credninja/oauth';

const app = express();
app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: false }));

app.use('/auth', credOAuth({
  google: {
    adapter: new GoogleAdapter(),
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    scopes: ['calendar.readonly'],
  },
  github: {
    adapter: new GitHubAdapter(),
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    scopes: ['repo'],
  },
}, {
  redirectUri: 'http://localhost:3000/auth/callback',
  onSuccess: (req, res, { provider, tokens }) => {
    req.session.tokens = tokens;
    res.redirect('/dashboard');
  },
  onError: (req, res, error) => {
    res.status(400).json({ error: error.message });
  },
}));

// Routes created automatically:
// GET /auth/google          → redirects to Google consent screen
// GET /auth/google/callback → exchanges code, calls onSuccess
// GET /auth/github          → redirects to GitHub auth
// GET /auth/github/callback → exchanges code, calls onSuccess

State parameter is validated on callback (CSRF protection). PKCE verifier is stored in session and sent automatically.


Custom Adapters

Extend BaseServiceAdapter for any OAuth 2.0 provider:

import { BaseServiceAdapter } from '@credninja/oauth';

export class MyProviderAdapter extends BaseServiceAdapter {
  readonly slug = 'my-provider';
  readonly authorizationUrl = 'https://provider.example.com/oauth/authorize';
  readonly tokenUrl = 'https://provider.example.com/oauth/token';
  readonly revocationUrl = 'https://provider.example.com/oauth/revoke';
  readonly supportsPkce = true;
  readonly supportsRefresh = true;
}

Zero Dependencies

@credninja/oauth has zero runtime dependencies. It uses:

  • fetch: built into Node 18+ (no axios, no node-fetch)
  • crypto: built into Node.js (no jsonwebtoken, no bcrypt)
  • URLSearchParams: built into Node.js

Cred Cloud (Coming Soon)

Need managed token refresh, multi-tenant storage, audit logs, or AI agent delegation?

Managed multi-tenant credential delegation is coming.

Join the waitlist for early access.


License

MIT © CredNinja