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

@oxaigen/react

v0.1.5

Published

React SDK for Oxaigen platform auth — drop-in <AuthProvider>, useAuth hook, RequireAuth and PermissionGate components for apps deployed behind the Oxaigen proxy.

Readme

@oxaigen/react

React SDK for apps deployed behind the Oxaigen platform proxy. Drop in an <AuthProvider>, then gate routes with <RequireAuth> and <PermissionGate>. No configuration — all auth traffic flows over same-origin /_oxa_auth/* endpoints that the proxy serves on every app's hostname.

Install

yarn add @oxaigen/react
# or
npm install @oxaigen/react

Peer dependency: react >= 18.

Quick start

import { AuthProvider, RequireAuth, PermissionGate } from "@oxaigen/react";

function App() {
	return (
		<AuthProvider>
			<Routes>
				<Route path="/" element={<Home />} />
				<Route
					path="/dashboard"
					element={
						<RequireAuth>
							<Dashboard />
						</RequireAuth>
					}
				/>
				<Route
					path="/audit"
					element={
						<RequireAuth>
							<PermissionGate name="audit">
								<Audit />
							</PermissionGate>
						</RequireAuth>
					}
				/>
			</Routes>
		</AuthProvider>
	);
}

<PermissionGate> on its own only checks the permission — it does not trigger a sign-in flow for anonymous users ( it renders the denied slot instead). Wrap it in <RequireAuth> when you want unauthenticated visitors to be prompted to sign in, as shown above.

The useAuth hook

import { useAuth } from "@oxaigen/react";

function Header() {
	const { status, signIn, signOut } = useAuth();

	if (status === "loading") return <span>…</span>;
	if (status === "authenticated") {
		return <button onClick={() => signOut()}>Sign out</button>;
	}
	return <button onClick={() => signIn()}>Sign in</button>;
}

status is one of:

| value | meaning | |-------------------|--------------------------------------------------------------| | "loading" | Initial check in flight | | "authenticated" | User has a valid session for this app | | "anonymous" | User is not signed in | | "error" | Something went wrong (e.g. server unreachable). See error. |

Reading the user profile

useAuth().user returns the user data fetched from /_oxa_auth/get-me, scoped to the current workspace and app. It's null when loading, anonymous, or errored — and also briefly null between sign-in completing and the get-me fetch resolving, so always gate on status first.

If a component only needs the user object, use the useUser() convenience hook:

import { useUser } from "@oxaigen/react";

function Greeting() {
	const user = useUser();
	if (!user) return null;
	return <span>Hi, {user.email}</span>;
}

Role and permission checks

The provider exposes both synchronous and async checks:

const { hasRole, hasPermission, checkPermission } = useAuth();

// Synchronous — reads from the cached User object captured at sign-in.
// Cheap, but reflects the user's state at the time get-me last ran.
if (hasRole("admin")) { /* … */
}
if (hasPermission("audit")) { /* … */
}

// Async — round-trips to the server. Use this when you need an
// authoritative answer (e.g. before a destructive action). Results
// are cached in-memory per name; the cache clears on signIn / signOut
// / refresh.
const ok = await checkPermission("audit");

Use refetchPermission(name) to bypass the cache and force a fresh check.

Important: the SDK does NOT hold tokens in JavaScript

The access token lives in an HTTP-only cookie set by the Oxaigen proxy. JavaScript cannot read it. That's by design — if it could, app XSS would be able to steal it.

What this means in practice:

  • Authenticated requests to your own backend just work. Use fetch with credentials: "same-origin" (or "include" for CORS) and the cookie rides along. No Authorization: Bearer … header needed.
  • You don't need to refresh tokens. The proxy handles that.
  • You can't decode the JWT in the browser. User details (email, name, roles, permissions) come from /_oxa_auth/get-me, which the provider fetches automatically and exposes via useAuth().user / useUser().

Customizing the gate UI

Both <RequireAuth> and <PermissionGate> ship with bare-bones default UI. Override with render props:

<RequireAuth
	loading={<Spinner />}
	unauthenticated={({ signIn, error }) => (
		<Box p={6}>
			<Heading size="md">Please sign in</Heading>
			{error && <Alert status="error">{error}</Alert>}
			<Button onClick={() => signIn()}>Sign in</Button>
		</Box>
	)}
>
	<Dashboard />
</RequireAuth>
<PermissionGate
	name="audit"
	loading={<Spinner />}
	denied={({ name }) => <AccessDenied permission={name} />}
	errored={({ message, retry }) => (
		<ErrorPanel message={message} onRetry={retry} />
	)}
>
	<AuditPanel />
</PermissionGate>

Cross-origin Bearer auth

If your frontend at app.{client}.oxaigen.com needs to call a backend API on a different host (e.g. api.{client}.oxaigen.com), the browser doesn't automatically send your access cookie there — cookies are scoped per host.

The proxy sets the access token cookie WITHOUT the HttpOnly flag specifically so frontend JS can read it and forward it as Authorization: Bearer .... Use the useAccessToken hook:

import { useAccessToken } from "@oxaigen/react";

function ApiClient() {
	const { token, refresh, isRefreshing, error } = useAccessToken();

	async function loadThings() {
		const resp = await fetch("https://api.acme.oxaigen.com/things", {
			headers: token ? { Authorization: `Bearer ${token}` } : {},
		});
		return resp.json();
	}

	// ...
}

The hook:

  • Reads the access token from document.cookie on mount and on every auth status change.
  • Auto-refreshes the token ~60 seconds before its exp claim expires. The refresh call goes to /_oxa_auth/refresh ( same-origin), which uses the HttpOnly refresh cookie server-side to mint new tokens and set new cookies.
  • Exposes a manual refresh() for cases where you want to force a fresh token (e.g. after a 401 from your backend API).

Security trade-off. Because the access cookie is JS-readable, any XSS in your frontend can read it. The refresh token cookie remains HttpOnly, so the attacker can't get a long-lived token — but they can use the access token until its expiry (typically 5 minutes).

If your app doesn't need cross-origin Bearer forwarding (e.g. all your APIs are same-origin behind a BFF pattern), set ACCESS_COOKIE_HTTP_ONLY=true on the proxy at deploy time. The SDK's useAccessToken hook will then return null for the token (cookie unreadable from JS), and you should use plain fetch(..., { credentials: "same-origin" }) instead — the browser handles cookie attachment.


API reference

<AuthProvider>

Provides auth context to the tree. On mount it calls /_oxa_auth/test-app-token and (if authenticated) /_oxa_auth/get-me to populate user.

| prop | type | default | description | |--------------------|-------------|---------|----------------------------------------------------------------------| | children | ReactNode | — | App tree | | skipInitialCheck | boolean | false | If true, skips the test-app-token call on mount. Useful for tests. |

useAuth()

Returns the full auth context. Throws if used outside <AuthProvider>.

| field | type | description | |---------------------------|----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| | status | "loading" \| "authenticated" \| "anonymous" \| "error" | Current auth state. | | user | User \| null | Current user profile, scoped to this app's workspace. Null when loading / anonymous / errored. | | error | string \| null | Last error message. | | signIn(returnTo?) | (returnTo?: string) => Promise<void> | Open the sign-in popup. returnTo defaults to the current URL. Resolves once the popup is opened, NOT when sign-in completes — listen to status for that. | | signOut() | () => Promise<void> | Sign out. Cookies are cleared by the API. | | refresh() | () => Promise<void> | Re-run test-app-token and get-me. | | checkPermission(name) | (name: string) => Promise<boolean> | Memoized server-side permission check. Errors are treated as denial. | | refetchPermission(name) | (name: string) => Promise<boolean> | Force a fresh server-side permission check, bypassing cache. | | hasRole(name) | (name: string) => boolean | Synchronous role check against the cached user. Returns false if user not yet loaded. | | hasPermission(name) | (name: string) => boolean | Synchronous permission check against the cached user. Returns false if user not yet loaded. For authoritative checks, use checkPermission. |

useUser()

Convenience hook returning just useAuth().user. Re-renders whenever the User reference changes.

<RequireAuth>

| prop | type | description | |-------------------|-------------------------------------------------|--------------------------------------------------| | children | ReactNode | What to render when authenticated. | | loading | ReactNode \| () => ReactNode | Optional override for the loading state. | | unauthenticated | ReactNode \| ({ signIn, error }) => ReactNode | Optional override for the unauthenticated state. |

<PermissionGate>

| prop | type | description | |------------|--------------------------------------------------------|-------------------------------------------------------------| | name | string | Permission name to check. | | children | ReactNode | What to render when allowed. | | loading | ReactNode \| () => ReactNode | Optional override. | | denied | ReactNode \| ({ name }) => ReactNode | Optional override. Also rendered for unauthenticated users. | | errored | ReactNode \| ({ message, retry, name }) => ReactNode | Optional override. |

Low-level API

For advanced use (custom providers, scripts, tests):

import {
	startLogin,
	testAppToken,
	testAppPermission,
	getMe,
	logout,
} from "@oxaigen/react";

Thin typed wrappers over fetch to the corresponding /_oxa_auth/* endpoints. See JSDoc for details.

SSR

All window/document access is inside useEffect, so the SDK is SSR-safe. On the server, status is "loading", user is null, and error is null — gates render their loading slots, so render appropriate placeholders.

How the popup flow works

  1. User clicks Sign in → signIn() calls GET /_oxa_auth/login → returns a Keycloak authorize URL.
  2. SDK opens that URL in a centered popup window (450 × 610).
  3. User signs in at Keycloak; Keycloak redirects the popup to /_oxa_auth/callback.
  4. The callback page exchanges the auth code for tokens (server-side), sets HTTP-only cookies on this host, and postMessages OXA_AUTH_SUCCESS (or OXA_AUTH_FAILURE) back to the opener.
  5. The SDK receives the message, re-runs test-app-token + get-me, and status flips to authenticated.

If the popup is blocked, signIn() sets an error message instead of opening it. If the popup is closed mid-flow without posting a message, the SDK polls for closure (every 500ms) and re-checks status as a fallback.

What the SDK still doesn't do

  • The refresh token never touches JavaScript. Refresh rotation goes through /_oxa_auth/refresh, which reads the HttpOnly refresh cookie server-side.
  • No client-side JWT decoding. All claim-derived data (email, name, roles, permissions) comes from /_oxa_auth/get-me, surfaced via useAuth().user.

License

ALL RIGHTS RESERVED