kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
254 lines (250 loc) • 8.32 kB
JavaScript
'use client';
import { ConvexProviderWithAuth, useConvexAuth } from "convex/react";
import { createContext, useContext } from "react";
import { createAtomStore } from "jotai-x";
import { jsx } from "react/jsx-runtime";
//#region src/crpc/error.ts
/**
* Client-side CRPC error.
* Mirrors backend CRPCError pattern with typed error codes.
*/
var CRPCClientError = class extends Error {
name = "CRPCClientError";
code;
functionName;
constructor(opts) {
super(opts.message ?? `${opts.code}: ${opts.functionName}`);
this.code = opts.code;
this.functionName = opts.functionName;
}
};
/** Type guard for CRPCClientError */
const isCRPCClientError = (error) => error instanceof CRPCClientError;
/** Default unauthorized detection - checks UNAUTHORIZED code */
const defaultIsUnauthorized = (error) => {
if (!error || typeof error !== "object") return false;
if ("data" in error) {
const data = error.data;
if (data && typeof data === "object" && "code" in data) return data.code === "UNAUTHORIZED";
}
if ("code" in error) return error.code === "UNAUTHORIZED";
return false;
};
//#endregion
//#region src/react/auth-session-fallback.ts
const SESSION_TOKEN_FALLBACK_KEY = "kitcn.auth.session-token";
const SESSION_DATA_FALLBACK_KEY = "kitcn.auth.session-data";
const getSessionStorage = () => {
if (typeof window === "undefined") return null;
try {
return window.sessionStorage;
} catch {
return null;
}
};
const readAuthSessionFallbackToken = () => {
const storage = getSessionStorage();
if (!storage) return null;
const token = storage.getItem(SESSION_TOKEN_FALLBACK_KEY);
return token && token.length > 0 ? token : null;
};
const writeAuthSessionFallbackToken = (token) => {
const storage = getSessionStorage();
if (!storage) return;
if (token && token.length > 0) {
storage.setItem(SESSION_TOKEN_FALLBACK_KEY, token);
return;
}
storage.removeItem(SESSION_TOKEN_FALLBACK_KEY);
};
const readAuthSessionFallbackData = () => {
const storage = getSessionStorage();
if (!storage) return null;
const value = storage.getItem(SESSION_DATA_FALLBACK_KEY);
if (!value) return null;
try {
return JSON.parse(value);
} catch {
return null;
}
};
const writeAuthSessionFallbackData = (data) => {
const storage = getSessionStorage();
if (!storage) return;
if (data === null || data === void 0) {
storage.removeItem(SESSION_DATA_FALLBACK_KEY);
return;
}
storage.setItem(SESSION_DATA_FALLBACK_KEY, JSON.stringify(data));
};
const clearAuthSessionFallback = () => {
writeAuthSessionFallbackToken(null);
writeAuthSessionFallbackData(null);
};
//#endregion
//#region src/react/auth-store.tsx
/**
* Auth Store - Generic auth state management with jotai-x
*
* Provides token storage and auth callback configuration.
* App configures handlers, lib hooks consume state.
*/
const FetchAccessTokenContext = createContext(null);
/** Get fetchAccessToken from context (available immediately, no race condition) */
const useFetchAccessToken = () => useContext(FetchAccessTokenContext);
/**
* Context that holds auth result from ConvexAuthBridge.
* Allows @convex-dev/auth users to use skipUnauth queries without better-auth.
*/
const ConvexAuthBridgeContext = createContext(null);
/** Get auth from bridge context (null if no bridge configured) */
const useConvexAuthBridge = () => useContext(ConvexAuthBridgeContext);
const AUTH_SESSION_SYNC_GRACE_MS = 1e4;
const isSessionSyncGraceActive = (sessionSyncGraceUntil) => typeof sessionSyncGraceUntil === "number" && sessionSyncGraceUntil > Date.now();
/** Decode JWT expiration (ms timestamp) from token */
function decodeJwtExp(token) {
try {
const payload = JSON.parse(atob(token.split(".")[1]));
return payload.exp ? payload.exp * 1e3 : null;
} catch {
return null;
}
}
const { AuthProvider, useAuthStore, useAuthState, useAuthValue } = createAtomStore({
onMutationUnauthorized: () => {
throw new CRPCClientError({
code: "UNAUTHORIZED",
functionName: "mutation"
});
},
onQueryUnauthorized: () => {},
isUnauthorized: defaultIsUnauthorized,
token: null,
expiresAt: null,
isLoading: true,
isAuthenticated: false,
sessionSyncGraceUntil: null
}, {
name: "auth",
suppressWarnings: true
});
/**
* Safe wrapper around useConvexAuth that doesn't throw when used outside auth provider.
* Returns { isAuthenticated: false, isLoading: false } when no auth provider.
*
* Supports both:
* - better-auth users (via AuthProvider)
* - @convex-dev/auth users (via ConvexAuthBridge)
*/
function useSafeConvexAuth() {
const authStore = useAuthStore();
const bridgeAuth = useConvexAuthBridge();
if (authStore.store) return {
isAuthenticated: useAuthValue("isAuthenticated"),
isLoading: useAuthValue("isLoading")
};
if (bridgeAuth !== null) return bridgeAuth;
return {
isAuthenticated: false,
isLoading: false
};
}
/**
* Internal bridge component. Use `ConvexProviderWithAuth` instead.
* @internal
*/
function ConvexAuthBridge({ children }) {
const auth = useConvexAuth();
return /* @__PURE__ */ jsx(ConvexAuthBridgeContext.Provider, {
value: auth,
children
});
}
/**
* Convex provider with auth bridge for @convex-dev/auth users.
* Automatically wraps children with ConvexAuthBridge.
*
* @example
* ```tsx
* import { ConvexProviderWithAuth } from 'kitcn/react';
*
* <ConvexProviderWithAuth client={convex} useAuth={useAuthFromConvexDev}>
* <App />
* </ConvexProviderWithAuth>
* ```
*/
function ConvexProviderWithAuth$1({ children, ...props }) {
return /* @__PURE__ */ jsx(ConvexProviderWithAuth, {
...props,
children: /* @__PURE__ */ jsx(ConvexAuthBridge, { children })
});
}
const useAuth = () => {
const authStore = useAuthStore();
const bridgeAuth = useConvexAuthBridge();
if (authStore.store) {
if (typeof window === "undefined") return {
hasSession: !!authStore.get("token"),
isAuthenticated: false,
isLoading: true
};
const token = useAuthValue("token");
const isAuthenticated = useAuthValue("isAuthenticated");
const isLoading = useAuthValue("isLoading");
return {
hasSession: !!token,
isAuthenticated,
isLoading
};
}
if (bridgeAuth !== null) return {
hasSession: false,
isAuthenticated: bridgeAuth.isAuthenticated,
isLoading: bridgeAuth.isLoading
};
return {
hasSession: false,
isAuthenticated: false,
isLoading: false
};
};
/** Check if user maybe has auth (optimistic, has token) */
const useMaybeAuth = () => {
const { hasSession } = useAuth();
return hasSession;
};
/** Check if user is authenticated (server-verified) */
const useIsAuth = () => {
const { isAuthenticated } = useAuth();
return isAuthenticated;
};
const useAuthGuard = () => {
const { isAuthenticated } = useSafeConvexAuth();
const onMutationUnauthorized = useAuthValue("onMutationUnauthorized");
return (callback) => {
if (!isAuthenticated) {
onMutationUnauthorized();
return true;
}
return callback ? void callback() : false;
};
};
/** Render children only when maybe has auth (optimistic) */
function MaybeAuthenticated({ children }) {
return useMaybeAuth() ? children : null;
}
/** Render children only when authenticated (server-verified) */
function Authenticated({ children }) {
return useIsAuth() ? children : null;
}
/** Render children only when maybe not auth (optimistic) */
function MaybeUnauthenticated({ children }) {
return useMaybeAuth() ? null : children;
}
/** Render children only when not authenticated (server-verified) */
function Unauthenticated({ children }) {
const { isAuthenticated, isLoading } = useAuth();
return isLoading || isAuthenticated ? null : children;
}
//#endregion
export { readAuthSessionFallbackData as C, CRPCClientError as D, writeAuthSessionFallbackToken as E, defaultIsUnauthorized as O, clearAuthSessionFallback as S, writeAuthSessionFallbackData as T, useConvexAuthBridge as _, ConvexProviderWithAuth$1 as a, useMaybeAuth as b, MaybeUnauthenticated as c, isSessionSyncGraceActive as d, useAuth as f, useAuthValue as g, useAuthStore as h, ConvexAuthBridge as i, isCRPCClientError as k, Unauthenticated as l, useAuthState as m, AuthProvider as n, FetchAccessTokenContext as o, useAuthGuard as p, Authenticated as r, MaybeAuthenticated as s, AUTH_SESSION_SYNC_GRACE_MS as t, decodeJwtExp as u, useFetchAccessToken as v, readAuthSessionFallbackToken as w, useSafeConvexAuth as x, useIsAuth as y };