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

@plannorium/id

v0.3.0

Published

Browser SDK for Plannorium Identity — popup sign-in with PKCE, origin-bound sessions, and short-lived credentials.

Readme

@plannorium/id

Add "Continue with Plannorium" to a web or mobile app. Plannorium Identity owns login, MFA, consent, account selection and the whole window lifecycle — your app gets a signed result and creates its own session.

Status: pre-1.0. The protocol is still being hardened and may change. Pin an exact version and read the release notes before upgrading.

npm install @plannorium/id

One package, two entry points. Pick the one that matches where your app runs.

| | Web app | React Native / Expo | | --- | --- | --- | | Import | @plannorium/id | @plannorium/id/expo (or /native) | | Class | PlannoriumID | PlannoriumIDNative | | Opens | a popup window | the system browser | | Comes back via | postMessage | a deep link | | You receive | a short-lived credential | an access token (+ refresh) | | Client secret | never | never | | Extra packages | none | expo-web-browser + expo-crypto |

Both use Authorization Code + PKCE (S256). Neither ever puts a client secret in client code, because anything shipped to a browser or an app binary can be read out of it.


"Can I just use a WebView?"

No — and this is the one place worth being blunt, because a WebView looks like the easy answer.

An embedded WebView is controlled by the app hosting it. The app can read everything typed into it, including a password, so the user has no way to tell a genuine login from a convincing imitation — and neither do we. Two more consequences follow from the same fact:

  • It cannot see the browser's existing session, so people sign in again every single time, even when already signed in on that device.
  • Passkeys do not work in it. They are bound to the browser's origin handling, which a WebView does not participate in.

RFC 8252 §8.12 says native apps MUST NOT use embedded user agents for authorization. ASWebAuthenticationSession (iOS) and Custom Tabs (Android) exist precisely so you do not have to: they are the real browser, with the real address bar and the real session, presented as a sheet over your app. @plannorium/id/native uses those, which is also what Google's own mobile sign-in does.


Web: popup sign-in

import { PlannoriumID } from "@plannorium/id";

const identity = new PlannoriumID({
  clientId: "plannorium_...",
  issuer: "https://id.plannorium.com",
});

// Call this DIRECTLY from a click. Browsers only allow a popup that opens in
// the same task as the user's gesture — wrap it in an await first and it is
// silently blocked, even though a real click triggered it.
button.addEventListener("click", async () => {
  try {
    const result = await identity.signIn({
      scopes: ["openid", "profile", "email"],
    });

    await fetch("/api/auth/plannorium", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ credential: result.credential }),
    });
  } catch (err) {
    if (err.code === "popup_blocked") {
      // Ask the user to allow popups, or fall back to a full-page redirect.
    }
  }
});

Script tag / CDN

<script src="https://id.plannorium.com/sdk/v1/plannorium-id.js"></script>
<script>
  const identity = new PlannoriumID({ clientId: "plannorium_..." });
</script>

Exposes window.PlannoriumID (and window.Plannorium.PlannoriumID). Built from the same source as the npm package, so the two never drift apart.


Native: React Native / Expo

npm install @plannorium/id expo-web-browser expo-crypto
import { createPlannoriumID } from "@plannorium/id/expo";

const identity = createPlannoriumID({
  clientId: "plannorium_...",
  redirectUri: "com.yourcompany.app://auth", // must be registered on the client
  issuer: "https://id.plannorium.com",
});

const result = await identity.signIn({ scopes: ["openid", "profile", "email"] });
// result.credential — an ID token. Send it to YOUR backend to verify (below).

That is the whole integration. @plannorium/id/expo wires the three platform adapters to Expo's modules for you.

Wiring it yourself

Use @plannorium/id/native if you are not on Expo. It imports nothing from react-native or expo, so the package stays installable in a plain web project — which means you supply the platform pieces:

import { PlannoriumIDNative } from "@plannorium/id/native";
import * as WebBrowser from "expo-web-browser";
import * as Crypto from "expo-crypto";

const identity = new PlannoriumIDNative({
  clientId: "plannorium_...",
  redirectUri: "com.yourcompany.app://auth",

  openAuthSession: (url, redirectUri) =>
    WebBrowser.openAuthSessionAsync(url, redirectUri),

  // React Native has no Web Crypto. BOTH of these are required there:
  // `sha256` for the PKCE challenge, `getRandomBytes` for the PKCE verifier,
  // `state` and `nonce`. Without the second, signIn() throws before the
  // browser ever opens.
  getRandomBytes: Crypto.getRandomBytes,

  sha256: async (value) => {
    const b64 = await Crypto.digestStringAsync(
      Crypto.CryptoDigestAlgorithm.SHA256,
      value,
      { encoding: Crypto.CryptoEncoding.BASE64 }
    );
    return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  },
});

Any browser library works — react-native-app-auth, expo-auth-session, or a bare Linking listener — as long as openAuthSession resolves with { type: "success", url } on the redirect.

Deep link setup

app.json (Expo):

{ "expo": { "scheme": "com.yourcompany.app" } }

Then register com.yourcompany.app://auth as a redirect URI on your client. A claimed HTTPS link (https://app.example.com/auth) is stronger than a custom scheme, since another app can register the same scheme — prefer it in production, and keep the custom scheme for development.


Account switching

Turn on the Google-style account picker per call. Nothing is forced on apps that do not want it.

// Web and native both take `prompt`.
await identity.signIn({ prompt: "select_account" });

// Native has a shorthand.
await identity.switchAccount();

| prompt | Behaviour | | --- | --- | | (omitted) | Uses the current session; consent screen only if needed | | select_account | Shows the account picker, even when signed in | | login | Forces re-authentication | | consent | Always shows the consent screen | | none | Never shows UI; returns login_required / interaction_required |

login_hint pre-fills the address when you already know it.

Account settings (native)

Send people to the web for account management — do not rebuild these screens in your app.

await identity.openAccountPage("security"); // opens the system browser
identity.accountUrls();
// { settings, profile, security, privacy, switchAccount }

Same reasoning as sign-in: a settings screen rendered inside your app is indistinguishable from one that harvests credentials. The system browser already holds the user's session, so they arrive signed in.


Verify on your server — required

signIn() returns a signed credential, not a session. Never trust profile data that only client code has seen: anything the client can read, the client can fabricate.

credential is always an ID token — RS256, signed with the key published at /.well-known/jwks.json, with aud set to your client ID. It is never an access token, on either entry point. The native SDK also checks the token's nonce against the one it generated before returning, so the token you receive answers the request you made.

// On your server
import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(
  new URL("https://id.plannorium.com/.well-known/jwks.json")
);

const { payload } = await jwtVerify(credential, jwks, {
  issuer: "https://id.plannorium.com",
  audience: process.env.PLANNORIUM_CLIENT_ID,
});

// payload.sub is the stable user id. Map it to your own user, then issue
// your own session cookie.

The web SDK never returns a refresh token to browser code and never writes the credential to localStorage.


Registering your client

In the developer console, create a client and choose the application type:

| Type | For | Secret | | --- | --- | --- | | Browser SPA | web apps using the popup | none — PKCE only | | Native App | React Native / Expo | none — PKCE only | | Web Server | server-side OAuth | yes, server-side only |

Then register, exactly:

  • Allowed origins (browser) — scheme + host + port, e.g. https://app.example.com
  • Redirect URIs (native) — your deep link, e.g. com.yourcompany.app://auth

Matching is exact. http:// is accepted only for localhost and 127.0.0.1.


API

PlannoriumID (web)

| Method | Description | | --- | --- | | signIn(options?) | Opens the popup, resolves with a short-lived credential | | cancel() | Cancels an in-flight attempt | | renderButton(el, options?) | Renders a branded sign-in button | | logout(options?) | Navigates to the hosted end-session endpoint |

createPlannoriumID (Expo)

Returns a PlannoriumIDNative with openAuthSession, sha256 and getRandomBytes already wired to Expo. Any of them can still be overridden.

PlannoriumIDNative

| Method | Description | | --- | --- | | signIn(options?) | Opens the system browser, resolves with tokens | | switchAccount(options?) | signIn with prompt: "select_account" | | openAccountPage(page?) | Opens an account page in the system browser | | accountUrls() | The account page URLs | | logoutUrl(returnTo?) | End-session URL to open |

Options

{
  scopes?: string[];          // default ["openid", "profile", "email"]
  prompt?: "none" | "login" | "consent" | "select_account";
  loginHint?: string;
}

Results

// Web
{ credential: string; expiresAt: number; scopes: string[]; sessionState?: string }

// Native
{ accessToken: string; refreshToken?: string; idToken?: string;
  tokenType: string; credential: string; expiresAt: number; scopes: string[] }

Errors

Both classes reject with a PlannoriumSDKError carrying a stable code:

| Code | Meaning | | --- | --- | | popup_blocked | Browser blocked the popup — invoke signIn() directly from a user gesture | | popup_closed | Window closed before authentication completed | | canceled | Authentication was canceled or dismissed | | invalid_origin | The calling origin is not registered on the client | | invalid_client | Invalid or unregistered client ID | | network_error | Unable to connect to Plannorium Identity | | attempt_in_progress | An authentication request is already in progress | | missing_crypto | No secure randomness or SHA-256 — pass getRandomBytes / sha256, or use @plannorium/id/expo | | invalid_config | A required constructor option is missing or the wrong type | | state_mismatch | The response did not match the request that was sent | | nonce_mismatch | The ID token's nonce is not the one this request generated | | missing_id_token | No id_token in the token response — request the openid scope | | invalid_response | The response was malformed, or carried no authorization code | | exchange_failed | The token endpoint refused the authorization code |


Requirements

  • HTTPS in production; http:// only for localhost / 127.0.0.1
  • Registered origins (web) or redirect URIs (native), matched exactly
  • S256 PKCE — mandatory, handled for you
  • Node 18+ for the server-side verification example

License

MIT