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

@runablehq/managed-auth

v0.6.0

Published

Runable managed auth — client plugins for web, Expo/React Native, and Electron apps, server plugin for generated apps, broker plugin for the Runable API

Readme

@runablehq/managed-auth

Turnkey Google / Apple / Microsoft sign-in for apps built on Runable, on every app surface: web, Expo/React Native, and Electron. Runable owns the upstream provider OAuth apps and the fixed provider callback, authenticates the end user, and hands the app a short, aud-scoped identity JWT signed by the broker. The app verifies that JWT offline against the broker's JWKS and mints its own Better Auth session — so the app keeps its session, revocation, and lifetime, while never touching provider credentials.

Generated apps configure nothing auth-provider-related: no client secret, no redirect-URI registration, no genericOAuth. Just the app id and broker issuer Runable already injects.

Env contract

Runable injects these into the app:

# root .env
APPLICATION_ID=             # this app's id; the JWT's `aud` is bound to it (server reads this)
VITE_RUNABLE_AUTH_ISSUER=   # broker base, e.g. https://api.runable.com/api/auth (server + web client)
VITE_APPLICATION_ID=        # browser copy of APPLICATION_ID for the web client

The mobile package's app.json carries the same values under platform-managed expo.extra: applicationId, runableAuthIssuer, and apiUrl (the app server the auth client targets). expo.scheme is also platform-managed (runable-<applicationId>) — it is the deep-link protocol the broker validates for standalone mobile builds and the desktop app.

issuer is required everywhere: there is no default broker, so a staging app can never silently call production.

Web (/client)

import { createAuthClient } from "better-auth/react";
import { managedAuthClient } from "@runablehq/managed-auth/client";

export const authClient = createAuthClient({
  baseURL: window.location.origin,
  basePath: "/api/auth",
  plugins: [
    managedAuthClient({
      applicationId: import.meta.env.VITE_APPLICATION_ID,
      issuer: import.meta.env.VITE_RUNABLE_AUTH_ISSUER,
    }),
  ],
});

// once, on boot — completes a returning redirect/popup:
await authClient.managedAuth.handleRedirect();

// a sign-in button — useSession() updates when it resolves:
authClient.managedAuth.signIn({ provider: "google" }); // "google" | "apple" | "microsoft"

signIn opens a first-party popup inside the cross-origin preview iframe and a top-level redirect otherwise. The plugin attaches the app's bearer session to every Better Auth request and exchanges through the client's own baseURL/basePath.

Expo / React Native (/native)

Peers (all bundled in Expo Go): expo-web-browser, expo-secure-store, expo-linking, expo-crypto.

import { createAuthClient } from "better-auth/react";
import { managedAuthExpoClient } from "@runablehq/managed-auth/native";
import Constants from "expo-constants";

const extra = Constants.expoConfig?.extra ?? {};

export const authClient = createAuthClient({
  baseURL: extra.apiUrl, // the app's server — the exchange and session ride through it
  basePath: "/api/auth",
  plugins: [
    managedAuthExpoClient({
      applicationId: extra.applicationId,
      issuer: extra.runableAuthIssuer,
    }),
  ],
});

On native, signIn runs the whole flow in a system auth session (openAuthSessionAsync): the broker 302s to Linking.createURL("auth/callback")exp://…/--/auth/callback in Expo Go, the app scheme in standalone builds — and the promise resolves signed-in, with the bearer stored in SecureStore. Ship a route at the redirect path (e.g. app/auth/callback.tsx) so the router has somewhere to land. On Expo Web the plugin delegates to the web transport, so call handleRedirect() once on boot (a no-op on native). The native/desktop legs are PKCE-bound: an intercepted deep link can't be redeemed without the in-memory verifier.

Electron (/desktop/main, /desktop/preload)

The renderer signs in through the system browser (Google blocks OAuth in embedded user agents) and completes over the app's custom protocol. The renderer side is just managedAuthClient — it detects the Electron bridge itself — while the main and preload halves ship as one entry per process because they can't share a bundle (the web bundle must build without electron; the main half imports electron and node:http). The entries register mechanism only — app control flow (single-instance lock, quitting, window creation, what gets exposed to the page) stays in app code:

// main process — registers runable-<applicationId>, delivers macOS open-url + argv deep links
// to the renderer (buffered until the first window loads), registers the managed-auth IPC
// surface, and runs the dev loopback relay in unpackaged builds.
import { createManagedDeepLinks } from "@runablehq/managed-auth/desktop/main";

const deepLinks = createManagedDeepLinks({
  applicationId: process.env.APPLICATION_ID,
  getWindow: () => win,
});

// Windows/Linux deliver deep links as argv — of a second instance while running, of this
// instance on cold start. Keep one instance and forward both.
if (app.requestSingleInstanceLock()) {
  app.on("second-instance", (_event, argv) => deepLinks.handleArgv(argv));
  app.whenReady().then(() => {
    createWindow();
    deepLinks.handleArgv(process.argv);
  });
} else {
  app.quit();
}
// preload — the app exposes the bridge itself (must be bundled in; preloads are sandboxed)
import { contextBridge } from "electron";
import { createManagedAuthBridge } from "@runablehq/managed-auth/desktop/preload";

// window.managedAuth — where managedAuthClient looks for the Electron bridge
contextBridge.exposeInMainWorld("managedAuth", createManagedAuthBridge());
// renderer — the regular web plugin detects window.managedAuth and switches to the
// system-browser + deep-link flow on its own; no desktop-specific client code needed.
import { createAuthClient } from "better-auth/react";
import { managedAuthClient } from "@runablehq/managed-auth/client";

export const authClient = createAuthClient({
  baseURL: import.meta.env.VITE_WEBSITE_URL, // packaged apps load from file:// — never the page origin
  basePath: "/api/auth",
  plugins: [
    managedAuthClient({
      applicationId: import.meta.env.VITE_APPLICATION_ID,
      issuer: import.meta.env.VITE_RUNABLE_AUTH_ISSUER,
    }),
  ],
});

The deep-link stream is not auth-specific: every URL on the app's scheme reaches window.managedAuth.onDeepLink, so app features can build on it (the bridge methods are plain functions — re-expose them under an app-owned key if preferred).

Server (/server)

Spread the plugins into the app's own Better Auth — it keeps email/password, the database, etc.:

import { betterAuth } from "better-auth";
import { runableManagedAuth } from "@runablehq/managed-auth/server";

export const auth = betterAuth({
  basePath: "/api/auth",
  baseURL: process.env.WEBSITE_URL,
  // ...your database, emailAndPassword, etc.
  plugins: [
    ...runableManagedAuth({
      applicationId: process.env.APPLICATION_ID!,
      issuer: process.env.VITE_RUNABLE_AUTH_ISSUER!,
    }),
    // add framework plugins (e.g. expo()) here
  ],
});

runableManagedAuth returns bearer() plus a plugin that adds POST /api/auth/managed/exchange, which verifies the broker JWT (offline, via JWKS, aud = APPLICATION_ID, PKCE when the token is challenge-bound) and mints the app's bearer-only session. The clients call it for you — no route wiring needed. Install the peers: Better Auth (>=1.6.19) and Zod (>=4).

Broker (/broker) — Runable internal

Mounted by the Runable API (not by apps): createManagedAuthStart(...) exposes /start (validate the redirect target against the application record, then drive the provider), and managedAuthBrokerPlugin(...) is a Better Auth plugin that mints the identity JWT inside the authenticated social callback and delivers it to the validated target in the URL fragment — origin-rooted for https, full-path for custom-scheme deep links. No per-app OAuth client and no separate /finish.

Versions & debugging

CHANGELOG.md documents every published version with breaking changes and agent migration steps. When a generated app's auth code doesn't match these docs, check the app's installed version first.

Playground

playground/ is a self-contained harness covering every client surface, against a broker serving /api/auth/managed/* (RUNABLE_AUTH_ISSUER in playground/.env — e.g. a local Runable API):

  • bun run playground — the app server plus both web contexts: top-level redirect at :4321, popup-in-iframe preview at :4322.
  • bun run playground:desktop — an Electron shell (mirrors the template's packages/desktop) built on createManagedDeepLinks + createManagedAuthBridge; managedAuthClient detects window.managedAuth and switches to the system-browser flow, returning via the runable-<APPLICATION_ID> deep link (or the dev loopback relay). A localhost broker accepts that scheme without a DB-backed application. Run bun run build first — the shell consumes the built package.
  • bun run playground:mobile — an Expo Go app (mirrors the template's packages/mobile) using managedAuthExpoClient; copy playground/mobile/.env.template to .env first. Simulators reach the app server on localhost; physical devices need the dev machine's LAN IP in both URLs.

All three mint sessions on the same in-memory app server (playground/server.ts).