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

@nihplod/sso-sdk

v1.0.1

Published

NIHPLOD SSO 子项目接入 SDK — OAuth 2.0 授权码 + PKCE 客户端封装

Readme

@nihplod/sso-sdk

NIHPLOD SSO client SDK — OAuth 2.0 Authorization Code + PKCE wrapper for sub-projects.

Install

npm install @nihplod/sso-sdk

Quick Start

import { SsoClient } from "@nihplod/sso-sdk";

const sso = new SsoClient({
  clientId: "your-client-id",
  redirectUri: "https://yourapp.com/callback",
  ssoBaseUrl: "https://nihplod.cn",
});

// 1. Initiate login
await sso.login();

// 2. Handle callback (inside the callback page)
const token = await sso.handleCallback(window.location.href);

// 3. Get user info
const user = await sso.getUserInfo();

// 4. Logout
await sso.logout();

API Reference

new SsoClient(config)

Create an SSO client instance.

| Parameter | Type | Required | Description | |------|------|------|------| | clientId | string | ✅ | OAuth Client ID | | redirectUri | string | ✅ | Callback URL, must match the one registered | | ssoBaseUrl | string | ✅ | SSO provider base URL, e.g. https://nihplod.cn | | scopes | string | ❌ | Space-separated scopes, default "openid profile" | | clientSecret | string | ❌ | Only for Confidential Clients. Do NOT pass this in browser SPA (Public Client) to avoid leaking secrets. BFF / Next.js Route Handlers may pass it. |

sso.login(returnUrl?)

Initiate SSO login. Generates PKCE parameters and redirects to the SSO login page.

| Parameter | Type | Description | |------|------|------| | returnUrl | string | Optional URL to return to after login |

sso.getLoginUrl(returnUrl?)

Build the login URL string without redirecting. Returns Promise<string>.

sso.handleCallback(callbackUrl)

Handle the OAuth callback. Parses code and state from the URL, validates state, and exchanges the code for tokens.

| Parameter | Type | Description | |------|------|------| | callbackUrl | string | Full callback URL (window.location.href) |

Returns Promise<TokenData>.

sso.refreshToken()

Refresh the access_token using the refresh_token. Uses a mutex to prevent concurrent refresh requests.

Returns Promise<TokenData>.

sso.getUserInfo()

Fetch current user info. Automatically refreshes the access_token if expired.

Returns Promise<SsoUser>.

interface SsoUser {
  sub: string;
  nickname?: string;
  avatar?: string;
  phone?: string;          // Masked phone number
  membership_level?: string;
  total_points?: number;
}

sso.getAccessToken()

Get the current valid access_token. Automatically refreshes if expired.

Returns Promise<string | null>.

sso.isAuthenticated()

Check whether the user is authenticated (local check only, no network request).

Returns boolean.

sso.logout(redirectToSso?)

Clear local token data and attempt to revoke the server-side refresh_token.

| Parameter | Type | Default | Description | |------|------|------|------| | redirectToSso | boolean | false | Whether to redirect to the SSO logout page (OIDC RP-Initiated Logout) |

When redirectToSso=true, the user is redirected to /logout?client_id=...&post_logout_redirect_uri=.... The main site clears the session and then returns to the sub-project callback address.

sso.getDiscovery()

Fetch the OIDC Discovery document.

Returns Promise<OidcDiscovery>.


TypeScript Types

// SsoClient config
interface SsoClientConfig {
  clientId: string;
  redirectUri: string;
  ssoBaseUrl: string;
  scopes?: string;
  clientSecret?: string; // Only for Confidential Clients
}

// Token data
interface TokenData {
  access_token: string;
  token_type: string;
  expires_in: number;
  refresh_token: string;
  id_token?: string;
  issued_at: number;
  expires_at: number;
}

// User info
interface SsoUser {
  sub: string;
  nickname?: string;
  avatar?: string;
  phone?: string;
  membership_level?: string;
  total_points?: number;
}

// OIDC Discovery
interface OidcDiscovery {
  issuer: string;
  authorization_endpoint: string;
  token_endpoint: string;
  userinfo_endpoint: string;
  jwks_uri: string;
  introspection_endpoint: string;
  revocation_endpoint?: string;
  end_session_endpoint?: string;
  scopes_supported: string[];
  response_types_supported: string[];
  grant_types_supported: string[];
  code_challenge_methods_supported: string[];
}

React Bindings

<SsoProvider>

import { SsoProvider } from "@nihplod/sso-sdk/react";

<SsoProvider
  config={{
    clientId: "...",
    redirectUri: "...",
    ssoBaseUrl: "...",
    scopes: "openid profile",
  }}
  refreshThreshold={60}  // Auto-refresh 60s before expiry
>
  <App />
</SsoProvider>

useSso()

import { useSso } from "@nihplod/sso-sdk/react";

const {
  user,              // SsoUser | null
  isAuthenticated,   // boolean
  isLoading,         // boolean
  login,             // (returnUrl?: string) => Promise<void>
  logout,            // (redirectToSso?: boolean) => Promise<void>
  refreshUser,       // () => Promise<void>
  getAccessToken,    // () => Promise<string | null>
  client,            // SsoClient instance
} = useSso();

<RequireAuth>

import { RequireAuth } from "@nihplod/sso-sdk/react";

<RequireAuth>
  <ProtectedContent />
</RequireAuth>

withAuth(Component)

import { withAuth } from "@nihplod/sso-sdk/react";

function DashboardPage() { return <div>Dashboard</div>; }
export default withAuth(DashboardPage);

<CallbackPage>

import { CallbackPage } from "@nihplod/sso-sdk/react";

// Render this component in the callback route
export default function AuthCallback() {
  return <CallbackPage />;
}

Next.js Bindings

For Next.js, the recommended approach is Middleware + Route Handler BFF pattern. Tokens are stored in httpOnly cookies so JavaScript cannot read them, providing the highest security.

// src/middleware.ts
import { createSsoMiddleware } from "@nihplod/sso-sdk/next";

export const middleware = createSsoMiddleware({
  clientId: "...",
  ssoBaseUrl: "https://nihplod.cn",
  redirectUri: "https://yourapp.com/api/auth/callback",
  scopes: "openid profile",
  publicPaths: ["/", "/public"],
  // Confidential Client (BFF) can pass clientSecret
  // clientSecret: process.env.SSO_CLIENT_SECRET,
});

export const config = {
  matcher: ["/((?!_next|favicon.ico).*)"],
};
// src/app/api/auth/callback/route.ts
import { createCallbackRouteHandler } from "@nihplod/sso-sdk/next";

export const GET = createCallbackRouteHandler({
  clientId: "...",
  ssoBaseUrl: "https://nihplod.cn",
  redirectUri: "https://yourapp.com/api/auth/callback",
  defaultReturnPath: "/dashboard",
  // Same as middleware; Confidential Client can pass clientSecret
});
// src/app/api/auth/logout/route.ts
import { createLogoutRouteHandler } from "@nihplod/sso-sdk/next";

export const GET = createLogoutRouteHandler({
  clientId: "...",
  ssoBaseUrl: "https://nihplod.cn",
  redirectUri: "https://yourapp.com/api/auth/callback",
  postLogoutRedirectUri: "https://yourapp.com/",
  redirectToSso: true,
});

Use a standard <a> tag to trigger the logout endpoint:

<a href="/api/auth/logout">Logout</a>

Cookie Configuration

Default cookie names:

| Cookie | Default Name | Description | |--------|--------------|-------------| | access_token | __Host-nihplod_sso_at | Requires Secure + Path=/ + no Domain | | refresh_token | __Host-nihplod_sso_rt | Requires Secure + Path=/ + no Domain | | state | __Host-nihplod_sso_state | Requires Secure + Path=/ + no Domain | | return_url | __Host-nihplod_sso_return | Requires Secure + Path=/ + no Domain | | verifier | __Secure-nihplod_sso_verifier | Requires Secure + no Domain; Path is the callback path, therefore uses __Secure- prefix |

For local development with http://localhost, the browser will reject Secure cookies. You may disable secure locally, but HTTPS is mandatory in production.


Security Recommendations and Token Storage

By default, the SDK stores tokens in memory. Public Clients (SPA / mobile / desktop) should never write the refresh_token to localStorage to prevent XSS from stealing long-lived credentials.

If the sub-project is a Next.js BFF / Confidential Client, you can store tokens in localStorage for multi-tab sharing:

import { setTokenStorage, createSecureStorage } from "@nihplod/sso-sdk";

setTokenStorage(createSecureStorage({ persist: true }));

In production, it is more secure to keep the refresh token in a Service Worker or HTTP-only cookie, exposing only the short-lived access token to the frontend.


Utility Functions

import {
  generateCodeVerifier,
  generateCodeChallenge,
  generateState,
  setTokenStorage,
  createSecureStorage,
  getTokenData,
  saveTokenData,
  removeTokenData,
  clearAllSsoData,
} from "@nihplod/sso-sdk";

// PKCE
const verifier = generateCodeVerifier(64);
const challenge = await generateCodeChallenge(verifier);

// State
const state = generateState();

// Custom token storage (default is memory storage)
setTokenStorage(createSecureStorage({ persist: false }));