better-convex 0.0.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,9 @@ type ConvexAuthProviderProps = {
25
25
  /**
26
26
  * Unified auth provider for Convex + Better Auth.
27
27
  * Handles token sync, HMR persistence, and auth callbacks.
28
+ *
29
+ * Structure: AuthProvider wraps ConvexAuthProviderInner so that
30
+ * useAuthStore() is available when creating fetchAccessToken.
28
31
  */
29
32
  declare function ConvexAuthProvider({
30
33
  children,
@@ -1,9 +1,9 @@
1
1
  'use client';
2
- import { _ as defaultIsUnauthorized, c as persistToken, f as useAuthStore, g as CRPCClientError, p as useAuthValue, s as getPersistedToken, t as AuthProvider } from "../auth-store-DANFmEdk.js";
2
+ import { _ as defaultIsUnauthorized, d as useAuthStore, f as useAuthValue, g as CRPCClientError, r as FetchAccessTokenContext, s as decodeJwtExp, t as AuthProvider } from "../auth-store-BMSJFMtR.js";
3
3
  import { c } from "react/compiler-runtime";
4
- import { ConvexProviderWithAuth } from "convex/react";
4
+ import { ConvexProviderWithAuth, useConvexAuth } from "convex/react";
5
5
  import { useCallback, useEffect, useMemo } from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { jsx } from "react/jsx-runtime";
7
7
 
8
8
  //#region src/auth-client/convex-auth-provider.tsx
9
9
  /**
@@ -18,122 +18,171 @@ const defaultMutationHandler = () => {
18
18
  /**
19
19
  * Unified auth provider for Convex + Better Auth.
20
20
  * Handles token sync, HMR persistence, and auth callbacks.
21
+ *
22
+ * Structure: AuthProvider wraps ConvexAuthProviderInner so that
23
+ * useAuthStore() is available when creating fetchAccessToken.
21
24
  */
22
25
  function ConvexAuthProvider(t0) {
23
- const $ = c(17);
26
+ const $ = c(15);
24
27
  const { children, client, authClient, initialToken, onMutationUnauthorized, onQueryUnauthorized, isUnauthorized } = t0;
28
+ useOTTHandler(authClient);
25
29
  let t1;
26
30
  if ($[0] !== initialToken) {
27
- t1 = initialToken ?? getPersistedToken();
31
+ t1 = initialToken ? decodeJwtExp(initialToken) : null;
28
32
  $[0] = initialToken;
29
33
  $[1] = t1;
30
34
  } else t1 = $[1];
31
- const effectiveToken = t1;
32
- if (effectiveToken) persistToken(effectiveToken);
33
- const useAuth = useCreateConvexAuth(authClient);
34
- useOTTHandler(authClient);
35
- const t2 = client;
36
- const t3 = effectiveToken ?? null;
37
- let t4;
38
- if ($[2] !== t3) {
39
- t4 = {
40
- isLoading: true,
41
- token: t3
35
+ const t2 = initialToken ?? null;
36
+ let t3;
37
+ if ($[2] !== t1 || $[3] !== t2) {
38
+ t3 = {
39
+ expiresAt: t1,
40
+ token: t2
42
41
  };
43
- $[2] = t3;
44
- $[3] = t4;
45
- } else t4 = $[3];
46
- const t5 = isUnauthorized ?? defaultIsUnauthorized;
47
- const t6 = onMutationUnauthorized ?? defaultMutationHandler;
48
- const t7 = onQueryUnauthorized ?? _temp;
49
- let t8;
50
- if ($[4] !== authClient) {
51
- t8 = /* @__PURE__ */ jsx(AuthSyncEffect, { authClient });
52
- $[4] = authClient;
53
- $[5] = t8;
54
- } else t8 = $[5];
55
- let t9;
56
- if ($[6] !== children || $[7] !== t4 || $[8] !== t5 || $[9] !== t6 || $[10] !== t7 || $[11] !== t8) {
57
- t9 = /* @__PURE__ */ jsxs(AuthProvider, {
58
- initialValues: t4,
59
- isUnauthorized: t5,
60
- onMutationUnauthorized: t6,
61
- onQueryUnauthorized: t7,
62
- children: [t8, children]
42
+ $[2] = t1;
43
+ $[3] = t2;
44
+ $[4] = t3;
45
+ } else t3 = $[4];
46
+ const tokenValues = t3;
47
+ const t4 = isUnauthorized ?? defaultIsUnauthorized;
48
+ const t5 = onMutationUnauthorized ?? defaultMutationHandler;
49
+ const t6 = onQueryUnauthorized ?? _temp;
50
+ let t7;
51
+ if ($[5] !== authClient || $[6] !== children || $[7] !== client) {
52
+ t7 = /* @__PURE__ */ jsx(ConvexAuthProviderInner, {
53
+ authClient,
54
+ client,
55
+ children
63
56
  });
57
+ $[5] = authClient;
64
58
  $[6] = children;
65
- $[7] = t4;
66
- $[8] = t5;
67
- $[9] = t6;
68
- $[10] = t7;
69
- $[11] = t8;
70
- $[12] = t9;
71
- } else t9 = $[12];
72
- let t10;
73
- if ($[13] !== t2 || $[14] !== t9 || $[15] !== useAuth) {
74
- t10 = /* @__PURE__ */ jsx(ConvexProviderWithAuth, {
75
- client: t2,
76
- useAuth,
77
- children: t9
59
+ $[7] = client;
60
+ $[8] = t7;
61
+ } else t7 = $[8];
62
+ let t8;
63
+ if ($[9] !== t4 || $[10] !== t5 || $[11] !== t6 || $[12] !== t7 || $[13] !== tokenValues) {
64
+ t8 = /* @__PURE__ */ jsx(AuthProvider, {
65
+ initialValues: tokenValues,
66
+ isUnauthorized: t4,
67
+ onMutationUnauthorized: t5,
68
+ onQueryUnauthorized: t6,
69
+ children: t7
78
70
  });
79
- $[13] = t2;
80
- $[14] = t9;
81
- $[15] = useAuth;
82
- $[16] = t10;
83
- } else t10 = $[16];
84
- return t10;
71
+ $[9] = t4;
72
+ $[10] = t5;
73
+ $[11] = t6;
74
+ $[12] = t7;
75
+ $[13] = tokenValues;
76
+ $[14] = t8;
77
+ } else t8 = $[14];
78
+ return t8;
85
79
  }
86
80
  /**
87
- * Syncs Better Auth session to auth-store.
88
- * Automatically handles login/logout token updates.
81
+ * Inner provider that has access to AuthStore via useAuthStore().
82
+ * Creates fetchAccessToken and passes it through context (no race condition).
89
83
  */
90
84
  function _temp() {}
91
- function AuthSyncEffect({ authClient }) {
92
- const session = authClient.useSession();
85
+ function ConvexAuthProviderInner({ children, client, authClient }) {
93
86
  const authStore = useAuthStore();
94
- useEffect(() => {
95
- if (!session.isPending) {
96
- const token = session.data?.session.token ?? null;
97
- if (token !== authStore.get("token")) {
98
- authStore.set("token", token);
99
- persistToken(token);
87
+ const { data: session, isPending } = authClient.useSession();
88
+ const fetchAccessToken = useCallback(async ({ forceRefreshToken = false } = {}) => {
89
+ if (!session) {
90
+ if (!isPending) {
91
+ authStore.set("token", null);
92
+ authStore.set("expiresAt", null);
100
93
  }
94
+ return authStore.get("token");
95
+ }
96
+ const cachedToken = authStore.get("token");
97
+ const expiresAt = authStore.get("expiresAt");
98
+ const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
99
+ if (!forceRefreshToken && cachedToken && expiresAt && timeRemaining >= 6e4) return cachedToken;
100
+ try {
101
+ const { data } = await authClient.convex.token();
102
+ const jwt = data?.token || null;
103
+ if (jwt) {
104
+ const exp = decodeJwtExp(jwt);
105
+ authStore.set("token", jwt);
106
+ authStore.set("expiresAt", exp);
107
+ }
108
+ return jwt;
109
+ } catch {
110
+ return null;
101
111
  }
102
112
  }, [
103
- session.data,
104
- session.isPending,
105
- authStore
113
+ session,
114
+ isPending,
115
+ authStore,
116
+ authClient
106
117
  ]);
107
- return null;
108
- }
109
- /**
110
- * Creates useAuth hook for ConvexProviderWithAuth.
111
- * Uses auth-store token as single source of truth.
112
- */
113
- function useCreateConvexAuth(authClient) {
114
- return useMemo(() => function useConvexAuth$1() {
115
- const { data: session, isPending: isSessionPending } = authClient.useSession();
116
- const token = useAuthValue("token");
117
- const sessionId = session?.session?.id;
118
- const fetchAccessToken = useCallback(async ({ forceRefreshToken = false } = {}) => {
119
- if (token && !forceRefreshToken) return token;
120
- try {
121
- const { data } = await authClient.convex.token();
122
- return data?.token || null;
123
- } catch {
124
- return null;
125
- }
126
- }, [sessionId, token]);
118
+ const useAuth = useMemo(() => function useConvexAuthHook() {
127
119
  return useMemo(() => ({
128
- isLoading: isSessionPending,
120
+ isLoading: isPending,
129
121
  isAuthenticated: session !== null,
130
122
  fetchAccessToken
131
123
  }), [
132
- isSessionPending,
133
- sessionId,
124
+ isPending,
125
+ session,
134
126
  fetchAccessToken
135
127
  ]);
136
- }, [authClient]);
128
+ }, [
129
+ isPending,
130
+ session,
131
+ fetchAccessToken
132
+ ]);
133
+ return /* @__PURE__ */ jsx(FetchAccessTokenContext.Provider, {
134
+ value: fetchAccessToken,
135
+ children: /* @__PURE__ */ jsx(ConvexProviderWithAuth, {
136
+ client,
137
+ useAuth,
138
+ children: /* @__PURE__ */ jsx(AuthStateSync, { children })
139
+ })
140
+ });
141
+ }
142
+ /**
143
+ * Syncs auth state from useConvexAuth() to the auth store.
144
+ * MUST be inside ConvexProviderWithAuth to access useConvexAuth().
145
+ *
146
+ * Defensive isLoading computation handles SSR hydration race:
147
+ * 1. SSR sets token from cookie
148
+ * 2. Client hydrates
149
+ * 3. Better Auth's useSession() briefly returns null before loading cookie
150
+ * 4. Convex sets isConvexAuthenticated = false (no auth to wait for)
151
+ * 5. Without defensive check, we'd sync { isLoading: false, isAuthenticated: false }
152
+ * 6. Queries would throw UNAUTHORIZED before token is validated
153
+ */
154
+ function AuthStateSync(t0) {
155
+ const $ = c(6);
156
+ const { children } = t0;
157
+ const { isLoading: convexIsLoading, isAuthenticated } = useConvexAuth();
158
+ const authStore = useAuthStore();
159
+ const token = useAuthValue("token");
160
+ let t1;
161
+ let t2;
162
+ if ($[0] !== authStore || $[1] !== convexIsLoading || $[2] !== isAuthenticated || $[3] !== token) {
163
+ t1 = () => {
164
+ const isLoading = convexIsLoading || !!token && !isAuthenticated;
165
+ authStore.set("isLoading", isLoading);
166
+ authStore.set("isAuthenticated", isAuthenticated);
167
+ };
168
+ t2 = [
169
+ convexIsLoading,
170
+ isAuthenticated,
171
+ token,
172
+ authStore
173
+ ];
174
+ $[0] = authStore;
175
+ $[1] = convexIsLoading;
176
+ $[2] = isAuthenticated;
177
+ $[3] = token;
178
+ $[4] = t1;
179
+ $[5] = t2;
180
+ } else {
181
+ t1 = $[4];
182
+ t2 = $[5];
183
+ }
184
+ useEffect(t1, t2);
185
+ return children;
137
186
  }
138
187
  /**
139
188
  * Handles cross-domain one-time token (OTT) verification.
@@ -1,3 +1,4 @@
1
+ import "../http-types-BwEQgYV1.js";
1
2
  import { CallerMeta, ConvexContext, LazyCaller } from "../server/index.js";
2
3
  import { GetTokenOptions } from "@convex-dev/better-auth/utils";
3
4
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { c } from "react/compiler-runtime";
3
3
  import { useConvexAuth } from "convex/react";
4
- import React from "react";
4
+ import { createContext, useContext } from "react";
5
5
  import { createAtomStore } from "jotai-x";
6
6
 
7
7
  //#region src/crpc/error.ts
@@ -40,59 +40,21 @@ const defaultIsUnauthorized = (error) => {
40
40
  * Provides token storage and auth callback configuration.
41
41
  * App configures handlers, lib hooks consume state.
42
42
  */
43
- const globalStore = globalThis;
44
- const AUTH_TOKEN_KEY = Symbol.for("convex.authToken");
45
- /** Get persisted token from globalThis (survives HMR) */
46
- const getPersistedToken = () => {
47
- if (typeof window === "undefined") return null;
48
- return globalStore[AUTH_TOKEN_KEY] ?? null;
43
+ const FetchAccessTokenContext = createContext(null);
44
+ /** Get fetchAccessToken from context (available immediately, no race condition) */
45
+ const useFetchAccessToken = () => {
46
+ return useContext(FetchAccessTokenContext);
49
47
  };
50
- /** Persist token to globalThis (survives HMR) */
51
- const persistToken = (token) => {
52
- if (typeof window !== "undefined") globalStore[AUTH_TOKEN_KEY] = token;
53
- };
54
- function AuthEffect() {
55
- const $ = c(7);
56
- const authStore$1 = useAuthStore();
57
- const { isAuthenticated, isLoading } = useConvexAuth();
58
- const token = useAuthValue("token");
59
- const onMutationUnauthorized = useAuthValue("onMutationUnauthorized");
60
- let t0;
61
- let t1;
62
- if ($[0] !== authStore$1 || $[1] !== isAuthenticated || $[2] !== isLoading || $[3] !== onMutationUnauthorized || $[4] !== token) {
63
- t0 = () => {
64
- authStore$1.set("isLoading", isLoading);
65
- authStore$1.set("isAuthenticated", isAuthenticated && !!token);
66
- authStore$1.set("guard", (callback) => {
67
- if (!token) {
68
- onMutationUnauthorized();
69
- return true;
70
- }
71
- return callback ? void callback() : false;
72
- });
73
- };
74
- t1 = [
75
- isLoading,
76
- isAuthenticated,
77
- token,
78
- onMutationUnauthorized,
79
- authStore$1
80
- ];
81
- $[0] = authStore$1;
82
- $[1] = isAuthenticated;
83
- $[2] = isLoading;
84
- $[3] = onMutationUnauthorized;
85
- $[4] = token;
86
- $[5] = t0;
87
- $[6] = t1;
88
- } else {
89
- t0 = $[5];
90
- t1 = $[6];
48
+ /** Decode JWT expiration (ms timestamp) from token */
49
+ function decodeJwtExp(token) {
50
+ try {
51
+ const payload = JSON.parse(atob(token.split(".")[1]));
52
+ return payload.exp ? payload.exp * 1e3 : null;
53
+ } catch {
54
+ return null;
91
55
  }
92
- React.useEffect(t0, t1);
93
- return null;
94
56
  }
95
- const { authStore, AuthProvider, useAuthStore, useAuthState, useAuthValue } = createAtomStore({
57
+ const { AuthProvider, useAuthStore, useAuthState, useAuthValue } = createAtomStore({
96
58
  onMutationUnauthorized: () => {
97
59
  throw new CRPCClientError({
98
60
  code: "UNAUTHORIZED",
@@ -101,33 +63,30 @@ const { authStore, AuthProvider, useAuthStore, useAuthState, useAuthValue } = cr
101
63
  },
102
64
  onQueryUnauthorized: () => {},
103
65
  isUnauthorized: defaultIsUnauthorized,
104
- token: getPersistedToken(),
105
- enabled: false,
106
- isLoading: false,
107
- isAuthenticated: false,
108
- guard: () => false
66
+ token: null,
67
+ expiresAt: null,
68
+ isLoading: true,
69
+ isAuthenticated: false
109
70
  }, {
110
- effect: AuthEffect,
111
71
  name: "auth",
112
72
  suppressWarnings: true
113
73
  });
114
74
  const useAuth = () => {
115
- const authStore$1 = useAuthStore();
116
- if (!authStore$1.store) return {
75
+ const authStore = useAuthStore();
76
+ if (!authStore.store) return {
117
77
  hasSession: false,
118
78
  isAuthenticated: false,
119
79
  isLoading: false
120
80
  };
121
81
  if (typeof window === "undefined") return {
122
- hasSession: !!authStore$1.get("token"),
82
+ hasSession: !!authStore.get("token"),
123
83
  isAuthenticated: false,
124
84
  isLoading: true
125
85
  };
126
- const { isAuthenticated, isLoading } = useConvexAuth();
127
- const token = useAuthValue("token");
86
+ const { isLoading, isAuthenticated } = useConvexAuth();
128
87
  return {
129
- hasSession: !!token,
130
- isAuthenticated: isAuthenticated && !!token,
88
+ hasSession: !!useAuthValue("token"),
89
+ isAuthenticated,
131
90
  isLoading
132
91
  };
133
92
  };
@@ -143,18 +102,18 @@ const useIsAuth = () => {
143
102
  };
144
103
  const useAuthGuard = () => {
145
104
  const $ = c(3);
146
- const isAuth = useIsAuth();
105
+ const { isAuthenticated } = useConvexAuth();
147
106
  const onMutationUnauthorized = useAuthValue("onMutationUnauthorized");
148
107
  let t0;
149
- if ($[0] !== isAuth || $[1] !== onMutationUnauthorized) {
108
+ if ($[0] !== isAuthenticated || $[1] !== onMutationUnauthorized) {
150
109
  t0 = (callback) => {
151
- if (!isAuth) {
110
+ if (!isAuthenticated) {
152
111
  onMutationUnauthorized();
153
112
  return true;
154
113
  }
155
114
  return callback ? void callback() : false;
156
115
  };
157
- $[0] = isAuth;
116
+ $[0] = isAuthenticated;
158
117
  $[1] = onMutationUnauthorized;
159
118
  $[2] = t0;
160
119
  } else t0 = $[2];
@@ -183,4 +142,4 @@ function Unauthenticated(t0) {
183
142
  }
184
143
 
185
144
  //#endregion
186
- export { defaultIsUnauthorized as _, Unauthenticated as a, persistToken as c, useAuthState as d, useAuthStore as f, CRPCClientError as g, useMaybeAuth as h, MaybeUnauthenticated as i, useAuth as l, useIsAuth as m, Authenticated as n, authStore as o, useAuthValue as p, MaybeAuthenticated as r, getPersistedToken as s, AuthProvider as t, useAuthGuard as u, isCRPCClientError as v };
145
+ export { defaultIsUnauthorized as _, MaybeUnauthenticated as a, useAuth as c, useAuthStore as d, useAuthValue as f, CRPCClientError as g, useMaybeAuth as h, MaybeAuthenticated as i, useAuthGuard as l, useIsAuth as m, Authenticated as n, Unauthenticated as o, useFetchAccessToken as p, FetchAccessTokenContext as r, decodeJwtExp as s, AuthProvider as t, useAuthState as u, isCRPCClientError as v };
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_codegen = require('./codegen-DN-vbQih.cjs');
2
+ const require_codegen = require('./codegen-Cca0gj6d.cjs');
3
3
  let node_module = require("node:module");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_codegen.__toESM(node_path);
@@ -33,6 +33,8 @@ node_fs = __toESM(node_fs);
33
33
  let jiti = require("jiti");
34
34
 
35
35
  //#region src/cli/codegen.ts
36
+ /** Valid JS identifier pattern for object keys */
37
+ const VALID_IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
36
38
  function getConvexConfig(outputDir) {
37
39
  const convexConfigPath = node_path.default.join(process.cwd(), "convex.json");
38
40
  const functionsDir = (node_fs.default.existsSync(convexConfigPath) ? JSON.parse(node_fs.default.readFileSync(convexConfigPath, "utf-8")) : {}).functions || "convex";
@@ -42,12 +44,22 @@ function getConvexConfig(outputDir) {
42
44
  };
43
45
  }
44
46
  /**
47
+ * Check if a value is a CRPCHttpRouter (has _def.router === true)
48
+ */
49
+ function isCRPCHttpRouter(value) {
50
+ return typeof value === "object" && value !== null && "_def" in value && value._def?.router === true;
51
+ }
52
+ /**
45
53
  * Import a module using jiti and extract CRPC metadata from exports
46
54
  */
47
55
  async function parseModuleRuntime(filePath, jiti$1) {
48
56
  const result = {};
57
+ const httpRoutes = {};
49
58
  const module$1 = await jiti$1.import(filePath);
50
- if (!module$1 || typeof module$1 !== "object") return null;
59
+ if (!module$1 || typeof module$1 !== "object") return {
60
+ meta: null,
61
+ httpRoutes: {}
62
+ };
51
63
  for (const [name, value] of Object.entries(module$1)) {
52
64
  if (name.startsWith("_")) continue;
53
65
  const meta = value?._crpcMeta;
@@ -58,8 +70,23 @@ async function parseModuleRuntime(filePath, jiti$1) {
58
70
  for (const [key, val] of Object.entries(meta)) if (key !== "type" && key !== "internal" && val !== void 0) fnMeta[key] = val;
59
71
  result[name] = fnMeta;
60
72
  }
73
+ const httpRoute = value?._crpcHttpRoute;
74
+ if (httpRoute?.path && httpRoute?.method) httpRoutes[name] = {
75
+ path: httpRoute.path,
76
+ method: httpRoute.method
77
+ };
78
+ if (isCRPCHttpRouter(value)) for (const [procPath, procedure] of Object.entries(value._def.procedures)) {
79
+ const route = procedure._crpcHttpRoute;
80
+ if (route?.path && route?.method) httpRoutes[procPath] = {
81
+ path: route.path,
82
+ method: route.method
83
+ };
84
+ }
61
85
  }
62
- return Object.keys(result).length > 0 ? result : null;
86
+ return {
87
+ meta: Object.keys(result).length > 0 ? result : null,
88
+ httpRoutes
89
+ };
63
90
  }
64
91
  async function generateMeta(outputDir, options) {
65
92
  const startTime = Date.now();
@@ -72,24 +99,25 @@ async function generateMeta(outputDir, options) {
72
99
  moduleCache: false
73
100
  });
74
101
  const meta = {};
102
+ const allHttpRoutes = {};
75
103
  let totalFunctions = 0;
76
104
  const files = node_fs.default.readdirSync(functionsDir).filter((file) => file.endsWith(".ts") && !file.startsWith("_") && ![
77
105
  "schema.ts",
78
106
  "convex.config.ts",
79
- "auth.config.ts",
80
- "http.ts"
107
+ "auth.config.ts"
81
108
  ].includes(file));
82
109
  for (const file of files) {
83
110
  const filePath = node_path.default.join(functionsDir, file);
84
111
  const moduleName = file.replace(".ts", "");
85
112
  try {
86
- const moduleMeta = await parseModuleRuntime(filePath, jiti$1);
113
+ const { meta: moduleMeta, httpRoutes } = await parseModuleRuntime(filePath, jiti$1);
87
114
  if (moduleMeta) {
88
115
  meta[moduleName] = moduleMeta;
89
116
  const fnCount = Object.keys(moduleMeta).length;
90
117
  totalFunctions += fnCount;
91
118
  if (debug) console.info(` ✓ ${moduleName}: ${fnCount} functions`);
92
119
  }
120
+ Object.assign(allHttpRoutes, httpRoutes);
93
121
  } catch (error) {
94
122
  if (debug) console.error(` ⚠ Failed to parse ${file}:`, error);
95
123
  }
@@ -106,11 +134,30 @@ async function generateMeta(outputDir, options) {
106
134
  return ` ${fn}: ${`{ ${metaProps.join(", ")} }`}`;
107
135
  }).join(",\n")},\n }`;
108
136
  }).join(",\n");
137
+ const metaContent = metaEntries ? `\n${metaEntries},\n` : "";
138
+ const routesByPath = /* @__PURE__ */ new Map();
139
+ for (const [key, route] of Object.entries(allHttpRoutes)) {
140
+ const pathKey = `${route.path}:${route.method}`;
141
+ const existing = routesByPath.get(pathKey) || [];
142
+ existing.push({
143
+ key,
144
+ route
145
+ });
146
+ routesByPath.set(pathKey, existing);
147
+ }
148
+ const dedupedRoutes = {};
149
+ for (const entries of routesByPath.values()) {
150
+ const best = entries.reduce((a, b) => a.key.length >= b.key.length ? a : b);
151
+ dedupedRoutes[best.key] = best.route;
152
+ }
153
+ const formatKey = (key) => VALID_IDENTIFIER_RE.test(key) ? key : `'${key}'`;
154
+ const httpEntries = Object.entries(dedupedRoutes).sort(([a], [b]) => a.localeCompare(b)).map(([name, route]) => ` ${formatKey(name)}: { path: '${route.path}', method: '${route.method}' }`).join(",\n");
155
+ const httpContent = httpEntries ? `\n${httpEntries},\n ` : "";
109
156
  const output = `// biome-ignore-all format: generated
110
157
  // This file is auto-generated by better-convex
111
158
  // Do not edit manually. Run \`better-convex codegen\` to regenerate.
112
159
 
113
- export const meta = {${metaEntries ? `\n${metaEntries},\n` : ""}} as const;
160
+ export const meta = {${metaContent ? `${metaContent} _http: {${httpContent}},\n` : `\n _http: {${httpContent}},\n`}} as const;
114
161
 
115
162
  export type Meta = typeof meta;
116
163
  `;
@@ -1,4 +1,5 @@
1
- import { C as PaginatedFnMeta, S as Meta, _ as ExtractPaginatedItem, a as ConvexInfiniteQueryMeta, b as InfiniteQueryInput, c as ConvexMutationKey, d as ConvexQueryMeta, f as ConvexQueryOptions, g as DecorateQuery, h as DecorateMutation, i as ConvexActionOptions, l as ConvexQueryHookOptions, m as DecorateInfiniteQuery, n as CRPCClient, o as ConvexInfiniteQueryOptions, p as DecorateAction, r as ConvexActionKey, s as ConvexInfiniteQueryOptionsWithRef, t as AuthType, u as ConvexQueryKey, v as FUNC_REF_SYMBOL, w as PaginationOpts, x as InfiniteQueryOptsParam, y as FnMeta } from "../types-CotWvon8.js";
1
+ import { _ as HttpRouterRecord, a as HttpErrorCode, c as HttpRouteMap, d as isHttpClientError, h as CRPCHttpRouter, i as HttpClientOptions, l as InferHttpInput, n as HttpClientError, o as HttpProcedureCall, r as HttpClientFromRouter, s as HttpRouteInfo, t as HttpClient, u as InferHttpOutput } from "../http-types-BwEQgYV1.js";
2
+ import { C as PaginatedFnMeta, S as Meta, _ as ExtractPaginatedItem, a as ConvexInfiniteQueryMeta, b as InfiniteQueryInput, c as ConvexMutationKey, d as ConvexQueryMeta, f as ConvexQueryOptions, g as DecorateQuery, h as DecorateMutation, i as ConvexActionOptions, l as ConvexQueryHookOptions, m as DecorateInfiniteQuery, n as CRPCClient, o as ConvexInfiniteQueryOptions, p as DecorateAction, r as ConvexActionKey, s as ConvexInfiniteQueryOptionsWithRef, t as AuthType, u as ConvexQueryKey, v as FUNC_REF_SYMBOL, w as PaginationOpts, x as InfiniteQueryOptsParam, y as FnMeta } from "../types-DLVNrXKq.js";
2
3
  import { FunctionArgs, FunctionReference } from "convex/server";
3
4
 
4
5
  //#region src/crpc/error.d.ts
@@ -27,6 +28,44 @@ declare const isCRPCErrorCode: (error: unknown, code: ClientErrorCode) => error
27
28
  /** Default unauthorized detection - checks UNAUTHORIZED code */
28
29
  declare const defaultIsUnauthorized: (error: unknown) => boolean;
29
30
  //#endregion
31
+ //#region src/crpc/http-client.d.ts
32
+
33
+ /**
34
+ * Create a typed HTTP client for cRPC HTTP routers
35
+ *
36
+ * Supports nested router access like tRPC:
37
+ * - `http.todos.get({ id })` for nested routers
38
+ * - `http.health()` for flat procedures
39
+ *
40
+ * For React apps, prefer using `createCRPCContext` with HTTP options
41
+ * which provides TanStack Query integration (queryOptions, mutationOptions).
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { createHttpClient } from 'better-convex/crpc';
46
+ * import { meta } from '../convex/shared/meta';
47
+ * import type { AppRouter } from '../convex/functions/http';
48
+ *
49
+ * const http = createHttpClient<AppRouter>({
50
+ * convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
51
+ * routes: meta._http!, // HTTP routes are in meta._http
52
+ * });
53
+ *
54
+ * // Nested access
55
+ * const todo = await http.todos.get({ id: 'abc123' });
56
+ *
57
+ * // Flat access
58
+ * const health = await http.health();
59
+ * ```
60
+ */
61
+ declare function createHttpClient<TRouter extends CRPCHttpRouter<any>, TRoutes extends HttpRouteMap = HttpRouteMap>(opts: HttpClientOptions<TRoutes>): HttpClientFromRouter<TRouter>;
62
+ /**
63
+ * Create a typed HTTP client for flat HTTP router record
64
+ *
65
+ * @deprecated Prefer passing a CRPCHttpRouter type for nested access
66
+ */
67
+ declare function createHttpClient<TRouter extends HttpRouterRecord, TRoutes extends HttpRouteMap = HttpRouteMap>(opts: HttpClientOptions<TRoutes>): HttpClient<TRouter>;
68
+ //#endregion
30
69
  //#region src/crpc/query-options.d.ts
31
70
  /**
32
71
  * Query options factory for Convex query function subscriptions.
@@ -67,4 +106,4 @@ declare function convexAction<T extends FunctionReference<'action'>>(funcRef: T,
67
106
  */
68
107
  declare function convexInfiniteQueryOptions<T extends FunctionReference<'query'>>(funcRef: T, args: Record<string, unknown> | 'skip', opts?: InfiniteQueryOptsParam<T>, meta?: Meta): ConvexInfiniteQueryOptions<T>;
69
108
  //#endregion
70
- export { AuthType, CRPCClient, CRPCClientError, ConvexActionKey, ConvexActionOptions, ConvexInfiniteQueryMeta, ConvexInfiniteQueryOptions, ConvexInfiniteQueryOptionsWithRef, ConvexMutationKey, ConvexQueryHookOptions, ConvexQueryKey, ConvexQueryMeta, ConvexQueryOptions, DecorateAction, DecorateInfiniteQuery, DecorateMutation, DecorateQuery, ExtractPaginatedItem, FUNC_REF_SYMBOL, FnMeta, InfiniteQueryInput, InfiniteQueryOptsParam, Meta, PaginatedFnMeta, PaginationOpts, convexAction, convexInfiniteQueryOptions, convexQuery, defaultIsUnauthorized, isCRPCClientError, isCRPCErrorCode };
109
+ export { AuthType, CRPCClient, CRPCClientError, ConvexActionKey, ConvexActionOptions, ConvexInfiniteQueryMeta, ConvexInfiniteQueryOptions, ConvexInfiniteQueryOptionsWithRef, ConvexMutationKey, ConvexQueryHookOptions, ConvexQueryKey, ConvexQueryMeta, ConvexQueryOptions, DecorateAction, DecorateInfiniteQuery, DecorateMutation, DecorateQuery, ExtractPaginatedItem, FUNC_REF_SYMBOL, FnMeta, HttpClient, HttpClientError, HttpClientFromRouter, HttpClientOptions, HttpErrorCode, HttpProcedureCall, HttpRouteInfo, HttpRouteMap, InferHttpInput, InferHttpOutput, InfiniteQueryInput, InfiniteQueryOptsParam, Meta, PaginatedFnMeta, PaginationOpts, convexAction, convexInfiniteQueryOptions, convexQuery, createHttpClient, defaultIsUnauthorized, isCRPCClientError, isCRPCErrorCode, isHttpClientError };