@convex-dev/better-auth 0.9.10 → 0.10.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.
Files changed (102) hide show
  1. package/dist/auth-config.d.ts +43 -0
  2. package/dist/auth-config.d.ts.map +1 -0
  3. package/dist/auth-config.js +45 -0
  4. package/dist/auth-config.js.map +1 -0
  5. package/dist/auth-options.d.ts +3 -0
  6. package/dist/auth-options.d.ts.map +1 -0
  7. package/dist/auth-options.js +41 -0
  8. package/dist/auth-options.js.map +1 -0
  9. package/dist/auth.d.ts +1 -3
  10. package/dist/auth.d.ts.map +1 -1
  11. package/dist/auth.js +2 -42
  12. package/dist/auth.js.map +1 -1
  13. package/dist/client/{adapterUtils.d.ts → adapter-utils.d.ts} +15 -15
  14. package/dist/client/adapter-utils.d.ts.map +1 -0
  15. package/dist/client/{adapterUtils.js → adapter-utils.js} +1 -1
  16. package/dist/client/adapter-utils.js.map +1 -0
  17. package/dist/client/adapter.d.ts +1 -2
  18. package/dist/client/adapter.d.ts.map +1 -1
  19. package/dist/client/adapter.js +4 -4
  20. package/dist/client/adapter.js.map +1 -1
  21. package/dist/client/create-api.d.ts +139 -0
  22. package/dist/client/create-api.d.ts.map +1 -0
  23. package/dist/client/create-api.js +204 -0
  24. package/dist/client/create-api.js.map +1 -0
  25. package/dist/client/create-client.d.ts +183 -0
  26. package/dist/client/create-client.d.ts.map +1 -0
  27. package/dist/client/create-client.js +311 -0
  28. package/dist/client/create-client.js.map +1 -0
  29. package/dist/client/{createSchema.d.ts → create-schema.d.ts} +1 -1
  30. package/dist/client/create-schema.d.ts.map +1 -0
  31. package/dist/client/{createSchema.js → create-schema.js} +11 -5
  32. package/dist/client/create-schema.js.map +1 -0
  33. package/dist/client/index.d.ts +4 -279
  34. package/dist/client/index.d.ts.map +1 -1
  35. package/dist/client/index.js +6 -476
  36. package/dist/client/index.js.map +1 -1
  37. package/dist/component/_generated/component.d.ts +0 -3
  38. package/dist/component/_generated/component.d.ts.map +1 -1
  39. package/dist/component/adapter.d.ts +19 -21
  40. package/dist/component/adapter.d.ts.map +1 -1
  41. package/dist/component/adapter.js +2 -2
  42. package/dist/component/adapter.js.map +1 -1
  43. package/dist/component/schema.d.ts +50 -50
  44. package/dist/nextjs/client.d.ts +4 -0
  45. package/dist/nextjs/client.d.ts.map +1 -0
  46. package/dist/nextjs/client.js +37 -0
  47. package/dist/nextjs/client.js.map +1 -0
  48. package/dist/nextjs/index.d.ts +19 -7
  49. package/dist/nextjs/index.d.ts.map +1 -1
  50. package/dist/nextjs/index.js +90 -36
  51. package/dist/nextjs/index.js.map +1 -1
  52. package/dist/plugins/convex/client.d.ts +1 -1
  53. package/dist/plugins/convex/client.d.ts.map +1 -1
  54. package/dist/plugins/convex/client.js +0 -1
  55. package/dist/plugins/convex/client.js.map +1 -1
  56. package/dist/plugins/convex/index.d.ts +239 -227
  57. package/dist/plugins/convex/index.d.ts.map +1 -1
  58. package/dist/plugins/convex/index.js +191 -37
  59. package/dist/plugins/convex/index.js.map +1 -1
  60. package/dist/plugins/cross-domain/client.d.ts +3 -3
  61. package/dist/plugins/cross-domain/client.d.ts.map +1 -1
  62. package/dist/plugins/cross-domain/index.d.ts +15 -70
  63. package/dist/plugins/cross-domain/index.d.ts.map +1 -1
  64. package/dist/plugins/cross-domain/index.js +8 -0
  65. package/dist/plugins/cross-domain/index.js.map +1 -1
  66. package/dist/react/index.d.ts +52 -2
  67. package/dist/react/index.d.ts.map +1 -1
  68. package/dist/react/index.js +133 -9
  69. package/dist/react/index.js.map +1 -1
  70. package/dist/react-start/index.d.ts +11 -41
  71. package/dist/react-start/index.d.ts.map +1 -1
  72. package/dist/react-start/index.js +82 -106
  73. package/dist/react-start/index.js.map +1 -1
  74. package/dist/utils/index.d.ts +20 -2
  75. package/dist/utils/index.d.ts.map +1 -1
  76. package/dist/utils/index.js +54 -1
  77. package/dist/utils/index.js.map +1 -1
  78. package/package.json +19 -12
  79. package/src/auth-config.ts +82 -0
  80. package/src/auth-options.ts +54 -0
  81. package/src/auth.ts +3 -56
  82. package/src/client/adapter.ts +5 -5
  83. package/src/client/create-api.ts +337 -0
  84. package/src/client/create-client.ts +446 -0
  85. package/src/client/{createSchema.ts → create-schema.ts} +10 -4
  86. package/src/client/index.ts +22 -786
  87. package/src/component/_generated/component.ts +0 -7
  88. package/src/component/adapter.ts +2 -3
  89. package/src/nextjs/client.tsx +52 -0
  90. package/src/nextjs/index.ts +138 -45
  91. package/src/plugins/convex/client.ts +1 -1
  92. package/src/plugins/convex/index.ts +337 -51
  93. package/src/plugins/cross-domain/index.ts +10 -2
  94. package/src/react/index.tsx +195 -9
  95. package/src/react-start/index.ts +126 -171
  96. package/src/test.ts +1 -1
  97. package/src/utils/index.ts +96 -1
  98. package/dist/client/adapterUtils.d.ts.map +0 -1
  99. package/dist/client/adapterUtils.js.map +0 -1
  100. package/dist/client/createSchema.d.ts.map +0 -1
  101. package/dist/client/createSchema.js.map +0 -1
  102. /package/src/client/{adapterUtils.ts → adapter-utils.ts} +0 -0
@@ -1,11 +1,24 @@
1
- import { useEffect } from "react";
2
-
3
- import { type ReactNode, useCallback, useMemo } from "react";
1
+ import {
2
+ Component,
3
+ type PropsWithChildren,
4
+ type ReactNode,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
4
10
  import { type AuthTokenFetcher } from "convex/browser";
5
- import { ConvexProviderWithAuth } from "convex/react";
11
+ import {
12
+ Authenticated,
13
+ ConvexProviderWithAuth,
14
+ useConvexAuth,
15
+ useQuery,
16
+ } from "convex/react";
17
+ import type { FunctionReference } from "convex/server";
6
18
  import { type BetterAuthClientPlugin } from "better-auth";
7
19
  import { createAuthClient } from "better-auth/react";
8
20
  import { convexClient, crossDomainClient } from "../client/plugins/index.js";
21
+ import type { EmptyObject } from "convex-helpers";
9
22
 
10
23
  type CrossDomainClient = ReturnType<typeof crossDomainClient>;
11
24
  type ConvexClient = ReturnType<typeof convexClient>;
@@ -45,13 +58,14 @@ export function ConvexBetterAuthProvider({
45
58
  children,
46
59
  client,
47
60
  authClient,
61
+ initialToken,
48
62
  }: {
49
63
  children: ReactNode;
50
64
  client: IConvexReactClient;
51
65
  authClient: AuthClient;
66
+ initialToken?: string | null;
52
67
  }) {
53
- const useBetterAuth = useUseAuthFromBetterAuth(authClient);
54
-
68
+ const useBetterAuth = useUseAuthFromBetterAuth(authClient, initialToken);
55
69
  useEffect(() => {
56
70
  (async () => {
57
71
  const url = new URL(window.location?.href);
@@ -86,19 +100,46 @@ export function ConvexBetterAuthProvider({
86
100
  );
87
101
  }
88
102
 
89
- function useUseAuthFromBetterAuth(authClient: AuthClient) {
103
+ let initialTokenUsed = false;
104
+
105
+ function useUseAuthFromBetterAuth(
106
+ authClient: AuthClient,
107
+ initialToken?: string | null
108
+ ) {
109
+ const [cachedToken, setCachedToken] = useState<string | null>(
110
+ initialTokenUsed ? (initialToken ?? null) : null
111
+ );
112
+ useEffect(() => {
113
+ if (!initialTokenUsed) {
114
+ initialTokenUsed = true;
115
+ }
116
+ }, []);
117
+
90
118
  return useMemo(
91
119
  () =>
92
120
  function useAuthFromBetterAuth() {
93
121
  const { data: session, isPending: isSessionPending } =
94
122
  authClient.useSession();
95
123
  const sessionId = session?.session?.id;
124
+ useEffect(() => {
125
+ if (!session && !isSessionPending && cachedToken) {
126
+ setCachedToken(null);
127
+ }
128
+ }, [session, isSessionPending]);
96
129
  const fetchAccessToken = useCallback(
97
- async () => {
130
+ async ({
131
+ forceRefreshToken = false,
132
+ }: { forceRefreshToken?: boolean } = {}) => {
133
+ if (cachedToken && !forceRefreshToken) {
134
+ return cachedToken;
135
+ }
98
136
  try {
99
137
  const { data } = await authClient.convex.token();
100
- return data?.token || null;
138
+ const token = data?.token || null;
139
+ setCachedToken(token);
140
+ return token;
101
141
  } catch {
142
+ setCachedToken(null);
102
143
  return null;
103
144
  }
104
145
  },
@@ -120,3 +161,148 @@ function useUseAuthFromBetterAuth(authClient: AuthClient) {
120
161
  [authClient]
121
162
  );
122
163
  }
164
+
165
+ interface ErrorBoundaryProps {
166
+ children: React.ReactNode;
167
+ onUnauth: () => void | Promise<void>;
168
+ renderFallback?: () => React.ReactNode;
169
+ isAuthError: (error: unknown) => boolean;
170
+ }
171
+ interface ErrorBoundaryState {
172
+ error?: unknown;
173
+ }
174
+
175
+ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
176
+ constructor(props: ErrorBoundaryProps) {
177
+ super(props);
178
+ this.state = {};
179
+ }
180
+ static defaultProps: Partial<ErrorBoundaryProps> = {
181
+ renderFallback: () => null,
182
+ };
183
+ static getDerivedStateFromError(error: Error) {
184
+ return { error };
185
+ }
186
+ async componentDidCatch(error: Error) {
187
+ if (this.props.isAuthError(error)) {
188
+ await this.props.onUnauth();
189
+ }
190
+ }
191
+ render() {
192
+ if (this.state.error && this.props.isAuthError(this.state.error)) {
193
+ return this.props.renderFallback?.();
194
+ }
195
+ return this.props.children;
196
+ }
197
+ }
198
+
199
+ // Subscribe to the session validated user to keep this check reactive to
200
+ // actual user auth state at the provider level (rather than just jwt validity state).
201
+ const UserSubscription = ({
202
+ getAuthUserFn,
203
+ }: {
204
+ getAuthUserFn: FunctionReference<"query">;
205
+ }) => {
206
+ useQuery(getAuthUserFn);
207
+ return null;
208
+ };
209
+
210
+ /**
211
+ * _Experimental_
212
+ *
213
+ * A wrapper React component which provides error handling for auth related errors.
214
+ * This is typically used to redirect the user to the login page when they are
215
+ * unauthenticated, and does so reactively based on the getAuthUserFn query.
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * // convex/auth.ts
220
+ * export const { getAuthUser } = authComponent.clientApi();
221
+ *
222
+ * // auth-client.tsx
223
+ * import { AuthBoundary } from "@convex-dev/react";
224
+ * import { api } from '../../convex/_generated/api'
225
+ * import { isAuthError } from '../lib/utils'
226
+ *
227
+ * export const ClientAuthBoundary = ({ children }: PropsWithChildren) => {
228
+ * return (
229
+ * <AuthBoundary
230
+ * onUnauth={() => redirect("/sign-in")}
231
+ * authClient={authClient}
232
+ * getAuthUserFn={api.auth.getAuthUser}
233
+ * isAuthError={isAuthError}
234
+ * >
235
+ * <>{children}</>
236
+ * </AuthBoundary>
237
+ * )
238
+ * ```
239
+ * @param props.children - Children to render.
240
+ * @param props.onUnauth - Function to call when the user is
241
+ * unauthenticated. Typically a redirect to the login page.
242
+ * @param props.authClient - Better Auth authClient to use.
243
+ * @param props.renderFallback - Fallback component to render when the user is
244
+ * unauthenticated. Defaults to null. Generally not rendered as error handling
245
+ * is typically a redirect.
246
+ * @param props.getAuthUserFn - Reference to a Convex query that returns user.
247
+ * The component provides a query for this via `export const { getAuthUser } = authComponent.clientApi()`.
248
+ * @param props.isAuthError - Function to check if the error is auth related.
249
+ */
250
+ export const AuthBoundary = ({
251
+ children,
252
+ /**
253
+ * The function to call when the user is unauthenticated. Typically a redirect
254
+ * to the login page.
255
+ */
256
+ onUnauth,
257
+ /**
258
+ * The Better Auth authClient to use.
259
+ */
260
+ authClient,
261
+ /**
262
+ * The fallback to render when the user is unauthenticated. Defaults to null.
263
+ * Generally not rendered as error handling is typically a redirect.
264
+ */
265
+ renderFallback,
266
+ /**
267
+ * The function to call to get the auth user.
268
+ */
269
+ getAuthUserFn,
270
+ /**
271
+ * The function to call to check if the error is auth related.
272
+ */
273
+ isAuthError,
274
+ }: PropsWithChildren<{
275
+ onUnauth: () => void | Promise<void>;
276
+ authClient: AuthClient;
277
+ renderFallback?: () => React.ReactNode;
278
+ getAuthUserFn: FunctionReference<"query", "public", EmptyObject>;
279
+ isAuthError: (error: unknown) => boolean;
280
+ }>) => {
281
+ const { isAuthenticated, isLoading } = useConvexAuth();
282
+ const handleUnauth = useCallback(async () => {
283
+ // Auth request that will clear cookies if session is invalid
284
+ await authClient.getSession();
285
+ await onUnauth();
286
+ }, [onUnauth]);
287
+
288
+ useEffect(() => {
289
+ void (async () => {
290
+ if (!isLoading && !isAuthenticated) {
291
+ await handleUnauth();
292
+ }
293
+ })();
294
+ }, [isLoading, isAuthenticated]);
295
+
296
+ return (
297
+ <ErrorBoundary
298
+ onUnauth={handleUnauth}
299
+ isAuthError={isAuthError}
300
+ renderFallback={renderFallback}
301
+ >
302
+ <Authenticated>
303
+ <UserSubscription getAuthUserFn={getAuthUserFn} />
304
+ </Authenticated>
305
+ {children}
306
+ </ErrorBoundary>
307
+ );
308
+ };
@@ -1,189 +1,69 @@
1
- import { betterAuth } from "better-auth";
2
- import { createCookieGetter } from "better-auth/cookies";
3
- import { betterFetch } from "@better-fetch/fetch";
4
- import * as jose from "jose";
5
- import {
6
- type FunctionReference,
7
- type FunctionReturnType,
8
- type GenericActionCtx,
9
- type GenericDataModel,
1
+ import { stripIndent } from "common-tags";
2
+ import type {
3
+ FunctionReference,
4
+ FunctionReturnType,
5
+ OptionalRestArgs,
10
6
  } from "convex/server";
11
- import { JWT_COOKIE_NAME } from "../plugins/convex/index.js";
12
7
  import { ConvexHttpClient } from "convex/browser";
13
- import { type CreateAuth, getStaticAuth } from "../client/index.js";
8
+ import { getToken, type GetTokenOptions } from "../utils/index.js";
9
+ import React from "react";
14
10
 
15
- export const getCookieName = <DataModel extends GenericDataModel>(
16
- createAuth: CreateAuth<DataModel>
17
- ) => {
18
- const createCookie = createCookieGetter(getStaticAuth(createAuth).options);
19
- const cookie = createCookie(JWT_COOKIE_NAME);
20
- return cookie.name;
21
- };
22
-
23
- export const getCookieNames = <DataModel extends GenericDataModel>(
24
- createAuth: CreateAuth<DataModel>
25
- ) => {
26
- const createCookie = createCookieGetter(getStaticAuth(createAuth).options);
27
- return {
28
- convexJwt: createCookie(JWT_COOKIE_NAME).name,
29
- sessionToken: createCookie("session_token").name,
30
- };
31
- };
32
-
33
- export const setupFetchClient = async <DataModel extends GenericDataModel>(
34
- createAuth: CreateAuth<DataModel>,
35
- getCookie: (name: string) => string | undefined
36
- ) => {
37
- const createClient = () => {
38
- const sessionCookieName = getCookieName(createAuth);
39
- const token = getCookie(sessionCookieName);
40
- const client = new ConvexHttpClient(process.env.VITE_CONVEX_URL!);
41
- if (token) {
42
- client.setAuth(token);
43
- }
44
- return client;
45
- };
46
- return {
47
- fetchQuery<
48
- Query extends FunctionReference<"query">,
49
- FuncRef extends FunctionReference<any, any>,
50
- >(
51
- query: Query,
52
- args: FuncRef["_args"]
53
- ): Promise<FunctionReturnType<Query>> {
54
- return createClient().query(query, args);
55
- },
56
- fetchMutation<
57
- Mutation extends FunctionReference<"mutation">,
58
- FuncRef extends FunctionReference<any, any>,
59
- >(
60
- mutation: Mutation,
61
- args: FuncRef["_args"]
62
- ): Promise<FunctionReturnType<Mutation>> {
63
- return createClient().mutation(mutation, args);
64
- },
65
- fetchAction<
66
- Action extends FunctionReference<"action">,
67
- FuncRef extends FunctionReference<any, any>,
68
- >(
69
- action: Action,
70
- args: FuncRef["_args"]
71
- ): Promise<FunctionReturnType<Action>> {
72
- return createClient().action(action, args);
73
- },
74
- };
75
- };
76
-
77
- export const fetchSession = async <
78
- T extends (ctx: GenericActionCtx<any>) => ReturnType<typeof betterAuth>,
79
- >(
80
- request: Request,
81
- opts?: {
82
- convexSiteUrl?: string;
83
- verbose?: boolean;
84
- }
85
- ) => {
86
- type Session = ReturnType<T>["$Infer"]["Session"];
11
+ // Caching supported for React 19+ only
12
+ const cache =
13
+ React.cache ||
14
+ ((fn: (...args: any[]) => any) => {
15
+ return (...args: any[]) => fn(...args);
16
+ });
87
17
 
88
- if (!request) {
89
- throw new Error("No request found");
90
- }
91
- const convexSiteUrl = opts?.convexSiteUrl ?? process.env.VITE_CONVEX_SITE_URL;
92
- if (!convexSiteUrl) {
93
- throw new Error("VITE_CONVEX_SITE_URL is not set");
94
- }
95
- const { data: session } = await betterFetch<Session>(
96
- "/api/auth/get-session",
97
- {
98
- baseURL: convexSiteUrl,
99
- headers: {
100
- cookie: request.headers.get("cookie") ?? "",
101
- },
102
- }
103
- );
104
- return {
105
- session,
106
- };
18
+ type ClientOptions = {
19
+ /**
20
+ * The URL of the Convex deployment to use for the function call.
21
+ */
22
+ convexUrl: string;
23
+ /**
24
+ * The HTTP Actions URL of the Convex deployment to use for the function call.
25
+ */
26
+ convexSiteUrl: string;
27
+ /**
28
+ * The JWT-encoded OpenID Connect authentication token to use for the function call.
29
+ * Just an optional override for edge cases, you probably don't need this.
30
+ */
31
+ token?: string;
107
32
  };
108
33
 
109
- export const fetchAuth = async (
110
- request: Request,
111
- opts?: {
112
- convexSiteUrl?: string;
113
- verbose?: boolean;
114
- }
115
- ) => {
116
- if (!request) {
117
- throw new Error("No request found");
118
- }
119
- const convexSiteUrl = opts?.convexSiteUrl ?? process.env.VITE_CONVEX_SITE_URL;
120
- if (!convexSiteUrl) {
121
- throw new Error("VITE_CONVEX_SITE_URL is not set");
34
+ function setupClient(options: ClientOptions) {
35
+ const client = new ConvexHttpClient(options.convexUrl);
36
+ if (options.token !== undefined) {
37
+ client.setAuth(options.token);
122
38
  }
123
- const { data } = await betterFetch<{ token: string }>(
124
- "/api/auth/convex/token",
125
- {
126
- baseURL: convexSiteUrl,
127
- headers: {
128
- cookie: request.headers.get("cookie") ?? "",
129
- },
130
- }
131
- );
132
- if (!data?.token) {
133
- return;
134
- }
135
- const claims = jose.decodeJwt(data.token);
136
- return {
137
- token: data.token,
138
- userId: claims.sub,
139
- };
140
- };
39
+ // @ts-expect-error - setFetchOptions is internal
40
+ client.setFetchOptions({ cache: "no-store" });
41
+ return client;
42
+ }
141
43
 
142
- export const getAuthFromCookie = (
143
- cookie?: string,
144
- { tolerance = 10 }: { tolerance?: number } = {}
145
- ) => {
146
- if (!cookie) {
147
- return;
44
+ const parseConvexSiteUrl = (url: string) => {
45
+ if (!url) {
46
+ throw new Error(stripIndent`
47
+ CONVEX_SITE_URL is not set.
48
+ This is automatically set in the Convex backend, but must be set in the TanStack Start environment.
49
+ For local development, this can be set in the .env.local file.
50
+ `);
148
51
  }
149
- const claims = jose.decodeJwt(cookie);
150
- const exp = claims?.exp;
151
- const now = Math.floor(new Date().getTime() / 1000);
152
- const isExpired = exp ? now > exp + tolerance : true;
153
- if (isExpired) {
154
- return;
52
+ if (url.endsWith(".convex.cloud")) {
53
+ throw new Error(stripIndent`
54
+ CONVEX_SITE_URL should be set to your Convex Site URL, which ends in .convex.site.
55
+ Currently set to ${url}.
56
+ `);
155
57
  }
156
- return { userId: claims?.sub, token: cookie };
157
- };
158
-
159
- export const getAuth = async <DataModel extends GenericDataModel>(
160
- request: Request,
161
- getCookie: (name: string) => string | undefined,
162
- createAuth: CreateAuth<DataModel>,
163
- opts?: { convexSiteUrl?: string }
164
- ) => {
165
- const sessionCookieName = getCookieName(createAuth);
166
- const token = getCookie(sessionCookieName);
167
- const { session } = await fetchSession(request, opts);
168
- return {
169
- userId: session?.user.id,
170
- token,
171
- };
58
+ return url;
172
59
  };
173
60
 
174
- export const reactStartHandler = (
175
- request: Request,
176
- opts?: { convexSiteUrl?: string; verbose?: boolean }
177
- ) => {
61
+ const handler = (request: Request, opts: { convexSiteUrl: string }) => {
178
62
  const requestUrl = new URL(request.url);
179
- const convexSiteUrl = opts?.convexSiteUrl ?? process.env.VITE_CONVEX_SITE_URL;
180
- if (!convexSiteUrl) {
181
- throw new Error("VITE_CONVEX_SITE_URL is not set");
182
- }
183
- const nextUrl = `${convexSiteUrl}${requestUrl.pathname}${requestUrl.search}`;
63
+ const nextUrl = `${opts.convexSiteUrl}${requestUrl.pathname}${requestUrl.search}`;
184
64
  const headers = new Headers(request.headers);
185
65
  headers.set("accept-encoding", "application/json");
186
- headers.set("host", convexSiteUrl);
66
+ headers.set("host", opts.convexSiteUrl);
187
67
  return fetch(nextUrl, {
188
68
  method: request.method,
189
69
  headers,
@@ -193,3 +73,78 @@ export const reactStartHandler = (
193
73
  duplex: "half",
194
74
  });
195
75
  };
76
+
77
+ export const convexBetterAuthReactStart = (
78
+ opts: Omit<GetTokenOptions, "forceRefresh"> & {
79
+ convexUrl: string;
80
+ convexSiteUrl: string;
81
+ }
82
+ ) => {
83
+ const siteUrl = parseConvexSiteUrl(opts.convexSiteUrl);
84
+
85
+ const cachedGetToken = cache(async (opts: GetTokenOptions) => {
86
+ const { getRequestHeaders } = await import("@tanstack/react-start/server");
87
+ const headers = getRequestHeaders();
88
+ return getToken(siteUrl, headers, opts);
89
+ });
90
+
91
+ const callWithToken = async <
92
+ FnType extends "query" | "mutation" | "action",
93
+ Fn extends FunctionReference<FnType>,
94
+ >(
95
+ fn: (token?: string) => Promise<FunctionReturnType<Fn>>
96
+ ): Promise<FunctionReturnType<Fn>> => {
97
+ const token = (await cachedGetToken(opts)) ?? {};
98
+ try {
99
+ return await fn(token?.token);
100
+ } catch (error) {
101
+ if (
102
+ !opts?.jwtCache?.enabled ||
103
+ token.isFresh ||
104
+ opts.jwtCache?.isAuthError(error)
105
+ ) {
106
+ throw error;
107
+ }
108
+ const newToken = await cachedGetToken({
109
+ ...opts,
110
+ forceRefresh: true,
111
+ });
112
+ return await fn(newToken.token);
113
+ }
114
+ };
115
+
116
+ return {
117
+ getToken: async () => {
118
+ const token = await cachedGetToken(opts);
119
+ return token.token;
120
+ },
121
+ handler: (request: Request) => handler(request, opts),
122
+ fetchAuthQuery: async <Query extends FunctionReference<"query">>(
123
+ query: Query,
124
+ ...args: OptionalRestArgs<Query>
125
+ ): Promise<FunctionReturnType<Query>> => {
126
+ return callWithToken((token?: string) => {
127
+ const client = setupClient({ ...opts, token });
128
+ return client.query(query, ...args);
129
+ });
130
+ },
131
+ fetchAuthMutation: async <Mutation extends FunctionReference<"mutation">>(
132
+ mutation: Mutation,
133
+ ...args: OptionalRestArgs<Mutation>
134
+ ): Promise<FunctionReturnType<Mutation>> => {
135
+ return callWithToken((token?: string) => {
136
+ const client = setupClient({ ...opts, token });
137
+ return client.mutation(mutation, ...args);
138
+ });
139
+ },
140
+ fetchAuthAction: async <Action extends FunctionReference<"action">>(
141
+ action: Action,
142
+ ...args: OptionalRestArgs<Action>
143
+ ): Promise<FunctionReturnType<Action>> => {
144
+ return callWithToken((token?: string) => {
145
+ const client = setupClient({ ...opts, token });
146
+ return client.action(action, ...args);
147
+ });
148
+ },
149
+ };
150
+ };
package/src/test.ts CHANGED
@@ -11,7 +11,7 @@ const modules = import.meta.glob("./component/**/*.ts");
11
11
  */
12
12
  export function register(
13
13
  t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
14
- name: string = "migrations"
14
+ name: string = "betterAuth"
15
15
  ) {
16
16
  t.registerComponent(name, schema, modules);
17
17
  }
@@ -1,10 +1,34 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import { type Auth, betterAuth } from "better-auth";
3
+ import { getSessionCookie } from "better-auth/cookies";
1
4
  import {
5
+ type AuthProvider,
6
+ type DefaultFunctionArgs,
7
+ type FunctionReference,
2
8
  type GenericActionCtx,
3
9
  type GenericDataModel,
4
10
  type GenericMutationCtx,
5
11
  type GenericQueryCtx,
6
12
  } from "convex/server";
7
- import { type GenericCtx } from "../client/index.js";
13
+ import { JWT_COOKIE_NAME } from "../plugins/convex/index.js";
14
+ import * as jose from "jose";
15
+ import type { Jwk } from "better-auth/plugins/jwt";
16
+
17
+ export type CreateAuth<
18
+ DataModel extends GenericDataModel,
19
+ A extends ReturnType<typeof betterAuth> = Auth,
20
+ > = (ctx: GenericCtx<DataModel>) => A;
21
+
22
+ export type EventFunction<T extends DefaultFunctionArgs> = FunctionReference<
23
+ "mutation",
24
+ "internal" | "public",
25
+ T
26
+ >;
27
+
28
+ export type GenericCtx<DataModel extends GenericDataModel = GenericDataModel> =
29
+ | GenericQueryCtx<DataModel>
30
+ | GenericMutationCtx<DataModel>
31
+ | GenericActionCtx<DataModel>;
8
32
 
9
33
  export type RunMutationCtx<DataModel extends GenericDataModel> = (
10
34
  | GenericMutationCtx<DataModel>
@@ -72,3 +96,74 @@ export const requireRunMutationCtx = <DataModel extends GenericDataModel>(
72
96
  }
73
97
  return ctx;
74
98
  };
99
+
100
+ export type GetTokenOptions = {
101
+ forceRefresh?: boolean;
102
+ cookiePrefix?: string;
103
+ jwtCache?: {
104
+ enabled: boolean;
105
+ expirationToleranceSeconds?: number;
106
+ isAuthError: (error: unknown) => boolean;
107
+ };
108
+ };
109
+
110
+ export const getToken = async (
111
+ siteUrl: string,
112
+ headers: Headers,
113
+ opts?: GetTokenOptions
114
+ ) => {
115
+ const fetchToken = async () => {
116
+ const { data } = await betterFetch<{ token: string }>(
117
+ "/api/auth/convex/token",
118
+ {
119
+ baseURL: siteUrl,
120
+ headers,
121
+ }
122
+ );
123
+ return { isFresh: true, token: data?.token };
124
+ };
125
+ if (!opts?.jwtCache?.enabled || opts.forceRefresh) {
126
+ return await fetchToken();
127
+ }
128
+ const token = getSessionCookie(new Headers(headers), {
129
+ cookieName: JWT_COOKIE_NAME,
130
+ cookiePrefix: opts?.cookiePrefix,
131
+ });
132
+ if (!token) {
133
+ return await fetchToken();
134
+ }
135
+ try {
136
+ const claims = jose.decodeJwt(token);
137
+ const exp = claims?.exp;
138
+ const now = Math.floor(new Date().getTime() / 1000);
139
+ const isExpired = exp
140
+ ? now > exp + (opts?.jwtCache?.expirationToleranceSeconds ?? 60)
141
+ : true;
142
+ if (!isExpired) {
143
+ return { isFresh: false, token };
144
+ }
145
+ } catch (error) {
146
+ console.error("Error decoding JWT", error);
147
+ }
148
+ return await fetchToken();
149
+ };
150
+
151
+ export const parseJwks = (providerConfig: AuthProvider) => {
152
+ const staticJwksString =
153
+ "jwks" in providerConfig && providerConfig.jwks?.startsWith("data:text/")
154
+ ? atob(providerConfig.jwks.split("base64,")[1])
155
+ : undefined;
156
+
157
+ if (!staticJwksString) {
158
+ return;
159
+ }
160
+ const parsed = JSON.parse(
161
+ staticJwksString?.slice(1, -1).replaceAll(/[\s\\]/g, "") || "{}"
162
+ );
163
+ const staticJwks = {
164
+ ...parsed,
165
+ privateKey: `"${parsed.privateKey}"`,
166
+ publicKey: `"${parsed.publicKey}"`,
167
+ } as Jwk;
168
+ return staticJwks;
169
+ };
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapterUtils.d.ts","sourceRoot":"","sources":["../../src/client/adapterUtils.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,KAAK,EAAK,MAAM,eAAe,CAAC;AAC9D,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,eAAO,MAAM,qBAAqB;;;;;;;;;;4DA0BhC,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kHAY/B,CAAC;AAiBH,eAAO,MAAM,eAAe,GAC1B,kBAAkB,kBAAkB,EACpC,OAAO,MAAM,EACb,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,YAQ3B,CAAC;AA0JF,eAAO,MAAM,iBAAiB,GAC5B,MAAM,SAAS,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,EAEzC,KAAK,eAAe,CAAC,gBAAgB,CAAC,EACtC,QAAQ,MAAM,EACd,kBAAkB,kBAAkB,EACpC,OAAO,MAAM,EACb,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,kBA6B1B,CAAC;AAIF,eAAO,MAAM,YAAY,GACvB,CAAC,SAAS,qBAAqB,CAAC,gBAAgB,CAAC,EACjD,CAAC,SAAS,cAAc,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAE7C,KAAK,CAAC,GAAG,IAAI,EACb,SAAS,MAAM,EAAE,sBAYlB,CAAC;AAsKF,eAAO,MAAM,QAAQ,GACnB,GAAG,SAAS,cAAc,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAC/C,CAAC,SAAS,qBAAqB,CAAC,gBAAgB,CAAC,EAEjD,KAAK,eAAe,CAAC,gBAAgB,CAAC,EACtC,QAAQ,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,EAClC,kBAAkB,kBAAkB,EACpC,MAAM,KAAK,CAAC,OAAO,oBAAoB,CAAC,GAAG;IACzC,cAAc,EAAE,iBAAiB,CAAC;CACnC,KACA,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAiJ/B,CAAC;AAEF,eAAO,MAAM,OAAO,GAClB,GAAG,SAAS,cAAc,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAC/C,CAAC,SAAS,qBAAqB,CAAC,gBAAgB,CAAC,EAEjD,KAAK,eAAe,CAAC,gBAAgB,CAAC,EACtC,QAAQ,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,EAClC,kBAAkB,kBAAkB,EACpC,MAAM,KAAK,CAAC,OAAO,oBAAoB,CAAC,KACvC,OAAO,CAAC,GAAG,GAAG,IAAI,CAUpB,CAAC"}