kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
1,633 lines (1,617 loc) • 94.2 kB
JavaScript
import { getFunctionName } from "convex/server";
import { Show, createContext, createEffect, createMemo, createSignal, on, onCleanup, onMount, useContext } from "solid-js";
import { createComponent, memo } from "solid-js/web";
import { createStore } from "solid-js/store";
import { notifyManager, skipToken, useQueries, useQueryClient } from "@tanstack/solid-query";
import { ConvexClient, ConvexHttpClient } from "convex/browser";
import { hashKey } from "@tanstack/query-core";
import { convexToJson } from "convex/values";
//#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/solid/auth-store.tsx
/** @jsxImportSource solid-js */
/**
* Auth Store - Generic auth state management with Solid stores
*
* 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);
/** 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 defaultState = {
onMutationUnauthorized: () => {
throw new CRPCClientError({
code: "UNAUTHORIZED",
functionName: "mutation"
});
},
onQueryUnauthorized: () => {},
isUnauthorized: defaultIsUnauthorized,
token: null,
expiresAt: null,
isLoading: true,
isAuthenticated: false
};
const AuthStoreContext = createContext(null);
function useAuthStore() {
const ctx = useContext(AuthStoreContext);
if (!ctx) return {
get: (key) => defaultState[key],
set: () => {},
store: null
};
return ctx;
}
function useAuthValue(key) {
return useAuthStore().get(key);
}
function AuthProvider(props) {
const [state, setState] = createStore({
...defaultState,
...props.initialValues,
...props.isUnauthorized && { isUnauthorized: props.isUnauthorized },
...props.onMutationUnauthorized && { onMutationUnauthorized: props.onMutationUnauthorized },
...props.onQueryUnauthorized && { onQueryUnauthorized: props.onQueryUnauthorized }
});
const store = {
get: (key) => state[key],
set: (key, value) => setState(key, value),
store: state
};
return createComponent(AuthStoreContext.Provider, {
value: store,
get children() {
return props.children;
}
});
}
/**
* Safe wrapper 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();
return {
get isAuthenticated() {
if (authStore.store) return authStore.get("isAuthenticated");
if (bridgeAuth !== null) return bridgeAuth.isAuthenticated;
return false;
},
get isLoading() {
if (authStore.store) return authStore.get("isLoading");
if (bridgeAuth !== null) return bridgeAuth.isLoading;
return false;
}
};
}
/**
* Bridge component that provides auth state via context.
* @internal
*/
function ConvexAuthBridge(props) {
return createComponent(ConvexAuthBridgeContext.Provider, {
value: {
get isLoading() {
return props.isLoading;
},
get isAuthenticated() {
return props.isAuthenticated;
}
},
get children() {
return props.children;
}
});
}
const useAuth = () => {
const authStore = useAuthStore();
const bridgeAuth = useConvexAuthBridge();
return {
get hasSession() {
if (authStore.store) return !!authStore.get("token");
return false;
},
get isAuthenticated() {
if (authStore.store) return authStore.get("isAuthenticated");
if (bridgeAuth !== null) return bridgeAuth.isAuthenticated;
return false;
},
get isLoading() {
if (authStore.store) return authStore.get("isLoading");
if (bridgeAuth !== null) return bridgeAuth.isLoading;
return false;
}
};
};
/** Check if user maybe has auth (optimistic, has token) */
const useMaybeAuth = () => {
const auth = useAuth();
return () => auth.hasSession;
};
/** Check if user is authenticated (server-verified) */
const useIsAuth = () => {
const auth = useAuth();
return () => auth.isAuthenticated;
};
const useAuthGuard = () => {
const authStore = useAuthStore();
const bridgeAuth = useConvexAuthBridge();
return (callback) => {
let isAuthenticated = false;
if (authStore.store) isAuthenticated = authStore.get("isAuthenticated");
else if (bridgeAuth !== null) isAuthenticated = bridgeAuth.isAuthenticated;
if (!isAuthenticated) {
authStore.get("onMutationUnauthorized")();
return true;
}
return callback ? void callback() : false;
};
};
/** Render children only when maybe has auth (optimistic) */
function MaybeAuthenticated(props) {
const isAuth = useMaybeAuth();
return createComponent(Show, {
get when() {
return isAuth();
},
get children() {
return props.children;
}
});
}
/** Render children only when authenticated (server-verified) */
function Authenticated(props) {
const isAuth = useIsAuth();
return createComponent(Show, {
get when() {
return isAuth();
},
get children() {
return props.children;
}
});
}
/** Render children only when maybe not auth (optimistic) */
function MaybeUnauthenticated(props) {
const isAuth = useMaybeAuth();
return createComponent(Show, {
get when() {
return !isAuth();
},
get children() {
return props.children;
}
});
}
/** Render children only when not authenticated (server-verified) */
function Unauthenticated(props) {
const auth = useAuth();
return createComponent(Show, {
get when() {
return memo(() => !!!auth.isAuthenticated)() && !auth.isLoading;
},
get children() {
return props.children;
}
});
}
//#endregion
//#region src/solid/auth.ts
const MetaContext = createContext(void 0);
/**
* Hook to access the meta object from context.
* Returns undefined if meta was not provided.
*/
function useMeta() {
return useContext(MetaContext);
}
/**
* Hook to get function metadata from the meta index.
*/
function useFnMeta() {
const meta = useMeta();
return (namespace, fnName) => meta?.[namespace]?.[fnName];
}
/** Get auth type from meta for a function */
function getAuthType(meta, funcName) {
const [namespace, fnName] = funcName.split(":");
return meta?.[namespace]?.[fnName]?.auth;
}
/** Hook to compute auth-based skip logic for queries */
function useAuthSkip(funcRef, opts) {
const auth = useSafeConvexAuth();
const authType = getAuthType(useMeta(), getFunctionName(funcRef));
const authLoadingApplies = authType === "optional" || authType === "required";
return {
authType,
get isAuthLoading() {
return auth.isLoading;
},
get isAuthenticated() {
return auth.isAuthenticated;
},
get shouldSkip() {
const isAuthLoading = auth.isLoading;
const isAuthenticated = auth.isAuthenticated;
return opts?.enabled === false || authLoadingApplies && isAuthLoading || authType === "required" && !isAuthenticated && !isAuthLoading || !isAuthenticated && !isAuthLoading && !!opts?.skipUnauth;
}
};
}
//#endregion
//#region src/shared/meta-utils.ts
const metaCache = /* @__PURE__ */ new WeakMap();
const nonMetaLeafKeys = new Set(["functionRef", "ref"]);
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function isFunctionType(value) {
return value === "query" || value === "mutation" || value === "action";
}
function isMetaScalar(value) {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
function extractLeafMeta(value) {
const type = value.type;
if (!isFunctionType(type)) return;
const result = { type };
for (const [key, entry] of Object.entries(value)) {
if (key === "type" || nonMetaLeafKeys.has(key) || key.startsWith("_")) continue;
if (entry === void 0) continue;
if (isMetaScalar(entry)) result[key] = entry;
}
return result;
}
function getHttpRoutes(api) {
const routes = api._http;
if (!isRecord(routes)) return;
const normalized = {};
for (const [routeKey, routeValue] of Object.entries(routes)) {
if (!isRecord(routeValue)) continue;
const routePath = routeValue.path;
const routeMethod = routeValue.method;
if (typeof routePath === "string" && typeof routeMethod === "string") normalized[routeKey] = {
path: routePath,
method: routeMethod
};
}
return normalized;
}
/**
* Build a metadata index from merged API leaves.
* Supports both generated `api` objects and plain metadata fixtures.
*/
function buildMetaIndex(api) {
const cached = metaCache.get(api);
if (cached) return cached;
const meta = {};
const httpRoutes = getHttpRoutes(api);
if (httpRoutes) meta._http = httpRoutes;
const walk = (node, path) => {
for (const [key, value] of Object.entries(node)) {
if (key.startsWith("_")) continue;
if (!isRecord(value)) continue;
const leafMeta = extractLeafMeta(value);
if (leafMeta) {
if (path.length === 0) continue;
const namespace = path.join("/");
meta[namespace] ??= {};
meta[namespace][key] = leafMeta;
continue;
}
walk(value, [...path, key]);
}
};
walk(api, []);
metaCache.set(api, meta);
return meta;
}
/**
* Get a function reference from the API object by traversing the path.
*/
function getFuncRef(api, path) {
let current = api;
for (const key of path) if (current && typeof current === "object") {
const next = current[key];
if (next === void 0) throw new Error(`Invalid path: ${path.join(".")}`);
current = next;
} else throw new Error(`Invalid path: ${path.join(".")}`);
if (current && typeof current === "object") {
const maybeFunctionRef = current.functionRef;
if (maybeFunctionRef && typeof maybeFunctionRef === "object") return maybeFunctionRef;
}
if (!current || typeof current !== "object") throw new Error(`Invalid function reference at path: ${path.join(".")}`);
return current;
}
/**
* Get function type from meta using path.
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
*
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
* @param meta - The meta object from codegen
* @returns Function type or 'query' as default
*/
function getFunctionType(path, source) {
if (path.length < 2) return "query";
const meta = buildMetaIndex(source);
const fnName = path.at(-1);
const fnType = meta[path.slice(0, -1).join("/")]?.[fnName]?.type;
if (fnType === "query" || fnType === "mutation" || fnType === "action") return fnType;
return "query";
}
/**
* Get function metadata from meta using path.
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
*
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
* @param meta - The meta object from codegen
* @returns Function metadata or undefined
*/
function getFunctionMeta(path, source) {
if (path.length < 2) return;
const meta = buildMetaIndex(source);
const fnName = path.at(-1);
return meta[path.slice(0, -1).join("/")]?.[fnName];
}
//#endregion
//#region src/crpc/http-types.ts
/** HTTP client error */
var HttpClientError = class extends Error {
name = "HttpClientError";
code;
status;
procedureName;
constructor(opts) {
super(opts.message ?? `${opts.code}: ${opts.procedureName}`);
this.code = opts.code;
this.status = opts.status;
this.procedureName = opts.procedureName;
}
};
//#endregion
//#region src/crpc/http-client.ts
/**
* HTTP Client Helpers
*
* Framework-agnostic utilities for executing HTTP requests
* against Convex HTTP endpoints.
*/
/** Reserved keys that are not part of JSON body */
const RESERVED_KEYS = new Set([
"params",
"searchParams",
"form",
"fetch",
"init",
"headers"
]);
/**
* Replace URL path parameters with actual values.
* e.g., '/users/:id' with { id: '123' } -> '/users/123'
*/
function replaceUrlParam(url, params) {
return url.replace(/:(\w+)/g, (_, key) => {
const value = params[key];
return value !== void 0 ? encodeURIComponent(value) : `:${key}`;
});
}
/**
* Build URLSearchParams from query object.
* Handles array values as multiple params with same key (like Hono).
*/
function buildSearchParams(query) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) if (Array.isArray(value)) for (const v of value) params.append(key, v);
else if (value !== void 0 && value !== null) params.append(key, value);
return params;
}
/**
* Hono-style HTTP request executor.
* Processes args in the same way as Hono's ClientRequestImpl.fetch().
*/
async function executeHttpRequest(opts) {
const { method, path } = opts.route;
const args = opts.args ?? {};
let rBody;
let cType;
if (args.form) {
const form = new FormData();
for (const [k, v] of Object.entries(args.form)) if (Array.isArray(v)) for (const v2 of v) form.append(k, v2);
else form.append(k, v);
rBody = form;
} else {
const jsonBody = {};
for (const [key, value] of Object.entries(args)) if (!RESERVED_KEYS.has(key) && value !== void 0) jsonBody[key] = value;
if (Object.keys(jsonBody).length > 0) {
rBody = JSON.stringify(opts.transformer.input.serialize(jsonBody));
cType = "application/json";
}
}
const argsClientOpts = {};
if (args.fetch) argsClientOpts.fetch = args.fetch;
if (args.init) argsClientOpts.init = args.init;
if (args.headers) argsClientOpts.headers = args.headers;
const mergedClientOpts = {
...opts.clientOpts,
...argsClientOpts
};
const resolvedBaseHeaders = typeof opts.baseHeaders === "function" ? await opts.baseHeaders() : opts.baseHeaders;
const headerValues = { ...typeof mergedClientOpts.headers === "function" ? await mergedClientOpts.headers() : mergedClientOpts.headers };
if (cType) headerValues["Content-Type"] = cType;
const finalHeaders = {};
if (resolvedBaseHeaders) {
for (const [key, value] of Object.entries(resolvedBaseHeaders)) if (value !== void 0) finalHeaders[key] = value;
}
Object.assign(finalHeaders, headerValues);
let url = opts.convexSiteUrl + path;
if (args.params) url = opts.convexSiteUrl + replaceUrlParam(path, args.params);
if (args.searchParams) {
const queryString = buildSearchParams(args.searchParams).toString();
if (queryString) url = `${url}?${queryString}`;
}
const methodUpperCase = method.toUpperCase();
const setBody = !(methodUpperCase === "GET" || methodUpperCase === "HEAD");
const response = await (mergedClientOpts.fetch ?? opts.baseFetch ?? globalThis.fetch)(url, {
body: setBody ? rBody : void 0,
method: methodUpperCase,
headers: finalHeaders,
...mergedClientOpts.init
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: {
code: "UNKNOWN",
message: response.statusText
} }));
const errorCode = errorData?.error?.code || "UNKNOWN";
const errorMessage = errorData?.error?.message || response.statusText;
throw new HttpClientError({
code: errorCode,
status: response.status,
procedureName: opts.procedureName,
message: errorMessage
});
}
if (response.headers.get("content-length") === "0" || response.status === 204) return;
if ((response.headers.get("content-type") || "").includes("application/json")) return opts.transformer.output.deserialize(await response.json());
return response.text();
}
//#endregion
//#region src/crpc/transformer.ts
const isPlainObject = (value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
const CODEC_MARKER_KEY = "__crpc";
const CODEC_MARKER_VALUE = 1;
const CODEC_TAG_KEY = "t";
const CODEC_VALUE_KEY = "v";
const hasOnlyCodecPayloadKeys = (value) => {
let keyCount = 0;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
keyCount += 1;
if (key !== CODEC_MARKER_KEY && key !== CODEC_TAG_KEY && key !== CODEC_VALUE_KEY) return false;
if (keyCount > 3) return false;
}
return keyCount === 3;
};
/**
* Date wire tag (Convex-style reserved key).
*/
const DATE_CODEC_TAG = "$date";
/**
* Built-in Date codec.
*/
const dateWireCodec = {
tag: DATE_CODEC_TAG,
isType: (value) => value instanceof Date,
encode: (value) => value.getTime(),
decode: (value) => {
if (typeof value !== "number") return value;
return new Date(value);
}
};
/**
* Build a recursive tagged transformer from codecs.
*/
const createTaggedTransformer = (codecs) => {
const codecByTag = /* @__PURE__ */ new Map();
for (const codec of codecs) {
if (!codec.tag.startsWith("$")) throw new Error(`Invalid wire codec tag '${codec.tag}'. Tags must start with '$'.`);
if (codecByTag.has(codec.tag)) throw new Error(`Duplicate wire codec tag '${codec.tag}'.`);
codecByTag.set(codec.tag, codec);
}
const serialize = (value) => {
for (const codec of codecs) if (codec.isType(value)) return {
[CODEC_MARKER_KEY]: CODEC_MARKER_VALUE,
[CODEC_TAG_KEY]: codec.tag,
[CODEC_VALUE_KEY]: serialize(codec.encode(value))
};
if (Array.isArray(value)) {
let result;
for (let index = 0; index < value.length; index += 1) {
const item = value[index];
const serialized = serialize(item);
if (serialized !== item) {
if (!result) result = value.slice();
result[index] = serialized;
}
}
return result ?? value;
}
if (isPlainObject(value)) {
let result;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
const nested = value[key];
const serialized = serialize(nested);
if (serialized !== nested) {
if (!result) result = { ...value };
result[key] = serialized;
}
}
return result ?? value;
}
return value;
};
const deserialize = (value) => {
if (Array.isArray(value)) {
let result;
for (let index = 0; index < value.length; index += 1) {
const item = value[index];
const deserialized = deserialize(item);
if (deserialized !== item) {
if (!result) result = value.slice();
result[index] = deserialized;
}
}
return result ?? value;
}
if (isPlainObject(value)) {
const marker = value[CODEC_MARKER_KEY];
const tag = value[CODEC_TAG_KEY];
if (marker === CODEC_MARKER_VALUE && typeof tag === "string" && CODEC_VALUE_KEY in value && hasOnlyCodecPayloadKeys(value)) {
const codec = codecByTag.get(tag);
if (codec) return codec.decode(deserialize(value[CODEC_VALUE_KEY]));
}
let result;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
const nested = value[key];
const deserialized = deserialize(nested);
if (deserialized !== nested) {
if (!result) result = { ...value };
result[key] = deserialized;
}
}
return result ?? value;
}
return value;
};
return {
serialize,
deserialize
};
};
/**
* Default cRPC transformer (Date-enabled).
*/
const defaultCRPCTransformer = createTaggedTransformer([dateWireCodec]);
const DEFAULT_COMBINED_TRANSFORMER = {
input: defaultCRPCTransformer,
output: defaultCRPCTransformer
};
/**
* Normalize transformer config to split input/output shape.
*/
const normalizeCustomTransformer = (transformer) => {
if (!transformer) return;
if ("input" in transformer && "output" in transformer) return transformer;
return {
input: transformer,
output: transformer
};
};
/**
* Compose user transformer with default Date transformer.
*
* Date transformer is always active:
* - serialize: user -> default(Date)
* - deserialize: default(Date) -> user
*/
const composeWithDefault = (transformer) => {
if (!transformer) return defaultCRPCTransformer;
return {
serialize: (value) => defaultCRPCTransformer.serialize(transformer.serialize(value)),
deserialize: (value) => transformer.deserialize(defaultCRPCTransformer.deserialize(value))
};
};
const transformerCache = /* @__PURE__ */ new WeakMap();
/**
* Normalize transformer config to split input/output shape.
* User transformers are additive and always composed with default Date handling.
*/
const getTransformer = (transformer) => {
if (!transformer) return DEFAULT_COMBINED_TRANSFORMER;
const cacheKey = transformer;
const cached = transformerCache.get(cacheKey);
if (cached) return cached;
const custom = normalizeCustomTransformer(transformer);
const resolved = {
input: composeWithDefault(custom?.input),
output: composeWithDefault(custom?.output)
};
transformerCache.set(cacheKey, resolved);
return resolved;
};
//#endregion
//#region src/solid/http-proxy.ts
/**
* Create a recursive proxy for HTTP routes with TanStack Query integration.
*
* Terminal methods:
* - GET endpoints: `queryOptions`, `queryKey`
* - POST/PUT/PATCH/DELETE: `mutationOptions`, `mutationKey`
*/
function createRecursiveHttpProxy(opts, path = []) {
return new Proxy(() => {}, { get(_, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
const routeKey = path.join(".");
const route = opts.routes[routeKey];
if (prop === "query") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
return async (args) => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
};
}
if (prop === "mutate") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
return async (args) => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
};
}
if (prop === "queryOptions") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
if (route.method !== "GET") throw new Error(`queryOptions is only available for GET endpoints, got ${route.method} for ${routeKey}`);
return (args, queryOpts) => ({
...queryOpts,
queryKey: [
"httpQuery",
routeKey,
args
],
queryFn: async () => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
}
});
}
if (prop === "queryKey") return (args) => {
return args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0) ? [
"httpQuery",
routeKey,
args
] : ["httpQuery", routeKey];
};
if (prop === "queryFilter") return (args, filters) => {
const hasArgs = args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0);
return {
...filters,
queryKey: hasArgs ? [
"httpQuery",
routeKey,
args
] : ["httpQuery", routeKey]
};
};
if (prop === "mutationOptions") {
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
return (mutationOpts) => ({
...mutationOpts,
mutationKey: ["httpMutation", routeKey],
mutationFn: async (args) => {
try {
return await executeHttpRequest({
convexSiteUrl: opts.convexSiteUrl,
route,
procedureName: routeKey,
args,
baseHeaders: opts.headers,
baseFetch: opts.fetch,
transformer: opts.transformer
});
} catch (error) {
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
throw error;
}
}
});
}
if (prop === "mutationKey") return () => ["httpMutation", routeKey];
return createRecursiveHttpProxy(opts, [...path, prop]);
} });
}
/**
* Create an HTTP proxy with TanStack Query integration for SolidJS.
*
* Returns a proxy that provides:
* - `queryOptions` for GET endpoints (no subscription)
* - `mutationOptions` for POST/PUT/PATCH/DELETE endpoints
*
* @example
* ```ts
* const httpProxy = createHttpProxy<AppRouter>({
* convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL,
* routes: httpRoutes,
* });
*
* // GET endpoint
* const opts = httpProxy.todos.get.queryOptions({ id: '123' });
* const query = createQuery(() => opts);
*
* // POST endpoint
* const mutation = createMutation(() => httpProxy.todos.create.mutationOptions());
* await mutation.mutateAsync({ title: 'New todo' });
* ```
*/
function createHttpProxy(opts) {
const transformer = getTransformer(opts.transformer);
return createRecursiveHttpProxy({
convexSiteUrl: opts.convexSiteUrl,
routes: opts.routes,
headers: opts.headers,
fetch: opts.fetch,
onError: opts.onError,
transformer
});
}
//#endregion
//#region src/crpc/query-options.ts
/**
* Query options factory for Convex query function subscriptions.
* Requires `convexQueryClient.queryFn()` set as the default `queryFn` globally.
*/
function convexQuery(funcRef, args, meta, opts) {
const finalArgs = args ?? {};
const isSkip = finalArgs === "skip";
const funcName = getFunctionName(funcRef);
const [namespace, fnName] = funcName.split(":");
const authType = meta?.[namespace]?.[fnName]?.auth;
const skipUnauth = opts?.skipUnauth;
return {
queryKey: [
"convexQuery",
funcName,
isSkip ? "skip" : finalArgs
],
staleTime: Number.POSITIVE_INFINITY,
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
...isSkip ? { enabled: false } : {},
meta: {
authType,
skipUnauth,
subscribe: true
}
};
}
/**
* Query options factory for Convex action functions.
* Actions are NOT reactive - they follow normal TanStack Query semantics.
*
* @example
* ```ts
* useQuery(convexAction(api.ai.generate, { prompt }))
* ```
*
* @example With additional options (use spread):
* ```ts
* useQuery({
* ...convexAction(api.files.process, { fileId }),
* staleTime: 60_000
* });
* ```
*/
function convexAction(funcRef, args, meta, opts) {
const finalArgs = args ?? {};
const isSkip = finalArgs === "skip";
const funcName = getFunctionName(funcRef);
const [namespace, fnName] = funcName.split(":");
const authType = meta?.[namespace]?.[fnName]?.auth;
const skipUnauth = opts?.skipUnauth;
return {
queryKey: [
"convexAction",
funcName,
isSkip ? {} : finalArgs
],
staleTime: Number.POSITIVE_INFINITY,
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
...isSkip ? { enabled: false } : {},
meta: {
authType,
skipUnauth,
subscribe: false
}
};
}
/**
* Infinite query options factory for paginated Convex queries.
* Server-safe (non-hook) - can be used in RSC.
*
* Uses flat { cursor, limit } input like tRPC.
*/
function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
const { limit, skipUnauth, enabled, ...queryOptions } = opts;
const finalArgs = args === "skip" ? {} : args;
const isSkip = args === "skip";
const funcName = getFunctionName(funcRef);
const [namespace, fnName] = funcName.split(":");
const authType = (meta?.[namespace]?.[fnName])?.auth;
const firstPageArgs = {
...finalArgs,
cursor: null,
limit
};
const finalEnabled = enabled === false || isSkip ? false : void 0;
return {
queryKey: [
"convexQuery",
funcName,
firstPageArgs
],
staleTime: Number.POSITIVE_INFINITY,
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
...queryOptions,
...finalEnabled === false ? { enabled: false } : {},
meta: {
authType,
skipUnauth,
subscribe: true,
queryName: funcName,
args: finalArgs,
limit
}
};
}
//#endregion
//#region src/crpc/types.ts
/** Symbol key for attaching FunctionReference to options (non-serializable) */
const FUNC_REF_SYMBOL = Symbol.for("convex.funcRef");
//#endregion
//#region src/solid/convex-solid.tsx
/** @jsxImportSource solid-js */
/**
* Convex Provider for SolidJS
*
* Provides ConvexClient via context and auth integration.
*/
const ConvexContext = createContext();
/** Get the ConvexClient instance from context */
function useConvex() {
const client = useContext(ConvexContext);
if (!client) throw new Error("useConvex must be used within a ConvexProvider");
return client;
}
function ConvexProvider(props) {
return createComponent(ConvexContext.Provider, {
get value() {
return props.client;
},
get children() {
return props.children;
}
});
}
function ConvexProviderWithAuth(props) {
const client = props.client;
const [isConvexLoading, setIsConvexLoading] = createSignal(true);
const [isConvexAuthenticated, setIsConvexAuthenticated] = createSignal(false);
createEffect(() => {
const auth = props.useAuth();
const loading = auth.isLoading;
const authenticated = auth.isAuthenticated;
if (loading) return;
if (!authenticated) {
client.clearAuth();
setIsConvexLoading(false);
setIsConvexAuthenticated(false);
return;
}
client.setAuth(auth.fetchAccessToken, (isAuth) => {
setIsConvexLoading(false);
setIsConvexAuthenticated(isAuth);
});
});
onCleanup(() => {
client.clearAuth();
});
return createComponent(ConvexContext.Provider, {
get value() {
return props.client;
},
get children() {
return createComponent(ConvexAuthBridge, {
get isAuthenticated() {
return isConvexAuthenticated();
},
get isLoading() {
return isConvexLoading();
},
get children() {
return props.children;
}
});
}
});
}
/** Hook returning auth state from Convex */
function useConvexAuth() {
const bridge = useConvexAuthBridge();
if (!bridge) return {
isLoading: false,
isAuthenticated: false
};
return bridge;
}
//#endregion
//#region src/solid/use-query-options.ts
/**
* Query options factories for Convex functions (SolidJS).
* Port of the React version — uses ConvexClient directly instead of convex/react hooks.
*
* Note: In @tanstack/solid-query, UseMutationOptions and UseQueryOptions are
* Accessor wrappers. We use SolidMutationOptions and SolidQueryOptions (plain
* object types) for parameters and return values.
*/
/**
* Hook that returns query options for use with useQuery.
* Handles skipUnauth by setting enabled: false when unauthorized.
*
* @example
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }));
* ```
*
* @example With skipToken for conditional queries
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, userId ? { id: userId } : skipToken));
* ```
*
* @example With skipUnauth
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { skipUnauth: true }));
* ```
*
* @example With TanStack Query options
* ```tsx
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { enabled: !!id, placeholderData: [] }));
* ```
*/
function useConvexQueryOptions(funcRef, args, options) {
const isSkipped = args === skipToken;
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
const authSkip = useAuthSkip(funcRef, {
enabled: isSkipped ? false : enabled,
skipUnauth: options?.skipUnauth
});
const baseOptions = convexQuery(funcRef, isSkipped ? {} : args);
const { skipUnauth: _, subscribe, ...queryOptions } = options ?? {};
return {
...baseOptions,
...queryOptions,
enabled: isSkipped ? false : !authSkip.shouldSkip,
meta: {
...baseOptions.meta,
authType: authSkip.authType,
subscribe: subscribe !== false
}
};
}
/**
* Hook that returns infinite query options for use with useInfiniteQuery.
* Handles auth type detection from meta and skipUnauth.
*
* @example
* ```tsx
* const { data } = useInfiniteQuery(
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20 })
* );
* ```
*
* @example With skipToken for conditional queries
* ```tsx
* const { data } = useInfiniteQuery(
* useConvexInfiniteQueryOptions(api.posts.list, userId ? { userId } : skipToken, { limit: 20 })
* );
* ```
*
* @example With skipUnauth
* ```tsx
* const { data } = useInfiniteQuery(
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20, skipUnauth: true })
* );
* ```
*/
function useConvexInfiniteQueryOptions(funcRef, args, opts) {
const meta = useMeta();
const isSkipped = args === skipToken;
const enabledOpt = typeof opts.enabled === "function" ? void 0 : opts.enabled;
const authSkip = useAuthSkip(funcRef, {
enabled: isSkipped ? false : enabledOpt,
skipUnauth: opts.skipUnauth
});
const enabled = isSkipped || authSkip.shouldSkip ? false : enabledOpt;
const baseOptions = convexInfiniteQueryOptions(funcRef, isSkipped ? {} : args, {
...opts,
enabled
}, meta);
return {
...baseOptions,
meta: {
...baseOptions.meta,
authType: authSkip.authType
}
};
}
/**
* Hook that returns query options for using an action as a one-shot query.
* Actions don't support WebSocket subscriptions - they're one-time calls.
*
* @example
* ```tsx
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }));
* ```
*
* @example With skipToken for conditional queries
* ```tsx
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, id ? { id } : skipToken));
* ```
*
* @example With skipUnauth
* ```tsx
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }, { skipUnauth: true }));
* ```
*/
function useConvexActionQueryOptions(action, args, options) {
const isSkipped = args === skipToken;
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
const authSkip = useAuthSkip(action, {
enabled: isSkipped ? false : enabled,
skipUnauth: options?.skipUnauth
});
const baseOptions = convexAction(action, isSkipped ? {} : args);
const { skipUnauth: _, ...queryOptions } = options ?? {};
return {
...baseOptions,
...queryOptions,
enabled: isSkipped ? false : !authSkip.shouldSkip
};
}
/**
* Hook that returns mutation options for use with useMutation.
* Wraps the Convex mutation with auth guard logic.
*
* @example
* ```tsx
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update));
* ```
*
* @example With TanStack Query options
* ```tsx
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update, {
* onSuccess: () => toast.success('Updated!'),
* }));
* ```
*/
function useConvexMutationOptions(mutation, options, transformer) {
const guard = useAuthGuard();
const getMeta = useFnMeta();
const name = getFunctionName(mutation);
const [namespace, fnName] = name.split(":");
const authType = getMeta(namespace, fnName)?.auth;
const convex = useConvex();
const resolvedTransformer = getTransformer(transformer);
return {
...options,
mutationFn: async (args) => {
if (authType === "required" && guard()) throw new CRPCClientError({
code: "UNAUTHORIZED",
functionName: name
});
return convex.mutation(mutation, resolvedTransformer.input.serialize(args));
}
};
}
/**
* Hook that returns action options for use with useMutation.
* Wraps the Convex action with auth guard logic.
*
* @example
* ```tsx
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate));
* ```
*
* @example With TanStack Query options
* ```tsx
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate, {
* onSuccess: (data) => console.info(data),
* }));
* ```
*/
function useConvexActionOptions(action, options, transformer) {
const guard = useAuthGuard();
const getMeta = useFnMeta();
const name = getFunctionName(action);
const [namespace, fnName] = name.split(":");
const authType = getMeta(namespace, fnName)?.auth;
const convex = useConvex();
const resolvedTransformer = getTransformer(transformer);
return {
...options,
mutationFn: async (args) => {
if (authType === "required" && guard()) throw new CRPCClientError({
code: "UNAUTHORIZED",
functionName: name
});
return convex.action(action, resolvedTransformer.input.serialize(args));
}
};
}
/**
* Hook that returns upload mutation options for use with useMutation.
* Generates a presigned URL, then uploads the file directly to storage.
*
* @example
* ```tsx
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl));
* mutate({ file, ...otherArgs });
* ```
*
* @example With TanStack Query options
* ```tsx
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl, {
* onSuccess: (result) => console.info('Uploaded:', result.key),
* }));
* ```
*/
function useUploadMutationOptions(generateUrlMutation, options) {
const convex = useConvex();
return {
...options,
mutationFn: async ({ file, ...args }) => {
const result = await convex.mutation(generateUrlMutation, args);
const { url } = result;
const response = await fetch(url, {
body: file,
headers: { "Content-Type": file.type },
method: "PUT"
});
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
return result;
}
};
}
//#endregion
//#region src/solid/proxy.ts
/**
* CRPC Recursive Proxy
*
* Creates a tRPC-like proxy that wraps Convex API functions with
* TanStack Query options builders.
*
* @example
* ```ts
* const crpc = createCRPCOptionsProxy(api);
* const opts = crpc.user.get.queryOptions({ id: '123' });
* const { data } = useQuery(opts);
* ```
*/
/** Get query key prefix based on function type */
function getQueryKeyPrefix(path, meta) {
if (getFunctionType(path, meta) === "action") return "convexAction";
return "convexQuery";
}
/**
* Create a recursive proxy that accumulates path segments.
*/
function createRecursiveProxy(api, path, meta, transformer) {
return new Proxy(() => {}, { get(_target, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
if (prop === "queryOptions") return (args = {}, opts) => {
const funcRef = getFuncRef(api, path);
if (getFunctionType(path, meta) === "action") return useConvexActionQueryOptions(funcRef, args, opts);
return useConvexQueryOptions(funcRef, args, opts);
};
if (prop === "staticQueryOptions") return (args = {}, opts) => {
const funcRef = getFuncRef(api, path);
const fnType = getFunctionType(path, meta);
const finalArgs = args === skipToken ? "skip" : args;
if (fnType === "action") return convexAction(funcRef, finalArgs, meta, opts);
return convexQuery(funcRef, finalArgs, meta, opts);
};
if (prop === "queryKey") return (args = {}) => {
const funcName = getFunctionName(getFuncRef(api, path));
return [
getQueryKeyPrefix(path, meta),
funcName,
args
];
};
if (prop === "queryFilter") return (args, filters) => {
const funcName = getFunctionName(getFuncRef(api, path));
const prefix = getQueryKeyPrefix(path, meta);
return {
...filters,
queryKey: [
prefix,
funcName,
args
]
};
};
if (prop === "infiniteQueryOptions") return (args = {}, opts = {}) => {
const funcRef = getFuncRef(api, path);
const options = useConvexInfiniteQueryOptions(funcRef, args, opts);
Object.defineProperty(options, FUNC_REF_SYMBOL, {
value: funcRef,
enumerable: false,
configurable: false
});
return options;
};
if (prop === "infiniteQueryKey") return (args) => {
return [
"convexQuery",
getFunctionName(getFuncRef(api, path)),
args ?? {}
];
};
if (prop === "meta" && path.length >= 2) return getFunctionMeta(path, meta);
if (prop === "mutationKey") return () => {
return ["convexMutation", getFunctionName(getFuncRef(api, path))];
};
if (prop === "mutationOptions") return (opts) => {
const funcRef = getFuncRef(api, path);
if (getFunctionType(path, meta) === "action") return useConvexActionOptions(funcRef, opts, transformer);
return useConvexMutationOptions(funcRef, opts, transformer);
};
return createRecursiveProxy(api, [...path, prop], meta, transformer);
} });
}
/**
* Create a CRPC proxy for a Convex API object.
*
* The proxy provides a tRPC-like interface for accessing Convex functions
* with TanStack Query options builders.
*
* @param api - The Convex API object (from `@convex/api`)
* @param meta - Generated function metadata for runtime type detection
* @returns A typed proxy with queryOptions/mutationOptions methods
*
* @example
* ```tsx
* import { api } from '@convex/api';
*
* // Usually you should use createCRPCContext({ api }) instead.
* // createCRPCOptionsProxy is a low-level helper.
* const crpc = createCRPCOptionsProxy(api, {} as any);
*
* function MyComponent() {
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
* const { mutate } = useMutation(crpc.user.update.mutationOptions());
* }
* ```
*/
function createCRPCOptionsProxy(api, meta, transformer) {
return createRecursiveProxy(api, [], meta, transformer);
}
//#endregion
//#region src/solid/vanilla-client.ts
/**
* Create a recursive proxy for vanilla (direct) calls.
*/
function createRecursiveVanillaProxy(api, path, meta, convexClient, transformer) {
return new Proxy(() => {}, { get(_target, prop) {
if (typeof prop === "symbol") return;
if (prop === "then") return;
if (prop === "query") return async (args = {}) => {
const funcRef = getFuncRef(api, path);
const fnType = getFunctionType(path, meta);
const wireArgs = transformer.input.serialize(args);
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
return transformer.output.deserialize(await convexClient.query(funcRef, wireArgs));
};
if (prop === "onUpdate") return (args = {}, callback, onError) => {
const funcRef = getFuncRef(api, path);
return convexClient.onUpdate(funcRef, transformer.input.serialize(args), callback ? (result) => callback(transformer.output.deserialize(result)) : () => {}, onError);
};
if (prop === "mutate") return async (args = {}) => {
const funcRef = getFuncRef(api, path);
const fnType = getFunctionType(path, meta);
const wireArgs = transformer.input.serialize(args);
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
return transformer.output.deserialize(await convexClient.mutation(funcRef, wireArgs));
};
return createRecursiveVanillaProxy(api, [...path, prop], meta, convexClient, transformer);
} });
}
/**
* Create a vanilla CRPC proxy for direct procedural calls.
*
* The proxy provides a tRPC-like interface for imperative Convex function calls.
*
* @param api - The Convex API object (from `@convex/api`)
* @param meta - Generated function metadata for runtime type detection
* @param convexClient - The ConvexClient instance
* @returns A typed proxy with query/mutate methods
*
* @example
* ```tsx
* const client = createVanillaCRPCProxy(api, meta, convexClient);
*
* // Direct calls (no Solid Query)
* const user = await client.user.get.query({ id });
* await client.user.update.mutate({ id, name: 'test' });
* ```
*/
function createVanillaCRPCProxy(api, meta, convexClient, transformer) {
return createRecursiveVanillaProxy(api, [], meta, convexClient, getTransformer(transformer));
}
//#endregion
//#region src/solid/context.tsx
/** @jsxImportSource solid-js */
/**
* CRPC Context and Provider
*
* Provides Solid context for the CRPC proxy, similar to tRPC's createTRPCContext.
*/
const ConvexQueryClientContext = createContext(null);
/** Access ConvexQueryClient (e.g., for logout cleanup) */
const useConvexQueryClient = () => useContext(ConvexQueryClientContext);
/** Headers record that allows empty objects and optional properties */
/**
* Extract HTTP router from TApi['http'] if present (optional).
* Uses NonNullable to handle optional http property.
*/
/**
* Create CRPC context, provider, and hooks for a Convex API.
*
* @param options - Configuration object containing api and optional HTTP settings
* @returns Object with CRPCProvider, useCRPC, and useCRPCClient
*
* @example
* ```tsx
* // lib/crpc.ts
* import { api } from '@convex/api';
* import { createCRPCContext } from 'kitcn/solid';
*
* // Works for both regular Convex functions and generated HTTP router types
* export const { useCRPC } = createCRPCContext({
* api,
* convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL,
* });
*
* // components/user-profile.tsx
* function UserProfile({ id }) {
* const crpc = useCRPC();
* const { data } = createQuery(() => crpc.user.get.queryOptions({ id }));
*
* // HTTP endpoints (if configured)
* const { data: httpData } = createQuery(() => crpc.http.todos.get.queryOptions({ id }));
* }
* ```
*/
function createCRPCContext(options) {
const { api, ...httpOptions } = options;
const meta = buildMetaIndex(api);
const CRPCProxyContext = createContext(null);
const VanillaClientContext = createContext(null);
const HttpProxyContext = createContext(void 0);
function CRPCProvider(props) {
const authStore = useAuthStore();
const fetchAccessToken = useFetchAccessToken();
const proxy = createCRPCOptionsProxy(api, meta, options.transformer);
const vanillaClient = createVanillaCRPCProxy(api, meta, props.convexClient, options.transformer);
const httpProxy = (() => {
if (!httpOptions.convexSiteUrl || !meta._http) return;
return createHttpProxy({
convexSiteUrl: httpOptions.convexSiteUrl,
routes: meta._http,
headers: async () => {
const token = authStore.get("token");
const expiresAt = authStore.get("expiresAt");
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
if (token && expiresAt && timeRemaining >= 6e4) return {
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
Authorization: `Bearer ${token}`
};
if (fetchAccessToken) {
const newToken = await fetchAccessToken({ forceRefreshToken: !!expiresAt });
if (newToken) return {
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
Authorization: `Bearer ${newToken}`
};
}
return { ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers };
},
fetch: httpOptions.fetch,
onError: httpOptions.onError,
transformer: options.transformer
});
})();
return createComponent(ConvexQueryClientContext.Provider, {
get value() {
return props.convexQueryClient;
},
get children() {
return createComponent(MetaContext.Provider, {
value: meta,
get children() {
return createComponent(VanillaClientContext.Provider, {
value: vanillaClient,
get children() {
return createComponent(HttpProxyContext.Provider, {
value: httpProxy,
get children() {
return createComponent(CRPCProxyContext.Provider, {
value: proxy,
get children() {
return props.children;
}
});
}
});
}
});
}
});
}
});
}
function useCRPC() {
const ctx = useContext(CRPCProxyContext);
const httpProxy = useContext(HttpProxyContext);
if (!ctx) throw new Error("useCRPC must be used within CRPCProvider");
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
if (prop === "http") return httpProxy;
return Reflect.get(target, prop);
} });
return ctx;
}
function useCRPCClient() {
const ctx = useContext(VanillaClientContext);
const httpProxy = useContext(HttpProxyContext);
if (!ctx) throw new Error("useCRPCClient must be used within CRPCProvider");
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
if (prop === "http") return httpProxy;
return Reflect.get(target, prop);
} });
return ctx;
}
return {
CRPCProvider,
useCRPC,
useCRPCClient
};
}
//#endregion
//#region src/crpc/auth-error.ts
/**
* Auth Mutation Error
*
* Framework-agnostic error class for Better Auth mutations.
*/
/**
* Error thrown when a Better Auth mutation fails.
* Contains the original error details from Better Auth.
*/
var AuthMutationError = class extends Error {
/** Error code from Better Auth (e.g., 'INVALID_PASSWORD', 'EMAIL_ALREADY_REGISTERED') */
code;
/** HTTP status code */
status;
/** HTTP status text */
statusText;
constructor(authError) {
super(authError.message || authError.statusText);
this.name = "AuthMutationError";
this.code = authError.code;
this.status = authError.status;
this.statusText = authError.statusTe