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

@mosano-product-framework/sdk

v0.2.1

Published

MPF SDK: typed clients for the MPF services

Readme

@mosano-product-framework/sdk/react

React bindings for the MPF SDK: a provider, six hooks, and nothing else.

Dependency-free apart from React. No TanStack Query, no zustand, no router. State lives in a small external store read through useSyncExternalStore, so there are no constraints on how you manage the rest of your data — SWR, Apollo, TanStack Query and plain fetch consumers are equally supported.

For the SDK core — tenant selection, claims, session management — see README.md.

Installation

React is an optional peer dependency. Installing the SDK does not install React, and importing the root barrel never pulls React in.

pnpm add @mosano-product-framework/sdk react

Requires React 18+ (useSyncExternalStore is 18+).

Setup

import { MPFAuthProvider } from '@mosano-product-framework/sdk/react';
import { createAuthClient } from '@mosano-product-framework/sdk/identity';
import { createDefaultStorage } from '@mosano-product-framework/sdk/auth';

const auth = createAuthClient({ baseUrl: 'https://api.example.com/auth' });

export function App() {
  return (
    <MPFAuthProvider
      renew={(refreshToken) => auth.renewSession({ refresh_token: refreshToken })}
      storage={createDefaultStorage()}
      onDeadSession={() => navigate('/login')}
    >
      <Routes />
    </MPFAuthProvider>
  );
}

<MPFAuthProvider> props

| Prop | Required | Description | |---|---|---| | renew | ✅ | (refreshToken) => Promise<{ access_token, refresh_token }>. Wire to auth.renewSession. | | storage | | Where the refresh token and tenant selection are persisted. Defaults to localStorage, falling back to memory. | | onDeadSession | | Called when the session cannot be renewed. Defaults to a one-time console.warn. | | tenantProvider | | A pre-built createTenantSelection() store, so non-React code can share the same selection. | | loadTenantDirectory | | Fetches tenant display names. See useTenantDirectory(). | | autoLoadTenantDirectory | | Fetch names automatically once signed in. Default true when a loader is supplied. | | sessionOptions | | Forwarded to the session manager (bufferSeconds, jitterMs, crossTab, ...). | | session | | A pre-built SessionManager. Mainly a test seam. |

Hydration

The provider hydrates synchronously, during its first render. Anything already in storage — the refresh token, the selected tenant — is present on the very first paint, so a reload does not flash a login form and does not reset the user's tenant to "none". There is no loading state to handle for hydration; useAuth().hydrated is always true by the time your component runs.

Hooks

useAuth()

Core state and actions.

function Header() {
  const { authenticated, claims, signOut } = useAuth();

  if (!authenticated) return <SignInLink />;
  return <button onClick={signOut}>Sign out {claims?.uid}</button>;
}

| Field | Description | |---|---| | hydrated | Always true after the first render. | | authenticated | Whether an access token is present. | | claims | Decoded (unverified) claims, or null. | | getAccessToken() | Promise<string \| null>, renewing first if the token is stale. | | setTokens(pair) | Adopt a fresh token pair after sign-in. | | signOut() | Drop the session locally. Does not navigate. |

Sign-in looks like this — the SDK does not own your form or your routing:

const { setTokens } = useAuth();

async function onSubmit(email: string, password: string) {
  const result = await auth.signInEmailPassword({ email, password });
  setTokens(result);          // provider state updates synchronously
  navigate('/dashboard');     // your router, your call
}

useTenants()

The tenants the current token grants access to. Synchronous, and cannot fail — the answer is already in the access token, which is fresher than anything a round trip could tell you.

function TenantSwitcher() {
  const { status, tenants } = useTenants();
  const current = useCurrentTenant();
  const select = useSelectTenant();

  if (status === 'unloaded') return null;   // not signed in

  return (
    <select value={current.tenant ?? ''} onChange={(e) => select(e.target.value || null)}>
      <option value="">All (no tenant)</option>
      {tenants.map((t) => (
        <option key={t.id} value={t.id}>{t.name ?? t.id}</option>
      ))}
    </select>
  );
}

status is only 'unloaded' | 'loaded', because there is no fetch here and therefore nothing to fail. Each entry has id, roles, defaultRole (the token's explicit dfr for that tenant when it carries one, else roles[0]), and — once the directory has loaded — name and slug.

useTenantDirectory()

Tenant display names, which do require a network call and can genuinely fail.

tnts in the token carries ids and roles only, so without this a switcher shows UUIDs. Opt in by giving the provider a loader:

// Simplest: REST. `listMyTenants()` returns id, name and slug.
<MPFAuthProvider
  renew={renew}
  loadTenantDirectory={() => auth.listMyTenants()}
>
// Or GraphQL, if your app already has a client. Query with `{ tenant: null }` —
// the tenantless role has exactly the permission a switcher needs.
import { TENANT_DIRECTORY_QUERY } from '@mosano-product-framework/sdk/auth';

<MPFAuthProvider
  renew={renew}
  loadTenantDirectory={async () => {
    const data = await gql.query(TENANT_DIRECTORY_QUERY, undefined, { tenant: null });
    return data.identity_tenants;
  }}
>

The loader is injected rather than built in, so the React layer never depends on the GraphQL client and either transport works unchanged.

const { status, directory, error, available, load } = useTenantDirectory();

| status | Meaning | |---|---| | 'unloaded' | No loader supplied, or not fetched yet. | | 'loading' | In flight. | | 'loaded' | Names fetched. May legitimately be empty — that means "this user has no tenants". | | 'failed' | The fetch failed. error says why. |

'loaded'-but-empty and 'failed' are deliberately distinct. Collapsing them is how an empty switcher comes to silently mean "the server was unreachable".

A failed name lookup never empties the switcher. The ids come from the token, so they remain listed and selectable; only the names are missing. Render name ?? id:

function TenantSwitcher() {
  const { tenants } = useTenants();
  const { status, error } = useTenantDirectory();

  return (
    <>
      {status === 'failed' && <Warning>Couldn't load names: {error?.message}</Warning>}
      {tenants.map((t) => <Option key={t.id} label={t.name ?? t.id} />)}
    </>
  );
}

Names are dropped on signOut(). Entries for tenants the token does not grant are ignored, so a name lookup can never make an unauthorised tenant selectable.

useCurrentTenant()

The current selection: { tenant: string | null, role?: string }. tenant is null on the no-tenant path.

useSelectTenant()

const select = useSelectTenant();

select('tenant-uuid');            // select a tenant, default role
select('tenant-uuid', 'admin');   // select a specific role
select(null);                     // no tenant

Synchronous. No await, no network, no renewal. Tenant is a request header, so switching is a local state change and the next request simply carries a different header — the access token is byte-identical before and after. A test asserts it fires zero fetches and zero renewals.

The selection is persisted, so it survives a reload.

useClaims()

Decoded claims of the current access token, or null.

⚠️ Decode, not verify

These claims are decoded, never verified. There is no signature check and no key material in this package. A hostile client can forge any value here: any user id, any tenant, any role, any expiry.

Use them for UI only — rendering a switcher, greying out an option, deciding when to renew. Never gate a security-relevant branch on them. The server verifies the signature and re-derives tenant and role on every request, and a forged claim buys an attacker nothing there.

const claims = useClaims();
const isOwnerSomewhere = (claims?.tnts ?? []).some((t) => t.rls.includes('owner'));
// ^ fine for showing a menu item. NOT fine as an authorization decision.

useMPFAuthOptional()

Returns the context or null instead of throwing, for components rendered both inside and outside an authenticated shell.

function Header() {
  const ctx = useMPFAuthOptional();
  if (!ctx) return <MarketingHeader />;
  return <AppHeader />;
}

Every other hook throws a descriptive error when no provider is mounted.

Storage adapters

import { createDefaultStorage, createMemoryStorage } from '@mosano-product-framework/sdk/auth';
  • createDefaultStorage()localStorage, probed for writability and falling back to memory (Safari private mode and blocked third-party contexts expose localStorage but throw on write).
  • createMemoryStorage() — in-memory; the automatic choice when window is absent, and useful in tests.
  • Anything with getItem / setItem / removeItem works, so sessionStorage or an encrypted wrapper drops straight in.

Only the refresh token and the tenant selection are persisted. The access token stays in memory.

onDeadSession is a callback, never a navigation

The SDK never navigates. Routing is your application's concern, and hard-coding window.location or a useNavigate() call would couple the library to one router and one URL scheme.

<MPFAuthProvider
  renew={renew}
  onDeadSession={(error) => {
    queryClient.clear();
    navigate('/login', { state: { reason: error.message } });
  }}
>

The default is a one-time console.warn telling you to supply one.

Using with TanStack Query

The bindings hold auth state; your query library holds server state. Wire them with getAccessToken, and key queries on the tenant so a switch refetches.

import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useAuth, useCurrentTenant, useSelectTenant } from '@mosano-product-framework/sdk/react';

function useMembers() {
  const { getAccessToken } = useAuth();
  const { tenant } = useCurrentTenant();

  return useQuery({
    // Tenant in the key: switching tenants refetches instead of showing stale rows.
    queryKey: ['members', tenant],
    queryFn: async () => {
      await getAccessToken();       // renews if stale, before the request goes out
      return auth.listTenantMembers(tenant!);
    },
    enabled: Boolean(tenant),
  });
}

// Switching stays synchronous; the query layer reacts to the key change.
function Switcher() {
  const select = useSelectTenant();
  return <button onClick={() => select('other-tenant')}>Switch</button>;
}

On sign-out, clear the cache so one user's data cannot be shown to the next:

const queryClient = useQueryClient();
const { signOut } = useAuth();

const logout = () => {
  signOut();
  queryClient.clear();
};

The same shape works with SWR (useSWR(['members', tenant], ...)) or Apollo (refetch on the tenant change).

Out of scope

GraphQL subscriptions and websockets are not supported — see the Out of scope section of the main README for why the tenant model makes long-lived sockets a separate design problem rather than a missing feature.

License

MIT