@meetreeve/auth
v0.2.2
Published
Unified Auth0 wrapper for MindFortress frontends — same scope rules, error classification, recovery state, and telemetry across Vite SPAs (Freya) and Next.js apps (Reeve, AgentPik). Companion to reeve_auth (Python backend).
Readme
@meetreeve/auth
Unified Auth0 wrapper for MindFortress frontends. One package, framework-specific entrypoints, identical scope/error/recovery/telemetry rules across all consumers.
Companion to reeve_auth (Python backend, DEV-551). Same north star, different surface.
Why this exists
Before this package: Freya/Reeve/AgentPik each had their own Auth0 plumbing (three different SDKs, three different scope strings, three different error-handling stories). Bugs like missing offline_access shipped to one app and silently existed in the other two — discovered one at a time. See DEV-624 for the bug that motivated this.
After this package: one rule set, three entrypoints, three consumers in lockstep.
Entrypoints
| Import | When | Wraps |
|---|---|---|
| @meetreeve/auth/react | Vite SPAs (Freya) | @auth0/auth0-react |
| @meetreeve/auth/nextjs | Next.js apps (Reeve, AgentPik post-DEV-629) | @auth0/nextjs-auth0 |
| @meetreeve/auth/capacitor | Native Capacitor apps (Freya iOS) | @auth0/auth0-react + @capacitor/browser |
| @meetreeve/auth/core | Anywhere | framework-free primitives |
The shared concerns live in /core — error classification, recovery state, telemetry event names, canonical scope. Each framework wrapper imports from /core so the rules are applied identically.
/react (Freya)
import { ReeveAuthProvider, useAuthToken } from "@meetreeve/auth/react";
<ReeveAuthProvider
config={{ domain, clientId, audience }}
errorPath="/error"
onRedirectCallback={(appState) => navigate(appState?.returnTo ?? "/")}
>
<App />
</ReeveAuthProvider>;
function MyComponent() {
const { getToken } = useAuthToken({
capture: (event, props) => posthog.capture(event, props),
onForceReauth: ({ returnTo, interactive }) => {
triggerForceReauth(returnTo, { interactive });
},
});
// ...
}Internally: wraps <Auth0Provider> with scope: "openid profile email offline_access", useRefreshTokens: true, cacheLocation: "localstorage". Provides terminal/transient error classification + force-reauth recovery via useAuthToken.
/nextjs (Reeve, AgentPik post-DEV-629)
// lib/auth0.ts
import { Auth0Client } from "@auth0/nextjs-auth0/server";
import { reeveAuth0Config } from "@meetreeve/auth/nextjs";
export const auth0 = new Auth0Client(
reeveAuth0Config(
{ host: "reeve" },
{
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
secret: process.env.AUTH0_SECRET!,
appBaseUrl: process.env.AUTH0_BASE_URL!,
audience: process.env.AUTH0_AUDIENCE!,
},
),
);Returns the canonical config object — host owns the Auth0Client instance (it's a singleton per app). We just hand it the right config: scope includes offline_access, route paths standardized, session defaults paired with DEV-624 tenant settings.
/core (advanced consumers, axios interceptors)
import {
TERMINAL_TOKEN_ERRORS,
isTransientTokenError,
incrementForceReauthCount,
isForceReauthCapped,
CANONICAL_SCOPE,
} from "@meetreeve/auth/core";For host apps that need the primitives without the React/Next.js layer (e.g. an axios 401 interceptor that participates in the same recovery counter).
/capacitor (Freya iOS, native Capacitor apps)
Runs Auth0 Universal Login natively by layering over @auth0/auth0-react:
loginWithRedirect({ openUrl }) opens Universal Login in SFSafariViewController
via @capacitor/browser, and the appUrlOpen deep link routes back to
handleRedirectCallback. The same code runs on web (standard redirect) — the
flow is selected automatically via @capacitor/core's isNativePlatform().
Optional peer deps (native consumers only): @auth0/auth0-react,
@capacitor/app, @capacitor/browser, @capacitor/core.
Batteries-included provider
import {
ReeveCapacitorAuthProvider,
useNativeAuth,
} from "@meetreeve/auth/capacitor";
<ReeveCapacitorAuthProvider
config={{
domain: "dev-52csukly772lub6s.us.auth0.com",
clientId: "<spa-client-id>",
audience: "https://api.meetfreya.com",
appScheme: "com.meetfreya.ios",
redirectUri: "https://app.meetfreya.com", // web fallback
}}
onLoginError={(e) => console.error(e)}
>
<App />
</ReeveCapacitorAuthProvider>;
// anywhere inside the provider:
const { login, logout } = useNativeAuth();Adopting from a direct @auth0/auth0-react setup (wp-frontend)
Keep your existing <Auth0Provider>. Make three changes:
import {
buildNativeAuthParams,
installAuthCallbackListener,
nativeOpenUrl,
isNativePlatform,
useAuth0,
} from "@meetreeve/auth/capacitor";
// 1. native redirect_uri
const redirectUri = isNativePlatform()
? buildNativeAuthParams({ scheme: "com.meetfreya.ios", domain }).redirectUri
: window.location.origin;
// 2. inside the provider, route the deep link to the SDK:
const { handleRedirectCallback } = useAuth0();
useEffect(() => {
if (!isNativePlatform()) return;
const pending = installAuthCallbackListener({
scheme: "com.meetfreya.ios",
onCallback: (url) => handleRedirectCallback(url),
});
return () => {
void pending.then((h) => h.remove());
};
}, [handleRedirectCallback]);
// 3. open Auth0 in the in-app browser on login/logout:
loginWithRedirect(isNativePlatform() ? { openUrl: nativeOpenUrl } : undefined);Auth0 tenant config (consumer ticket DEV-1394)
Register this native login callback as an Allowed Callback URL in the tenant:
com.meetfreya.ios://<domain>/capacitor/com.meetfreya.ios/callbackLogout uses the default full-page web redirect (not the custom scheme), so its
returnTo is your web origin — already an Allowed Logout URL.
Not included (roadmap)
- Keychain-backed refresh-token cache (PR2 — v0 uses localStorage, which persists across WKWebView cold starts).
- Biometric unlock (Phase 2).
Error classification
| Class | Examples | Behavior |
|---|---|---|
| Terminal | login_required, consent_required, invalid_grant, missing_refresh_token | Trigger reauth on first hit |
| Transient | timeout, network error, aborted, offline, load failed | Don't count toward threshold |
| Unknown | anything else | Count toward threshold; reauth after 6 consecutive |
Telemetry contract
The package never imports a tracker. Pass a capture function and the lib will call it with these events (PostHog event names match what Freya already records — see DEV-624):
auth_force_reauth_triggered—{ source: "token_terminal" | "token_threshold" | "api_401_retry", error, interactive }auth_force_reauth_capped(future)auth_callback_completed(future)auth_recovery_redirect(future)
What's intentionally not in v1
- Magic-link / passwordless flows (defer until a product needs it)
- React Native SDK (Capacitor is covered via
/capacitor; RN is not planned) - Multi-tenant SSO across MindFortress apps (defer to Reeve.Embed)
- Tenant bootstrap CLI (Account Linker Action + email templates — separate
@meetreeve/auth-tenant-bootstrappackage, coming next)
Related
- DEV-624 — login persistence fix on Freya, motivated this work
- DEV-627 — this ticket
- DEV-629 — AgentPik PKCE retirement, consumes this package
- DEV-551 —
reeve_authPython backend JWT verify - DEV-609 — Reeve.Auth v2, Management API Python wrapper
