@convex-dev/better-auth 0.10.5 → 0.10.7

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 (53) hide show
  1. package/dist/auth-options.d.ts +1 -1
  2. package/dist/auth-options.d.ts.map +1 -1
  3. package/dist/auth.js +1 -1
  4. package/dist/auth.js.map +1 -1
  5. package/dist/client/adapter-utils.d.ts +3 -3
  6. package/dist/client/adapter-utils.js +1 -1
  7. package/dist/client/adapter-utils.js.map +1 -1
  8. package/dist/client/adapter.d.ts.map +1 -1
  9. package/dist/client/adapter.js +15 -13
  10. package/dist/client/adapter.js.map +1 -1
  11. package/dist/client/create-api.d.ts +4 -2
  12. package/dist/client/create-api.d.ts.map +1 -1
  13. package/dist/client/create-api.js +2 -0
  14. package/dist/client/create-api.js.map +1 -1
  15. package/dist/client/create-client.d.ts.map +1 -1
  16. package/dist/client/create-client.js +3 -0
  17. package/dist/client/create-client.js.map +1 -1
  18. package/dist/client/create-schema.js +1 -1
  19. package/dist/component/_generated/component.d.ts +4 -8
  20. package/dist/component/_generated/component.d.ts.map +1 -1
  21. package/dist/component/adapter.d.ts +3 -1
  22. package/dist/component/adapter.d.ts.map +1 -1
  23. package/dist/component/adapterTest.d.ts +8 -15
  24. package/dist/component/adapterTest.d.ts.map +1 -1
  25. package/dist/component/adapterTest.js +390 -61
  26. package/dist/component/adapterTest.js.map +1 -1
  27. package/dist/nextjs/index.js +1 -1
  28. package/dist/nextjs/index.js.map +1 -1
  29. package/dist/plugins/convex/index.d.ts +2 -1
  30. package/dist/plugins/convex/index.d.ts.map +1 -1
  31. package/dist/plugins/convex/index.js +5 -5
  32. package/dist/plugins/convex/index.js.map +1 -1
  33. package/dist/react-start/index.js +1 -1
  34. package/dist/react-start/index.js.map +1 -1
  35. package/dist/utils/index.d.ts +2 -1
  36. package/dist/utils/index.d.ts.map +1 -1
  37. package/dist/utils/index.js +1 -0
  38. package/dist/utils/index.js.map +1 -1
  39. package/package.json +5 -4
  40. package/src/auth-options.ts +1 -1
  41. package/src/auth.ts +1 -1
  42. package/src/client/adapter-utils.ts +1 -1
  43. package/src/client/adapter.test.ts +24 -440
  44. package/src/client/adapter.ts +19 -14
  45. package/src/client/create-api.ts +3 -1
  46. package/src/client/create-client.ts +3 -0
  47. package/src/client/create-schema.ts +1 -1
  48. package/src/component/_generated/component.ts +4 -8
  49. package/src/component/adapterTest.ts +457 -100
  50. package/src/nextjs/index.ts +1 -1
  51. package/src/plugins/convex/index.ts +6 -10
  52. package/src/react-start/index.ts +1 -1
  53. package/src/utils/index.ts +3 -1
@@ -1,116 +1,473 @@
1
- import { convexAdapter } from "../client/index.js";
1
+ import { runAdapterTest } from "better-auth/adapters/test";
2
+ import { createClient } from "../client/index.js";
2
3
  import type { GenericCtx } from "../client/index.js";
3
4
  import { api } from "./_generated/api.js";
4
- import { mutation, query } from "./_generated/server.js";
5
- import type {
6
- GenericDataModel,
7
- GenericMutationCtx,
8
- GenericQueryCtx,
9
- RegisteredMutation,
10
- RegisteredQuery,
11
- } from "convex/server";
5
+ import { action } from "./_generated/server.js";
6
+ import type { GenericActionCtx } from "convex/server";
7
+ import type { DataModel } from "./_generated/dataModel.js";
8
+ import type { BetterAuthOptions } from "better-auth";
9
+ import type { EmptyObject } from "convex-helpers";
10
+ import { beforeEach, expect, test } from "vitest";
12
11
 
13
- export const createAdapter = <DataModel extends GenericDataModel>(
12
+ export const getAdapter: (
14
13
  ctx: GenericCtx<DataModel>
15
- ) =>
16
- convexAdapter(ctx as any, api as any, {
17
- debugLogs: {
18
- isRunningAdapterTests: true,
19
- },
20
- });
21
-
22
- export const deserialize = (data: any) => {
23
- const dateStringRegex =
24
- /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
25
- return Object.fromEntries(
26
- Object.entries(data).map(([key, value]) => [
27
- key,
28
- dateStringRegex.test(value as string) ? new Date(value as string) : value,
29
- ])
30
- );
31
- };
32
-
33
- export const serialize = (data: any) => {
34
- if (!data) {
35
- return data;
36
- }
37
- return Object.fromEntries(
38
- Object.entries(data).map(([key, value]) => [
39
- key,
40
- value instanceof Date ? value.toISOString() : value,
41
- ])
42
- );
43
- };
14
+ ) => Parameters<typeof runAdapterTest>[0]["getAdapter"] =
15
+ (ctx: GenericCtx<DataModel>) =>
16
+ async (opts?: Omit<BetterAuthOptions, "database">) => {
17
+ const authComponent = createClient<DataModel>(api as any, {
18
+ verbose: false,
19
+ });
20
+ const adapterFactory = authComponent.adapter(ctx);
21
+ const options = {
22
+ ...(opts ?? {}),
23
+ user: {
24
+ ...(opts?.user ?? {}),
25
+ // We don't currently support custom schema for tests, need to find a
26
+ // way to do this.
27
+ fields: undefined,
28
+ },
29
+ };
30
+ return adapterFactory(options);
31
+ };
44
32
 
45
- export const create: RegisteredMutation<"public", any, Promise<any>> = mutation(
46
- async (ctx: GenericMutationCtx<any>, args: any) => {
47
- const adapter = createAdapter(ctx);
48
- const result = await adapter({}).create({
49
- ...args,
50
- data: deserialize(args.data),
51
- });
52
- return serialize(result);
33
+ // Tests need to run inside of a Convex function to use the Convex adapter
34
+ export const runTests = action(
35
+ async (
36
+ ctx: GenericActionCtx<DataModel>,
37
+ args: { disableTests: Record<string, boolean> }
38
+ ) => {
39
+ runAdapterTest({
40
+ getAdapter: getAdapter(ctx),
41
+ disableTests: args.disableTests,
42
+ });
53
43
  }
54
44
  );
55
45
 
56
- export const findOne: RegisteredQuery<"public", any, Promise<any>> = query(
57
- async (ctx: GenericQueryCtx<any>, args: any) => {
58
- const adapter = createAdapter(ctx);
59
- const result = await adapter({}).findOne(args);
60
- return serialize(result);
46
+ export const runCustomTests = action(
47
+ async (ctx: GenericActionCtx<DataModel>, _args: EmptyObject) => {
48
+ runCustomAdapterTests({
49
+ getAdapter: getAdapter(ctx),
50
+ });
61
51
  }
62
52
  );
63
53
 
64
- export const findMany: RegisteredQuery<"public", any, Promise<any>> = query(
65
- async (ctx: GenericQueryCtx<any>, args: any) => {
66
- const adapter = createAdapter(ctx);
67
- const result = await adapter({}).findMany(args);
68
- return result.map(serialize);
69
- }
70
- );
54
+ function runCustomAdapterTests({
55
+ getAdapter,
56
+ }: {
57
+ getAdapter: Parameters<typeof runAdapterTest>[0]["getAdapter"];
58
+ }) {
59
+ beforeEach(async () => {
60
+ const adapter = await getAdapter();
61
+ await adapter.deleteMany({
62
+ model: "user",
63
+ where: [],
64
+ });
65
+ await adapter.deleteMany({
66
+ model: "session",
67
+ where: [],
68
+ });
69
+ });
70
+ test("should handle lone range operators", async () => {
71
+ const adapter = await getAdapter();
72
+ const user = await adapter.create({
73
+ model: "user",
74
+ data: {
75
+ name: "ab",
76
+ email: "a@a.com",
77
+ },
78
+ });
79
+ expect(
80
+ await adapter.findMany({
81
+ model: "user",
82
+ where: [
83
+ {
84
+ field: "name",
85
+ operator: "lt",
86
+ value: "a",
87
+ },
88
+ ],
89
+ })
90
+ ).toEqual([]);
91
+ expect(
92
+ await adapter.findMany({
93
+ model: "user",
94
+ where: [
95
+ {
96
+ field: "name",
97
+ operator: "lte",
98
+ value: "a",
99
+ },
100
+ ],
101
+ })
102
+ ).toEqual([]);
103
+ expect(
104
+ await adapter.findMany({
105
+ model: "user",
106
+ where: [
107
+ {
108
+ field: "name",
109
+ operator: "gt",
110
+ value: "a",
111
+ },
112
+ ],
113
+ })
114
+ ).toEqual([user]);
115
+ expect(
116
+ await adapter.findMany({
117
+ model: "user",
118
+ where: [
119
+ {
120
+ field: "name",
121
+ operator: "gte",
122
+ value: "ab",
123
+ },
124
+ ],
125
+ })
126
+ ).toEqual([user]);
127
+ });
71
128
 
72
- export const count: RegisteredQuery<"public", any, Promise<any>> = query(
73
- async (ctx: GenericQueryCtx<any>, args: any) => {
74
- const adapter = createAdapter(ctx);
75
- return adapter({}).count(args);
76
- }
77
- );
129
+ test("should handle compound indexes that include id field", async () => {
130
+ const adapter = await getAdapter();
131
+ const user = await adapter.create({
132
+ model: "user",
133
+ data: {
134
+ name: "foo",
135
+ email: "foo@bar.com",
136
+ },
137
+ });
138
+ expect(
139
+ await adapter.findOne({
140
+ model: "user",
141
+ where: [
142
+ {
143
+ field: "id",
144
+ value: user.id,
145
+ },
146
+ {
147
+ field: "name",
148
+ value: "wrong name",
149
+ },
150
+ ],
151
+ })
152
+ ).toEqual(null);
153
+ expect(
154
+ await adapter.findOne({
155
+ model: "user",
156
+ where: [
157
+ {
158
+ field: "id",
159
+ value: user.id,
160
+ },
161
+ {
162
+ field: "name",
163
+ value: "foo",
164
+ },
165
+ ],
166
+ })
167
+ ).toEqual(user);
168
+ expect(
169
+ await adapter.findOne({
170
+ model: "user",
171
+ where: [
172
+ {
173
+ field: "id",
174
+ value: user.id,
175
+ },
176
+ {
177
+ field: "name",
178
+ value: "foo",
179
+ operator: "lt",
180
+ },
181
+ ],
182
+ })
183
+ ).toEqual(null);
184
+ expect(
185
+ await adapter.findOne({
186
+ model: "user",
187
+ where: [
188
+ {
189
+ field: "id",
190
+ value: user.id,
191
+ },
192
+ {
193
+ field: "name",
194
+ value: "foo",
195
+ operator: "lte",
196
+ },
197
+ ],
198
+ })
199
+ ).toEqual(user);
200
+ expect(
201
+ await adapter.findOne({
202
+ model: "user",
203
+ where: [
204
+ {
205
+ field: "id",
206
+ value: user.id,
207
+ },
208
+ {
209
+ field: "name",
210
+ value: "foo",
211
+ operator: "gt",
212
+ },
213
+ ],
214
+ })
215
+ ).toEqual(null);
216
+ expect(
217
+ await adapter.findOne({
218
+ model: "user",
219
+ where: [
220
+ {
221
+ field: "id",
222
+ value: user.id,
223
+ },
224
+ {
225
+ field: "name",
226
+ value: "foo",
227
+ operator: "gte",
228
+ },
229
+ ],
230
+ })
231
+ ).toEqual(user);
232
+ expect(
233
+ await adapter.findOne({
234
+ model: "user",
235
+ where: [
236
+ {
237
+ field: "id",
238
+ value: user.id,
239
+ },
240
+ {
241
+ field: "name",
242
+ operator: "in",
243
+ value: ["wrong", "name"],
244
+ },
245
+ ],
246
+ })
247
+ ).toEqual(null);
248
+ expect(
249
+ await adapter.findOne({
250
+ model: "user",
251
+ where: [
252
+ {
253
+ field: "id",
254
+ value: user.id,
255
+ },
256
+ {
257
+ field: "name",
258
+ operator: "in",
259
+ value: ["foo"],
260
+ },
261
+ ],
262
+ })
263
+ ).toEqual(user);
264
+ });
265
+ test("should automatically paginate", async () => {
266
+ const adapter = await getAdapter();
267
+ for (let i = 0; i < 300; i++) {
268
+ await adapter.create({
269
+ model: "user",
270
+ data: {
271
+ name: `foo${i}`,
272
+ email: `foo${i}@bar.com`,
273
+ },
274
+ });
275
+ }
276
+ // Better Auth defaults to a limit of 100
277
+ expect(
278
+ await adapter.findMany({
279
+ model: "user",
280
+ })
281
+ ).toHaveLength(100);
78
282
 
79
- export const update: RegisteredMutation<"public", any, Promise<any>> = mutation(
80
- async (ctx: GenericMutationCtx<any>, args: any) => {
81
- const adapter = createAdapter(ctx);
82
- const result = await adapter({}).update({
83
- ...args,
84
- update: deserialize(args.update),
85
- });
86
- return serialize(result);
87
- }
88
- );
283
+ // Pagination has a hardcoded numItems max of 200, this tests that it can handle
284
+ // specified limits beyond that
285
+ expect(
286
+ await adapter.findMany({
287
+ model: "user",
288
+ limit: 250,
289
+ })
290
+ ).toHaveLength(250);
291
+ expect(
292
+ await adapter.findMany({
293
+ model: "user",
294
+ limit: 350,
295
+ })
296
+ ).toHaveLength(300);
297
+ });
298
+ test("should handle OR where clauses", async () => {
299
+ const adapter = await getAdapter();
300
+ const user = await adapter.create({
301
+ model: "user",
302
+ data: {
303
+ name: "foo",
304
+ email: "foo@bar.com",
305
+ },
306
+ });
307
+ expect(
308
+ await adapter.findOne({
309
+ model: "user",
310
+ where: [
311
+ { field: "name", value: "bar", connector: "OR" },
312
+ { field: "name", value: "foo", connector: "OR" },
313
+ ],
314
+ })
315
+ ).toEqual(user);
316
+ });
317
+ test("should handle OR where clauses with sortBy", async () => {
318
+ const adapter = await getAdapter();
319
+ const fooUser = await adapter.create({
320
+ model: "user",
321
+ data: {
322
+ name: "foo",
323
+ email: "foo@bar.com",
324
+ },
325
+ });
326
+ const barUser = await adapter.create({
327
+ model: "user",
328
+ data: {
329
+ name: "bar",
330
+ email: "bar@bar.com",
331
+ },
332
+ });
333
+ await adapter.create({
334
+ model: "user",
335
+ data: {
336
+ name: "baz",
337
+ email: "baz@bar.com",
338
+ },
339
+ });
340
+ expect(
341
+ await adapter.findMany({
342
+ model: "user",
343
+ where: [
344
+ { field: "name", value: "bar", connector: "OR" },
345
+ { field: "name", value: "foo", connector: "OR" },
346
+ ],
347
+ sortBy: { field: "name", direction: "asc" },
348
+ })
349
+ ).toEqual([barUser, fooUser]);
350
+ expect(
351
+ await adapter.findMany({
352
+ model: "user",
353
+ where: [
354
+ { field: "name", value: "bar", connector: "OR" },
355
+ { field: "name", value: "foo", connector: "OR" },
356
+ ],
357
+ sortBy: { field: "name", direction: "desc" },
358
+ })
359
+ ).toEqual([fooUser, barUser]);
360
+ });
361
+ test("should handle count", async () => {
362
+ const adapter = await getAdapter();
363
+ await adapter.create({
364
+ model: "user",
365
+ data: {
366
+ name: "foo",
367
+ email: "foo@bar.com",
368
+ },
369
+ });
370
+ await adapter.create({
371
+ model: "user",
372
+ data: {
373
+ name: "bar",
374
+ email: "bar@bar.com",
375
+ },
376
+ });
377
+ expect(
378
+ await adapter.count({
379
+ model: "user",
380
+ where: [{ field: "name", value: "foo" }],
381
+ })
382
+ ).toEqual(1);
383
+ });
384
+ test("should handle queries with no index", async () => {
385
+ const adapter = await getAdapter();
386
+ const user = await adapter.create({
387
+ model: "user",
388
+ data: {
389
+ name: "foo",
390
+ email: "foo@bar.com",
391
+ emailVerified: true,
392
+ },
393
+ });
394
+ expect(
395
+ await adapter.findOne({
396
+ model: "user",
397
+ where: [{ field: "emailVerified", value: true }],
398
+ })
399
+ ).toEqual(user);
400
+ expect(
401
+ await adapter.findOne({
402
+ model: "user",
403
+ where: [{ field: "emailVerified", value: false }],
404
+ })
405
+ ).toEqual(null);
406
+ });
89
407
 
90
- export const updateMany: RegisteredMutation<
91
- "public",
92
- any,
93
- Promise<any>
94
- > = mutation(async (ctx: GenericMutationCtx<any>, args: any) => {
95
- const adapter = createAdapter(ctx);
96
- return adapter({}).updateMany(args);
97
- });
408
+ test("should handle compound operator on non-unique field without an index", async () => {
409
+ const adapter = await getAdapter();
410
+ await adapter.create({
411
+ model: "account",
412
+ data: {
413
+ accountId: "foo",
414
+ providerId: "bar",
415
+ userId: "baz",
416
+ accessTokenExpiresAt: null,
417
+ createdAt: Date.now(),
418
+ updatedAt: Date.now(),
419
+ },
420
+ });
421
+ expect(
422
+ await adapter.findOne({
423
+ model: "account",
424
+ where: [
425
+ {
426
+ operator: "lt",
427
+ connector: "AND",
428
+ field: "accessTokenExpiresAt",
429
+ value: Date.now(),
430
+ },
431
+ {
432
+ operator: "ne",
433
+ connector: "AND",
434
+ field: "accessTokenExpiresAt",
435
+ value: null,
436
+ },
437
+ ],
438
+ })
439
+ ).toEqual(null);
440
+ });
98
441
 
99
- const deleteMutation: RegisteredMutation<
100
- "public",
101
- any,
102
- Promise<any>
103
- > = mutation(async (ctx: GenericMutationCtx<any>, args: any) => {
104
- const adapter = createAdapter(ctx);
105
- await adapter({}).delete(args);
106
- });
107
- export { deleteMutation as delete };
442
+ test("should fail to create a record with a unique field that already exists", async () => {
443
+ const adapter = await getAdapter();
444
+ await adapter.create({
445
+ model: "user",
446
+ data: { name: "foo", email: "foo@bar.com" },
447
+ });
448
+ await expect(
449
+ adapter.create({
450
+ model: "user",
451
+ data: { name: "foo", email: "foo@bar.com" },
452
+ })
453
+ ).rejects.toThrow("user email already exists");
454
+ });
108
455
 
109
- export const deleteMany: RegisteredMutation<
110
- "public",
111
- any,
112
- Promise<any>
113
- > = mutation(async (ctx: GenericMutationCtx<any>, args: any) => {
114
- const adapter = createAdapter(ctx);
115
- return adapter({}).deleteMany(args);
116
- });
456
+ test("should be able to compare against a date", async () => {
457
+ const adapter = await getAdapter();
458
+ const user = await adapter.create({
459
+ model: "user",
460
+ data: {
461
+ name: "foo",
462
+ email: "foo@bar.com",
463
+ createdAt: new Date().toISOString(),
464
+ },
465
+ });
466
+ expect(
467
+ await adapter.findOne({
468
+ model: "user",
469
+ where: [{ field: "createdAt", value: new Date().toISOString() }],
470
+ })
471
+ ).toEqual(user);
472
+ });
473
+ }
@@ -45,7 +45,7 @@ const handler = (request: Request, siteUrl: string) => {
45
45
  const nextUrl = `${siteUrl}${requestUrl.pathname}${requestUrl.search}`;
46
46
  const newRequest = new Request(nextUrl, request);
47
47
  newRequest.headers.set("accept-encoding", "application/json");
48
- newRequest.headers.set("host", siteUrl);
48
+ newRequest.headers.set("host", new URL(siteUrl).host);
49
49
  return fetch(newRequest, { method: request.method, redirect: "manual" });
50
50
  };
51
51
 
@@ -1,9 +1,9 @@
1
1
  import type {
2
- BetterAuthOptions,
3
2
  BetterAuthPlugin,
4
3
  Session,
5
4
  User,
6
5
  } from "better-auth";
6
+ import type { BetterAuthOptions } from "better-auth/minimal";
7
7
  import { createAuthMiddleware, sessionMiddleware } from "better-auth/api";
8
8
  import {
9
9
  createAuthEndpoint,
@@ -53,6 +53,7 @@ const parseAuthConfig = (authConfig: AuthConfig, opts: { jwks?: string }) => {
53
53
  );
54
54
  }
55
55
  if (!isDataUriJwks && opts.jwks) {
56
+ // eslint-disable-next-line no-console
56
57
  console.warn(
57
58
  "Static JWKS provided to Convex plugin, but not to auth config. This adds an unnecessary network request for token verification."
58
59
  );
@@ -241,8 +242,9 @@ export const convex = (opts: {
241
242
  return {
242
243
  id: "convex",
243
244
  init: (ctx) => {
244
- const { options, logger } = ctx;
245
+ const { options, logger: _logger } = ctx;
245
246
  if (options.basePath !== "/api/auth" && !opts.options?.basePath) {
247
+ // eslint-disable-next-line no-console
246
248
  console.warn(
247
249
  `Better Auth basePath set to ${options.basePath} but no basePath is set in the Convex plugin. This is probably a mistake.`
248
250
  );
@@ -251,18 +253,11 @@ export const convex = (opts: {
251
253
  opts.options?.basePath &&
252
254
  options.basePath !== opts.options?.basePath
253
255
  ) {
256
+ // eslint-disable-next-line no-console
254
257
  console.warn(
255
258
  `Better Auth basePath ${options.basePath} does not match Convex plugin basePath ${opts.options?.basePath}. This is probably a mistake.`
256
259
  );
257
260
  }
258
- if (
259
- options.plugins?.every((p) => p.id !== "cross-domain") &&
260
- !options.baseURL
261
- ) {
262
- logger.warn(
263
- "Better Auth baseURL is undefined. This is probably a mistake."
264
- );
265
- }
266
261
  },
267
262
  hooks: {
268
263
  before: [
@@ -585,6 +580,7 @@ export const convex = (opts: {
585
580
  });
586
581
  return await runEndpoint();
587
582
  } else {
583
+ // eslint-disable-next-line no-console
588
584
  console.error(
589
585
  "Try temporarily setting jwksRotateOnTokenGenerationError: true on the Convex Better Auth plugin."
590
586
  );
@@ -64,7 +64,7 @@ const handler = (request: Request, opts: { convexSiteUrl: string }) => {
64
64
  const nextUrl = `${opts.convexSiteUrl}${requestUrl.pathname}${requestUrl.search}`;
65
65
  const headers = new Headers(request.headers);
66
66
  headers.set("accept-encoding", "application/json");
67
- headers.set("host", opts.convexSiteUrl);
67
+ headers.set("host", new URL(opts.convexSiteUrl).host);
68
68
  return fetch(nextUrl, {
69
69
  method: request.method,
70
70
  headers,
@@ -1,5 +1,6 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { Auth, betterAuth } from "better-auth";
2
+ import type { Auth } from "better-auth";
3
+ import type { betterAuth } from "better-auth/minimal";
3
4
  import { getSessionCookie } from "better-auth/cookies";
4
5
  import type {
5
6
  AuthProvider,
@@ -143,6 +144,7 @@ export const getToken = async (
143
144
  return { isFresh: false, token };
144
145
  }
145
146
  } catch (error) {
147
+ // eslint-disable-next-line no-console
146
148
  console.error("Error decoding JWT", error);
147
149
  }
148
150
  return await fetchToken();