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

@corbits/oauth-core

v0.3.0

Published

Provider-agnostic OAuth 2.0 authorization-code + PKCE login for desktop/CLI hosts.

Readme

@corbits/oauth-core

OAuth 2.0 authorization-code login with PKCE and a loopback callback, in the Corbits auth & credentials bucket: it mints the tokens an Interchange hub (the multi-tenant control plane that holds tenants, principals and credentials) stores as oauth_token credentials. It also works standalone in any CLI or desktop host, with token refresh and MCP server discovery.

Why @corbits/oauth-core?

  1. Provider-agnostic. A provider is a config plus an exchange function the host supplies. The package never names or depends on one.
  2. One call to sign in. loginWithProvider binds the provider's registered redirect URI, opens the browser, exchanges the code, and saves the profile.
  3. Tokens stay fresh. The token session refreshes ahead of expiry and coalesces concurrent refreshes; on a hub, a background refresher renews stored credentials under a row lock.
  4. Hub-ready. The /hub entry mounts login routes on a hub tenant router, gated by the host's grants (a principal's permission on a resource), and writes tokens through the host's CredentialCipher.

Install

bun add @corbits/oauth-core

This installs @intx/db, @intx/hub-api, @intx/hub-common, @intx/types, drizzle-orm and hono as dependencies. Only the @corbits/oauth-core/hub entry imports them, so browser and CLI bundles built from the root entry do not include them.

Quickstart

import {
  baseTokensFromResponse,
  exchangeCode,
  loginWithProvider,
  openInBrowser,
} from "@corbits/oauth-core";

const oauthConfig = {
  clientId: "my-cli",
  authorizeUrl: "https://auth.example.com/authorize",
  tokenUrl: "https://auth.example.com/token",
  redirectUri: "http://127.0.0.1:1455/callback",
  scopes: ["openid", "offline_access"],
  tokenTimeoutMs: 10_000,
};

const profile = await loginWithProvider(
  {
    oauthConfig,
    exchange: async (code, verifier, now) =>
      baseTokensFromResponse(
        await exchangeCode(oauthConfig, code, verifier),
        now,
        undefined,
      ),
  },
  {
    profile: "default",
    signal: AbortSignal.timeout(120_000),
    openInBrowser,
    save: async (saved) => console.log(`saved ${saved.name}`),
  },
);
console.log(profile.tokens.expiresAt);

The callback server binds only loopback addresses (127.0.0.0/8 or ::1) on the port in redirectUri, which must match the client registration exactly.

Where it fits

  • CLI or desktop host: the root entry. The host keeps profiles in its own store (an OS keychain, an encrypted file).
  • Interchange hub (@intx/hub-api, @intx/db, @intx/types): the /hub entry mounts login routes and refreshes oauth_token credentials that an InferenceSource names by credentialId.
  • Providers: a provider package or the host itself supplies an OAuthLoginProvider.
  • MCP servers: discoverMcpLoginEntry builds the OAuthClientConfig for servers that publish their authorization server instead of a fixed client.

Reference

Root entry (@corbits/oauth-core)

| Export | Purpose | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | loginWithProvider(provider, opts) | Sign in with a provider and save the profile. | | startOAuthLogin(opts, deps) | Lower-level login; returns the authorize URL and a staged profile to commit(). Use it for custom callback pages. | | createTokenSession(deps) | getValidToken(profile) refreshes ahead of expiry and coalesces concurrent refreshes. | | startCallbackServer(state, config) | The loopback callback listener. | | buildAuthorizeUrl, exchangeCode, refreshTokenRequest, baseTokensFromResponse | Token endpoint helpers. | | callbackTargetFor(config) | The host, port and path a redirectUri binds. | | discoverMcpLoginEntry, registerMcpClient, mcpClientConfig, selectMcpScopes | MCP authorization discovery and dynamic client registration. | | generatePkce, generateState, openInBrowser | Primitives. |

Hub entry (@corbits/oauth-core/hub)

| Export | Purpose | | --------------------------------- | --------------------------------------------------------- | | mountOAuthLogin(app, opts) | Adds the login routes below to a tenant router. | | createOAuthTokenRefresher(opts) | Background refresher with start() and stop(). | | persistOAuthCredential | Writes tokens into an encrypted oauth_token credential. |

| Route | | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | GET /oauth-logins/providers | Provider names the host offers. | | POST /oauth-logins | Start a login from provider or an MCP resourceUrl (plus providerId, credentialName); returns the authorize URL and a login id. | | GET /oauth/callback | The redirect target of resource-URL logins (${baseUrl}/oauth/callback); needs no grant. | | GET /oauth-logins/:loginId | Poll for completion; returns the credential id when done. | | DELETE /oauth-logins/:loginId | Cancel. |

A resource-URL login discovers the server's authorization server, registers a client and stores its client id, token URL and resource on the credential, so the refresher renews it without a registry entry. It needs the mount's callbackUrl and store options (below). The PKCE verifier never leaves the hub process.

What the callback route relies on: PKCE S256 is required of the server (discovery refuses one that does not advertise it); the state is 32 random bytes, single-use and bound to the tenant and principal that started the login; the RFC 8707 resource goes on the authorize, exchange and refresh requests; when the server advertises RFC 9207 the redirect's iss must match the discovered issuer; the page is served no-store / no-referrer; and callbackUrl must be https unless it is loopback (localhost, 127.x, ::1), which is what local development uses.

Using with Interchange

Mount the routes under the tenant prefix, gate them with the host's grant middleware (a grant is a principal's permission on a resource), and run the refresher:

import {
  createOAuthTokenRefresher,
  mountOAuthLogin,
  type OAuthLoginProviders,
  type OAuthTokenRefresher,
} from "@corbits/oauth-core/hub";
import type { DB } from "@intx/db";
import type { TenantEnv } from "@intx/hub-api";
import type { CredentialCipher } from "@intx/types";
import { Hono, type MiddlewareHandler } from "hono";

export function installOAuthLogin(
  app: Hono<TenantEnv>,
  opts: {
    db: DB["db"];
    cipher: CredentialCipher;
    requireGrant: MiddlewareHandler<TenantEnv>;
    providers: OAuthLoginProviders;
    onRefreshed: (context: { tenantId: string; credentialId: string }) => void;
  },
): OAuthTokenRefresher {
  const oauthLoginApi = new Hono<TenantEnv>();
  mountOAuthLogin(oauthLoginApi, {
    db: opts.db,
    cipher: opts.cipher,
    requireGrant: opts.requireGrant,
    providers: opts.providers,
  });
  app.route("/api/tenants/:tenantId", oauthLoginApi);

  const refresher = createOAuthTokenRefresher({
    db: opts.db,
    cipher: opts.cipher,
    providers: opts.providers,
    onRefreshed: opts.onRefreshed,
  });
  refresher.start();
  return refresher;
}

The refresher only updates the credential row. A process that loaded the old token into memory, such as a running sidecar (the agent runtime), keeps it until told to reload, so notify those processes from onRefreshed.

The hub entry will move to a separate @corbits/oauth-hub package in a later release.

Resource-URL logins

The redirect lands on a hub-root route outside any tenant, so mount it beside the tenant router and share one store:

const store = createOAuthLoginStore();
mountOAuthCallback(app, { store, path: "/api/oauth/callback" }); // unauthenticated
mountOAuthLogin(oauthLoginApi, {
  // ...db, cipher, requireGrant, providers
  store,
  callbackUrl: "https://hub.example.com/api/oauth/callback",
});

callbackUrl is the absolute public URL of that route; it is registered verbatim as the redirect_uri.

Upgrading from 0.1

  • OAuthLoginProvider and callbackTargetFor import from @corbits/oauth-core, not @corbits/oauth-core/hub.
  • CallbackServer.port is required. A custom startCallbackServer must return the bound port.
  • Stored profiles and oauth_token credentials are unchanged and keep working.

License

LGPL-2.1-only.