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

@soorya-u/better-auth-desktop

v0.1.2

Published

Loopback-based desktop auth for Better Auth. Framework-agnostic server + core with thin Electrobun and Electron adapters; OAuth hand-off via a 127.0.0.1 loopback navigation (no custom URL scheme).

Readme

@soorya-u/better-auth-desktop

Loopback-based desktop authentication for Better Auth.

OAuth completes in the system browser and the resulting session is handed back to the desktop app through a 127.0.0.1 loopback the app briefly listens on — reached by a top-level browser navigation, not a custom URL scheme. That works on macOS, Linux, and Windows with no OS scheme registration, and avoids the CORS / Private-Network-Access / mixed-content problems a fetch-based hand-off would hit.

A framework-agnostic server plugin and core drive the flow; thin adapters bind it to a runtime. Electrobun ships today; Electron is a drop-in (it never loads any Electrobun code).

How it works

WebView (renderer)        Desktop main process            Better Auth server
   │ requestAuth(github) ─────▶│                                  │
   │                           │ bind 127.0.0.1:<port>            │
   │                           │ openExternal(                    │
   │                           │   /desktop/init-oauth-proxy       │
   │                           │   ?provider&pkce&callbackURL=http://127.0.0.1:<port>/callback?nonce=N)
   │                           │                                  │
 [ system browser ] ── OAuth ──▶ provider ──▶ server callback ──▶ session established
   │                           │                       /desktop/oauth-complete:
   │                           │                         mint one-time code; 302 →
   │                           │                         http://127.0.0.1:<port>/callback?nonce=N&token=T
 [ system browser ] ── navigates to 127.0.0.1:<port>/callback?token=T&nonce=N
   │                           │ loopback handler:                │
   │                           │  verify nonce                    │
   │                           │  exchange T ────────────────────▶│ /desktop/token (PKCE verify)
   │                           │  ◀── session ────────────────────│
   │                           │  store session (keychain)        │
   │◀── onAuthenticated ───────┤  "you can close this tab"        │
   │  navigate("/app")         │  close loopback                  │

The one-time code in the loopback URL is single-use and PKCE-bound (the verifier never leaves the desktop), the listener binds 127.0.0.1 only, the request must carry the matching nonce, and the loopback closes after success or a timeout. Works in dev (with oAuthProxy bouncing through /oauth-proxy-callback) and prod (direct callback) alike.

Installation

bun add better-auth @soorya-u/better-auth-desktop

Server

Add the plugin to your Better Auth server. Compose it with oAuthProxy if your desktop/web origins differ from the OAuth-registered callback origin.

// server/auth.ts
import { betterAuth } from "better-auth";
import { betterAuthDesktop } from "@soorya-u/better-auth-desktop/server";
import { oAuthProxy } from "better-auth/plugins";

export const auth = betterAuth({
  socialProviders: {
    github: {
      clientId: process.env.OAUTH_GITHUB_CLIENT_ID!,
      clientSecret: process.env.OAUTH_GITHUB_CLIENT_SECRET!,
    },
  },
  plugins: [
    betterAuthDesktop({ clientID: "my-desktop-app" }),
    oAuthProxy(),
  ],
});

Server options

| Option | Default | Description | | --- | --- | --- | | clientID | "desktop" | Must match the desktop adapter's clientID. | | codeExpiresIn | 300 | One-time-code TTL (seconds). | | hashKey | "token" | Name of the one-time-code parameter. | | webCallbackUrl | — | Optional branded callback page (see below). Omit for the default direct-to-loopback redirect. | | allowedLoopbackPorts | — | Optional hardening: number[] or { min, max }. Restricts which loopback port the server will redirect to. | | disableOriginOverride | false | Disable rewriting the request origin from the desktop-origin header. |

By default the server redirects the browser straight to the desktop loopback. No web callback page is involved.

Electrobun (desktop)

electrobunDesktop is a standard Better Auth client plugin. Use it with createAuthClient in the Bun main process, pass createBunRPC() to your BrowserWindow, and call setupMain().

storage is required — pass keychainStorage() (Bun.secrets) or your own keychain-backed Storage. The package never picks a default, so it can't silently land on a runtime-inappropriate one.

// desktop/bun/auth.ts
import { createAuthClient } from "better-auth/client";
import {
  electrobunDesktop,
  keychainStorage,
} from "@soorya-u/better-auth-desktop/electrobun";

export const authClient = createAuthClient({
  baseURL: process.env.SERVER_URL!,
  plugins: [
    electrobunDesktop({
      clientID: "my-desktop-app",
      storage: await keychainStorage(),
      // loopbackPort: 51789,  // optional; omit to bind 127.0.0.1:0 (OS-assigned)
      // onStorageError: (err) => console.error("Keychain write failed", err),
    }),
  ],
});

// Bun-side RPC the WebView calls; pass to `new BrowserWindow({ rpc })`.
export const authBunRpc = authClient.createBunRPC();
// desktop/bun/index.ts
import { BrowserWindow } from "electrobun/bun";
import { authClient, authBunRpc } from "./auth";

new BrowserWindow({ title: "My App", url, rpc: authBunRpc });
await authClient.setupMain();

Renderer (WebView)

// renderer entry
import { defineAuthWebviewRPC } from "@soorya-u/better-auth-desktop/rpc/webview";

export const auth = defineAuthWebviewRPC();

auth.onAuthenticated((user) => navigate("/app"));
await auth.requestAuth({ provider: "github" });

The bridge exposes: requestAuth (open browser flow), getAuthUrl (return the URL without opening a browser — useful for copy-link flows), getUser, getSession, signOut, getUserImage, watchUser (reactive subscription to the current user, race-safe), destroy (remove all listeners on unmount / hot-reload), and the onAuthenticated / onUserUpdated / onAuthError event subscriptions.

No urlSchemes / protocol registration is needed in electrobun.config.ts.

Electron (desktop)

The Electron adapter shares the same core; it pulls in no Electrobun code. storage is required. The package ships electronStorage() — persistent via electron-store (install it: bun add electron-store) with values encrypted by Electron safeStorage (OS keychain) when available — or you can supply your own Storage.

// main process
import { createAuthClient } from "better-auth/client";
import {
  electronDesktop,
  electronStorage,
} from "@soorya-u/better-auth-desktop/electron";

const authClient = createAuthClient({
  baseURL: process.env.SERVER_URL!,
  plugins: [
    electronDesktop({
      clientID: "my-desktop-app",
      getWindow: () => mainWindow,
      storage: await electronStorage(),
    }),
  ],
});
await authClient.setupMain();

The renderer talks to it over IPC; the channel names are exported as ELECTRON_AUTH_CHANNELS from @soorya-u/better-auth-desktop/electron.

Optional: branded web callback page

If you set webCallbackUrl on the server plugin, oauth-complete redirects the browser to your page (#token=…&loopback=…) instead of straight to the loopback. Your page calls forwardToDesktop(), which performs the top-level navigation to 127.0.0.1 — still no CORS / PNA / mixed-content.

It's available both as a standalone function and as a Better Auth client plugin:

// standalone
import { forwardToDesktop } from "@soorya-u/better-auth-desktop/web";
forwardToDesktop(); // safe no-op outside a desktop sign-in

// or as a plugin
import { webDesktop } from "@soorya-u/better-auth-desktop/web";
const authClient = createAuthClient({ plugins: [webDesktop()] });
authClient.forwardToDesktop();

Wrapping an auth client (renderer / web)

wrapForDesktop proxy-wraps a better-auth client so that getSession, signOut, and signIn.social delegate to the desktop bridge instead of hitting the network directly. Pass null as the bridge to get the original client back unchanged (useful during SSR / before the bridge is initialized).

import { wrapForDesktop } from "@soorya-u/better-auth-desktop/web";

// wrap once, use everywhere like a normal better-auth client
const authClient = wrapForDesktop(baseClient, bridge);
await authClient.signIn.social({ provider: "github" }); // → opens loopback flow
await authClient.signOut();                               // → bridge.signOut()

To also intercept useSession, pass a framework hook as the third argument:

import { useSession } from "@soorya-u/better-auth-desktop/react"; // React
const authClient = wrapForDesktop(baseClient, bridge, useSession);

Or supply your own hook for Vue / Svelte — type it with UseSessionFn from @soorya-u/better-auth-desktop/web.

Customizing the loopback success page

By default the loopback serves a minimal "you can close this tab" page. Override it via loopbackSuccess on the desktop plugin — a string is used as the HTML body, or { redirectTo } bounces the browser to your own page:

electrobunDesktop({
  storage: await keychainStorage(),
  loopbackSuccess: { redirectTo: "https://app.example.com/signed-in" },
});

Custom adapters

@soorya-u/better-auth-desktop/core exposes the framework-agnostic pieces — the DesktopAdapter interface, startAuthFlow, exchangeToken, the loopback helpers, and desktopClient — so you can target another runtime. An adapter only has to:

type DesktopAdapter = {
  openExternal(url: string): void | Promise<void>;
  serveLoopback(
    onRequest: (req: LoopbackRequest) => Promise<LoopbackResponse>,
    opts?: { port?: number },
  ): Promise<{ port: number; close(): void }>;
  notifyRenderer(event: AuthEvent): void;
  storage: Storage;
};

Exports

| Subpath | Purpose | | --- | --- | | @soorya-u/better-auth-desktop | re-exports core (types & utilities) | | @soorya-u/better-auth-desktop/server | betterAuthDesktop() server plugin | | @soorya-u/better-auth-desktop/electrobun | electrobunDesktop() plugin + keychainStorage() (Bun main process) | | @soorya-u/better-auth-desktop/rpc/webview | defineAuthWebviewRPC() (Electrobun renderer) | | @soorya-u/better-auth-desktop/electron | electronDesktop() plugin + electronStorage() (Electron main process) | | @soorya-u/better-auth-desktop/web | wrapForDesktop(), forwardToDesktop(), webDesktop() plugin | | @soorya-u/better-auth-desktop/react | useSession() React hook for the desktop bridge | | @soorya-u/better-auth-desktop/client | desktopClient() Better Auth client plugin | | @soorya-u/better-auth-desktop/core | shared types & utilities for custom adapters |

Security notes

  • The loopback binds 127.0.0.1 only and is unreachable off-box.
  • The token in the loopback URL is a one-time, short-TTL, PKCE-bound code; the verifier never leaves the desktop, so interception/replay is useless.
  • The nonce stops a different local app from consuming the redirect.
  • The loopback closes immediately after a successful exchange (or on timeout).

License

MIT