@convex-dev/better-auth 0.12.0 → 0.12.2

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 (57) hide show
  1. package/dist/client/adapter-utils.d.ts.map +1 -1
  2. package/dist/client/adapter-utils.js +4 -2
  3. package/dist/client/adapter-utils.js.map +1 -1
  4. package/dist/client/adapter.d.ts +0 -1
  5. package/dist/client/adapter.d.ts.map +1 -1
  6. package/dist/client/adapter.js.map +1 -1
  7. package/dist/client/create-api.d.ts.map +1 -1
  8. package/dist/client/create-api.js +17 -7
  9. package/dist/client/create-api.js.map +1 -1
  10. package/dist/client/create-client.d.ts +0 -2
  11. package/dist/client/create-client.d.ts.map +1 -1
  12. package/dist/client/create-client.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +2 -2
  14. package/dist/component/_generated/api.d.ts.map +1 -1
  15. package/dist/component/_generated/component.d.ts +0 -3
  16. package/dist/component/_generated/component.d.ts.map +1 -1
  17. package/dist/component/testTriggerHandlers.d.ts +10 -0
  18. package/dist/component/testTriggerHandlers.d.ts.map +1 -0
  19. package/dist/component/testTriggerHandlers.js +25 -0
  20. package/dist/component/testTriggerHandlers.js.map +1 -0
  21. package/dist/nextjs/index.d.ts.map +1 -1
  22. package/dist/nextjs/index.js +24 -9
  23. package/dist/nextjs/index.js.map +1 -1
  24. package/dist/plugins/cross-domain/client.d.ts.map +1 -1
  25. package/dist/plugins/cross-domain/client.js +21 -1
  26. package/dist/plugins/cross-domain/client.js.map +1 -1
  27. package/dist/react-start/index.d.ts.map +1 -1
  28. package/dist/react-start/index.js +5 -0
  29. package/dist/react-start/index.js.map +1 -1
  30. package/dist/utils/index.d.ts.map +1 -1
  31. package/dist/utils/index.js +1 -0
  32. package/dist/utils/index.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +24 -24
  36. package/src/client/adapter-utils.ts +4 -2
  37. package/src/client/adapter.test.ts +215 -6
  38. package/src/client/adapter.ts +0 -1
  39. package/src/client/create-api.ts +36 -10
  40. package/src/client/create-client.ts +0 -2
  41. package/src/client/triggers.test.ts +58 -0
  42. package/src/component/_generated/api.ts +2 -2
  43. package/src/component/_generated/component.ts +0 -3
  44. package/src/component/testTriggerHandlers.ts +27 -0
  45. package/src/nextjs/index.test.ts +109 -0
  46. package/src/nextjs/index.ts +28 -9
  47. package/src/plugins/cross-domain/client.test.ts +26 -7
  48. package/src/plugins/cross-domain/client.ts +28 -2
  49. package/src/react-start/index.test.ts +91 -0
  50. package/src/react-start/index.ts +5 -0
  51. package/src/utils/index.ts +1 -0
  52. package/src/version.ts +1 -1
  53. package/dist/component/adapterTest.d.ts +0 -3
  54. package/dist/component/adapterTest.d.ts.map +0 -1
  55. package/dist/component/adapterTest.js +0 -202
  56. package/dist/component/adapterTest.js.map +0 -1
  57. package/src/component/adapterTest.ts +0 -275
@@ -2,8 +2,28 @@
2
2
 
3
3
  import { describe, it } from "vitest";
4
4
  import { convexTest } from "convex-test";
5
- import { api } from "../component/_generated/api.js";
5
+ import {
6
+ testAdapter,
7
+ transactionsTestSuite,
8
+ } from "@better-auth/test-utils/adapter";
9
+ import type { BetterAuthOptions } from "better-auth";
10
+ import { internal } from "../component/_generated/api.js";
6
11
  import schema from "../component/testProfiles/schema.profile-plugin-table.js";
12
+ import { createClient } from "./index.js";
13
+ import type { DataModel } from "../component/_generated/dataModel.js";
14
+ import type { ComponentApi } from "../component/_generated/component.js";
15
+ import {
16
+ additionalFieldsAuthFlowTestSuite,
17
+ additionalFieldsNormalTestSuite,
18
+ convexCustomTestSuite,
19
+ coreAuthFlowTestSuite,
20
+ coreNormalTestSuite,
21
+ multiJoinsMissingRowsTestSuite,
22
+ pluginTableNormalTestSuite,
23
+ renameFieldAndJoinTestSuite,
24
+ renameModelUserCustomTestSuite,
25
+ renameModelUserTableTestSuite,
26
+ } from "../test/adapter-factory/index.js";
7
27
 
8
28
  const MIN_NODE_MAJOR = 24;
9
29
  const currentNodeMajor = Number.parseInt(
@@ -11,16 +31,205 @@ const currentNodeMajor = Number.parseInt(
11
31
  10
12
32
  );
13
33
 
34
+ const NORMAL_DISABLED_TESTS = [
35
+ // dynamic-schema-plugin-table/dynamic-schema-additional-fields:
36
+ // Convex validators are static in this harness, so runtime schema mutation
37
+ // tests are validated in dedicated profiles instead.
38
+ "create - should apply default values to fields",
39
+ "create - should return null for nullable foreign keys",
40
+ "create - should support arrays",
41
+ "create - should support json",
42
+ // convex-id-generation:
43
+ // Convex controls generated IDs at write time.
44
+ "create - should use generateId if provided",
45
+ // offset-unsupported:
46
+ // Convex adapter rejects offset pagination.
47
+ "findMany - should be able to perform a complex limited join",
48
+ "findMany - should find many models with limit and offset",
49
+ "findMany - should find many models with offset",
50
+ "findMany - should find many models with sortBy and limit and offset",
51
+ "findMany - should find many models with sortBy and limit and offset and where",
52
+ "findMany - should find many models with sortBy and offset",
53
+ "findMany - should find many with both one-to-one and one-to-many joins",
54
+ "findMany - should find many with join and offset",
55
+ "findMany - should find many with join, where, limit, and offset",
56
+ "findMany - should find many with one-to-one join",
57
+ "findMany - should handle mixed joins correctly when some are missing",
58
+ "findMany - should return empty array when base records don't exist with joins",
59
+ "findMany - should return null for one-to-one join when joined records don't exist",
60
+ "findMany - should select fields with one-to-one join",
61
+ // profile-specific coverage:
62
+ // These are intentionally exercised in dedicated profile suites.
63
+ "findOne - backwards join with modified field name (session base, users-table join)",
64
+ "findOne - multiple joins should return result even when some joined tables have no matching rows",
65
+ "findOne - should find a model with modified field name",
66
+ "findOne - should find a model with modified model name",
67
+ "findOne - should join a model with modified field name",
68
+ "findOne - should not apply defaultValue if value not found",
69
+ "findOne - should return an object for one-to-one joins",
70
+ "findOne - should return null for failed base model lookup that has joins",
71
+ "findOne - should return null for one-to-one join when joined record doesn't exist",
72
+ "findOne - should select fields with one-to-one join",
73
+ "findOne - should work with both one-to-one and one-to-many joins",
74
+ ] as const;
75
+
76
+ const toDisableMap = (testNames: readonly string[]) =>
77
+ Object.fromEntries(testNames.map((testName) => [testName, true]));
78
+
79
+ const getOverrideBetterAuthOptions = (
80
+ opts: BetterAuthOptions
81
+ ): BetterAuthOptions => ({
82
+ ...opts,
83
+ advanced: {
84
+ ...opts.advanced,
85
+ database: {
86
+ ...opts.advanced?.database,
87
+ generateId: "uuid",
88
+ },
89
+ },
90
+ });
91
+
92
+ type AdapterModule = ComponentApi["adapter"];
93
+ type TestProfileName =
94
+ | "adapterAdditionalFields"
95
+ | "adapterPluginTable"
96
+ | "adapterRenameField"
97
+ | "adapterRenameUserCustom"
98
+ | "adapterRenameUserTable"
99
+ | "adapterOrganizationJoins";
100
+
101
+ type InternalWithTestProfiles = {
102
+ adapter: AdapterModule;
103
+ testProfiles: Record<TestProfileName, AdapterModule>;
104
+ };
105
+
14
106
  if (currentNodeMajor < MIN_NODE_MAJOR) {
15
107
  describe("Better Auth Adapter Tests", () => {
16
- it.skip(
17
- `requires Node ${MIN_NODE_MAJOR}+ (adapter test-utils uses explicit resource management syntax)`,
18
- () => {}
19
- );
108
+ it.skip(`requires Node ${MIN_NODE_MAJOR}+ (adapter test-utils uses explicit resource management syntax)`, () => {});
20
109
  });
21
110
  } else {
22
111
  describe("Better Auth Adapter Tests", async () => {
23
112
  const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
24
- await t.action(api.adapterTest.runTests, {});
113
+
114
+ // Bridge `convexAdapter`'s ctx surface to `convexTest`. Each adapter call
115
+ // re-enters convex-test's AsyncLocalStorage frame via t.query / t.mutation,
116
+ // which is required by convex-test ≥0.0.45. State persists across calls
117
+ // because the test database lives on `t`, not in ALS.
118
+ const wrappedCtx = {
119
+ runQuery: t.query.bind(t),
120
+ runMutation: t.mutation.bind(t),
121
+ } as any;
122
+
123
+ const internalWithTestProfiles =
124
+ internal as unknown as InternalWithTestProfiles;
125
+ const profileApi = (name: TestProfileName): { adapter: AdapterModule } => ({
126
+ adapter: internalWithTestProfiles.testProfiles[name],
127
+ });
128
+
129
+ const baseProfileClient = createClient<DataModel>(
130
+ { adapter: internalWithTestProfiles.adapter },
131
+ { verbose: false }
132
+ );
133
+ const additionalFieldsProfileClient = createClient<DataModel>(
134
+ profileApi("adapterAdditionalFields"),
135
+ { verbose: false }
136
+ );
137
+ const pluginTableProfileClient = createClient<DataModel>(
138
+ profileApi("adapterPluginTable"),
139
+ { verbose: false }
140
+ );
141
+ const renameFieldProfileClient = createClient<DataModel>(
142
+ profileApi("adapterRenameField"),
143
+ { verbose: false }
144
+ );
145
+ const renameUserCustomProfileClient = createClient<DataModel>(
146
+ profileApi("adapterRenameUserCustom"),
147
+ { verbose: false }
148
+ );
149
+ const renameUserTableProfileClient = createClient<DataModel>(
150
+ profileApi("adapterRenameUserTable"),
151
+ { verbose: false }
152
+ );
153
+ const organizationJoinsProfileClient = createClient<DataModel>(
154
+ profileApi("adapterOrganizationJoins"),
155
+ { verbose: false }
156
+ );
157
+
158
+ const noMigrations = () => {
159
+ // Convex schema is static — no migrations needed.
160
+ };
161
+
162
+ const { execute: executeBaseProfile } = await testAdapter({
163
+ adapter: () => baseProfileClient.adapter(wrappedCtx),
164
+ runMigrations: noMigrations,
165
+ overrideBetterAuthOptions: getOverrideBetterAuthOptions,
166
+ tests: [
167
+ coreNormalTestSuite({
168
+ disableTests: toDisableMap(NORMAL_DISABLED_TESTS),
169
+ }),
170
+ transactionsTestSuite({ disableTests: { ALL: true } }),
171
+ coreAuthFlowTestSuite(),
172
+ convexCustomTestSuite(),
173
+ ],
174
+ });
175
+
176
+ const { execute: executeAdditionalFieldsProfile } = await testAdapter({
177
+ adapter: () => additionalFieldsProfileClient.adapter(wrappedCtx),
178
+ runMigrations: noMigrations,
179
+ overrideBetterAuthOptions: getOverrideBetterAuthOptions,
180
+ prefixTests: "profile:additional-fields",
181
+ tests: [
182
+ additionalFieldsNormalTestSuite(),
183
+ additionalFieldsAuthFlowTestSuite(),
184
+ ],
185
+ });
186
+
187
+ const { execute: executePluginTableProfile } = await testAdapter({
188
+ adapter: () => pluginTableProfileClient.adapter(wrappedCtx),
189
+ runMigrations: noMigrations,
190
+ overrideBetterAuthOptions: getOverrideBetterAuthOptions,
191
+ prefixTests: "profile:plugin-table",
192
+ tests: [pluginTableNormalTestSuite()],
193
+ });
194
+
195
+ const { execute: executeRenameFieldProfile } = await testAdapter({
196
+ adapter: () => renameFieldProfileClient.adapter(wrappedCtx),
197
+ runMigrations: noMigrations,
198
+ overrideBetterAuthOptions: getOverrideBetterAuthOptions,
199
+ prefixTests: "profile:rename-field-join",
200
+ tests: [renameFieldAndJoinTestSuite()],
201
+ });
202
+
203
+ const { execute: executeRenameUserCustomProfile } = await testAdapter({
204
+ adapter: () => renameUserCustomProfileClient.adapter(wrappedCtx),
205
+ runMigrations: noMigrations,
206
+ overrideBetterAuthOptions: getOverrideBetterAuthOptions,
207
+ prefixTests: "profile:rename-user-custom",
208
+ tests: [renameModelUserCustomTestSuite()],
209
+ });
210
+
211
+ const { execute: executeRenameUserTableProfile } = await testAdapter({
212
+ adapter: () => renameUserTableProfileClient.adapter(wrappedCtx),
213
+ runMigrations: noMigrations,
214
+ overrideBetterAuthOptions: getOverrideBetterAuthOptions,
215
+ prefixTests: "profile:rename-user-table",
216
+ tests: [renameModelUserTableTestSuite()],
217
+ });
218
+
219
+ const { execute: executeOrganizationJoinsProfile } = await testAdapter({
220
+ adapter: () => organizationJoinsProfileClient.adapter(wrappedCtx),
221
+ runMigrations: noMigrations,
222
+ overrideBetterAuthOptions: getOverrideBetterAuthOptions,
223
+ prefixTests: "profile:organization-joins",
224
+ tests: [multiJoinsMissingRowsTestSuite()],
225
+ });
226
+
227
+ executeBaseProfile();
228
+ executeAdditionalFieldsProfile();
229
+ executePluginTableProfile();
230
+ executeRenameFieldProfile();
231
+ executeRenameUserCustomProfile();
232
+ executeRenameUserTableProfile();
233
+ executeOrganizationJoinsProfile();
25
234
  });
26
235
  }
@@ -171,7 +171,6 @@ export const convexAdapter = <
171
171
  ctx: Ctx,
172
172
  api: {
173
173
  adapter: ComponentApi["adapter"];
174
- adapterTest?: ComponentApi["adapterTest"];
175
174
  },
176
175
  config: {
177
176
  debugLogs?: DBAdapterDebugLogOption;
@@ -55,9 +55,7 @@ const whereValidator = (
55
55
  v.null()
56
56
  ),
57
57
  connector: v.optional(v.union(v.literal("AND"), v.literal("OR"))),
58
- mode: v.optional(
59
- v.union(v.literal("sensitive"), v.literal("insensitive"))
60
- ),
58
+ mode: v.optional(v.union(v.literal("sensitive"), v.literal("insensitive"))),
61
59
  });
62
60
 
63
61
  export const createApi = <Schema extends SchemaDefinition<any, any>>(
@@ -91,7 +89,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
91
89
  args.input.model as any,
92
90
  args.input.data
93
91
  );
94
- const doc = await ctx.db.get(id);
92
+ const doc = await ctx.db.get(args.input.model, id);
95
93
  if (!doc) {
96
94
  throw new Error(`Failed to create ${args.input.model}`);
97
95
  }
@@ -104,6 +102,13 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
104
102
  doc,
105
103
  }
106
104
  );
105
+ const updatedDoc = await ctx.db.get(args.input.model, id);
106
+ if (!updatedDoc) {
107
+ throw new Error(
108
+ `Failed to create ${args.input.model} (deleted by onCreate trigger?)`
109
+ );
110
+ }
111
+ return selectFields(updatedDoc, args.select);
107
112
  }
108
113
  return result;
109
114
  },
@@ -174,10 +179,14 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
174
179
  doc
175
180
  );
176
181
  await ctx.db.patch(
177
- doc._id as GenericId<string>,
182
+ args.input.model,
183
+ doc._id as GenericId<TableNames>,
178
184
  args.input.update as any
179
185
  );
180
- const updatedDoc = await ctx.db.get(doc._id as GenericId<string>);
186
+ const updatedDoc = await ctx.db.get(
187
+ args.input.model,
188
+ doc._id as GenericId<TableNames>
189
+ );
181
190
  if (!updatedDoc) {
182
191
  throw new Error(`Failed to update ${args.input.model}`);
183
192
  }
@@ -190,6 +199,16 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
190
199
  oldDoc: doc,
191
200
  }
192
201
  );
202
+ const innerUpdatedDoc = await ctx.db.get(
203
+ args.input.model,
204
+ doc._id as GenericId<TableNames>
205
+ );
206
+ if (!innerUpdatedDoc) {
207
+ throw new Error(
208
+ `Failed to update ${args.input.model} (deleted by onUpdate trigger?)`
209
+ );
210
+ }
211
+ return innerUpdatedDoc;
193
212
  }
194
213
  return updatedDoc;
195
214
  },
@@ -245,7 +264,8 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
245
264
  doc
246
265
  );
247
266
  await ctx.db.patch(
248
- doc._id as GenericId<string>,
267
+ args.input.model,
268
+ doc._id as GenericId<TableNames>,
249
269
  args.input.update as any
250
270
  );
251
271
 
@@ -254,7 +274,10 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
254
274
  args.onUpdateHandle as FunctionHandle<"mutation">,
255
275
  {
256
276
  model: args.input.model,
257
- newDoc: await ctx.db.get(doc._id as GenericId<string>),
277
+ newDoc: await ctx.db.get(
278
+ args.input.model,
279
+ doc._id as GenericId<TableNames>
280
+ ),
258
281
  oldDoc: doc,
259
282
  }
260
283
  );
@@ -286,7 +309,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
286
309
  if (!doc) {
287
310
  return;
288
311
  }
289
- await ctx.db.delete(doc._id as GenericId<string>);
312
+ await ctx.db.delete(args.input.model, doc._id as GenericId<TableNames>);
290
313
  if (args.onDeleteHandle) {
291
314
  await ctx.runMutation(
292
315
  args.onDeleteHandle as FunctionHandle<"mutation">,
@@ -330,7 +353,10 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
330
353
  }
331
354
  );
332
355
  }
333
- await ctx.db.delete(doc._id as GenericId<string>);
356
+ await ctx.db.delete(
357
+ args.input.model,
358
+ doc._id as GenericId<TableNames>
359
+ );
334
360
  });
335
361
  return {
336
362
  ...result,
@@ -17,7 +17,6 @@ import type { Infer } from "convex/values";
17
17
  import { convexAdapter } from "./adapter.js";
18
18
  import { corsRouter } from "convex-helpers/server/cors";
19
19
  import type defaultSchema from "../component/schema.js";
20
- import type { ComponentApi } from "../component/_generated/component.js";
21
20
  import type { CreateAuth, GenericCtx } from "./index.js";
22
21
  import type { TrustedOriginsOption } from "../utils/index.js";
23
22
 
@@ -70,7 +69,6 @@ type SlimComponentApi = {
70
69
  deleteOne: FunctionReference<"mutation", "internal">;
71
70
  deleteMany: FunctionReference<"mutation", "internal">;
72
71
  };
73
- adapterTest?: ComponentApi["adapterTest"];
74
72
  };
75
73
 
76
74
  type RouteCorsOptions =
@@ -0,0 +1,58 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { convexTest } from "convex-test";
5
+ import { createFunctionHandle } from "convex/server";
6
+ import schema from "../component/schema.js";
7
+ import { api, internal } from "../component/_generated/api.js";
8
+
9
+ const baseSessionData = () => ({
10
+ expiresAt: Date.now() + 60_000,
11
+ createdAt: Date.now(),
12
+ updatedAt: Date.now(),
13
+ userId: "user-1",
14
+ userAgent: "original",
15
+ });
16
+
17
+ describe("trigger result propagation", () => {
18
+ it("api.adapter.create returns the doc reflecting onCreateHandle writes", async () => {
19
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
20
+ const result = await t.run(async (ctx) => {
21
+ const handle = await createFunctionHandle(
22
+ internal.testTriggerHandlers.sessionOnCreateUpdater
23
+ );
24
+ return await ctx.runMutation(api.adapter.create, {
25
+ input: {
26
+ model: "session",
27
+ data: { ...baseSessionData(), token: "create-token-1" },
28
+ },
29
+ onCreateHandle: handle,
30
+ });
31
+ });
32
+ expect(result.userAgent).toBe("trigger-ran-on-create");
33
+ });
34
+
35
+ it("api.adapter.updateOne returns the doc reflecting onUpdateHandle writes", async () => {
36
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
37
+ const result = await t.run(async (ctx) => {
38
+ await ctx.runMutation(api.adapter.create, {
39
+ input: {
40
+ model: "session",
41
+ data: { ...baseSessionData(), token: "update-token-1" },
42
+ },
43
+ });
44
+ const handle = await createFunctionHandle(
45
+ internal.testTriggerHandlers.sessionOnUpdateUpdater
46
+ );
47
+ return await ctx.runMutation(api.adapter.updateOne, {
48
+ input: {
49
+ model: "session",
50
+ update: { userAgent: "set-by-update" },
51
+ where: [{ field: "token", operator: "eq", value: "update-token-1" }],
52
+ },
53
+ onUpdateHandle: handle,
54
+ });
55
+ });
56
+ expect(result.userAgent).toBe("trigger-ran-on-update");
57
+ });
58
+ });
@@ -9,13 +9,13 @@
9
9
  */
10
10
 
11
11
  import type * as adapter from "../adapter.js";
12
- import type * as adapterTest from "../adapterTest.js";
13
12
  import type * as testProfiles_adapterAdditionalFields from "../testProfiles/adapterAdditionalFields.js";
14
13
  import type * as testProfiles_adapterOrganizationJoins from "../testProfiles/adapterOrganizationJoins.js";
15
14
  import type * as testProfiles_adapterPluginTable from "../testProfiles/adapterPluginTable.js";
16
15
  import type * as testProfiles_adapterRenameField from "../testProfiles/adapterRenameField.js";
17
16
  import type * as testProfiles_adapterRenameUserCustom from "../testProfiles/adapterRenameUserCustom.js";
18
17
  import type * as testProfiles_adapterRenameUserTable from "../testProfiles/adapterRenameUserTable.js";
18
+ import type * as testTriggerHandlers from "../testTriggerHandlers.js";
19
19
 
20
20
  import type {
21
21
  ApiFromModules,
@@ -26,13 +26,13 @@ import { anyApi, componentsGeneric } from "convex/server";
26
26
 
27
27
  const fullApi: ApiFromModules<{
28
28
  adapter: typeof adapter;
29
- adapterTest: typeof adapterTest;
30
29
  "testProfiles/adapterAdditionalFields": typeof testProfiles_adapterAdditionalFields;
31
30
  "testProfiles/adapterOrganizationJoins": typeof testProfiles_adapterOrganizationJoins;
32
31
  "testProfiles/adapterPluginTable": typeof testProfiles_adapterPluginTable;
33
32
  "testProfiles/adapterRenameField": typeof testProfiles_adapterRenameField;
34
33
  "testProfiles/adapterRenameUserCustom": typeof testProfiles_adapterRenameUserCustom;
35
34
  "testProfiles/adapterRenameUserTable": typeof testProfiles_adapterRenameUserTable;
35
+ testTriggerHandlers: typeof testTriggerHandlers;
36
36
  }> = anyApi as any;
37
37
 
38
38
  /**
@@ -1893,9 +1893,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
1893
1893
  Name
1894
1894
  >;
1895
1895
  };
1896
- adapterTest: {
1897
- runTests: FunctionReference<"action", "internal", any, any, Name>;
1898
- };
1899
1896
  testProfiles: {
1900
1897
  adapterAdditionalFields: {
1901
1898
  create: FunctionReference<
@@ -0,0 +1,27 @@
1
+ // Stub trigger handles for tests that need a real FunctionReference to pass
2
+ // as onCreateHandle / onUpdateHandle on adapter writes.
3
+
4
+ import { internalMutationGeneric } from "convex/server";
5
+ import { v } from "convex/values";
6
+
7
+ export const sessionOnCreateUpdater = internalMutationGeneric({
8
+ args: { doc: v.any(), model: v.string() },
9
+ handler: async (ctx, args) => {
10
+ if (args.model === "session") {
11
+ await ctx.db.patch("session", args.doc._id, {
12
+ userAgent: "trigger-ran-on-create",
13
+ });
14
+ }
15
+ },
16
+ });
17
+
18
+ export const sessionOnUpdateUpdater = internalMutationGeneric({
19
+ args: { newDoc: v.any(), oldDoc: v.any(), model: v.string() },
20
+ handler: async (ctx, args) => {
21
+ if (args.model === "session") {
22
+ await ctx.db.patch("session", args.newDoc._id, {
23
+ userAgent: "trigger-ran-on-update",
24
+ });
25
+ }
26
+ },
27
+ });
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { convexBetterAuthNextJs } from "./index.js";
3
+
4
+ const SITE_URL = "https://test.convex.site";
5
+ const CONVEX_URL = "https://test.convex.cloud";
6
+
7
+ const setup = () => {
8
+ const { handler } = convexBetterAuthNextJs({
9
+ convexUrl: CONVEX_URL,
10
+ convexSiteUrl: SITE_URL,
11
+ });
12
+ const fetchSpy = vi
13
+ .spyOn(globalThis, "fetch")
14
+ .mockResolvedValue(new Response());
15
+ return { handler, fetchSpy };
16
+ };
17
+
18
+ const initOf = (spy: ReturnType<typeof vi.spyOn>): RequestInit =>
19
+ (spy.mock.calls[0]?.[1] as RequestInit) ?? {};
20
+
21
+ const headersOf = (spy: ReturnType<typeof vi.spyOn>): Headers =>
22
+ new Headers(initOf(spy).headers);
23
+
24
+ describe("convexBetterAuthNextJs handler", () => {
25
+ afterEach(() => {
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ it("strips hop-by-hop headers from the forwarded request", async () => {
30
+ const { handler, fetchSpy } = setup();
31
+ const request = new Request(
32
+ "https://app.example.com/api/auth/sign-in/email",
33
+ {
34
+ method: "POST",
35
+ headers: {
36
+ "transfer-encoding": "chunked",
37
+ "content-length": "42",
38
+ connection: "keep-alive",
39
+ "content-type": "application/json",
40
+ },
41
+ body: JSON.stringify({ email: "test@example.com" }),
42
+ }
43
+ );
44
+ await handler.POST(request);
45
+ const headers = headersOf(fetchSpy);
46
+ expect(headers.get("transfer-encoding")).toBeNull();
47
+ expect(headers.get("content-length")).toBeNull();
48
+ expect(headers.get("connection")).toBeNull();
49
+ });
50
+
51
+ it("forwards to upstream URL preserving path and query", async () => {
52
+ const { handler, fetchSpy } = setup();
53
+ const request = new Request(
54
+ "https://app.example.com/api/auth/sign-in/email?foo=bar",
55
+ { method: "POST", body: "{}" }
56
+ );
57
+ await handler.POST(request);
58
+ expect(fetchSpy.mock.calls[0]?.[0]).toBe(
59
+ `${SITE_URL}/api/auth/sign-in/email?foo=bar`
60
+ );
61
+ });
62
+
63
+ it("sets host and forwarding headers", async () => {
64
+ const { handler, fetchSpy } = setup();
65
+ const request = new Request(
66
+ "https://app.example.com/api/auth/sign-in/email",
67
+ { method: "POST", body: "{}" }
68
+ );
69
+ await handler.POST(request);
70
+ const headers = headersOf(fetchSpy);
71
+ expect(headers.get("host")).toBe(new URL(SITE_URL).host);
72
+ expect(headers.get("x-forwarded-host")).toBe("app.example.com");
73
+ expect(headers.get("x-forwarded-proto")).toBe("https");
74
+ expect(headers.get("x-better-auth-forwarded-host")).toBe("app.example.com");
75
+ expect(headers.get("x-better-auth-forwarded-proto")).toBe("https");
76
+ });
77
+
78
+ it("buffers POST body and forwards as ArrayBuffer", async () => {
79
+ const { handler, fetchSpy } = setup();
80
+ const body = JSON.stringify({ email: "test@example.com" });
81
+ const request = new Request(
82
+ "https://app.example.com/api/auth/sign-in/email",
83
+ { method: "POST", body }
84
+ );
85
+ await handler.POST(request);
86
+ const init = initOf(fetchSpy);
87
+ expect(init.body).toBeInstanceOf(ArrayBuffer);
88
+ expect(new TextDecoder().decode(init.body as ArrayBuffer)).toBe(body);
89
+ });
90
+
91
+ it("does not set body for GET requests", async () => {
92
+ const { handler, fetchSpy } = setup();
93
+ const request = new Request(
94
+ "https://app.example.com/api/auth/get-session",
95
+ { method: "GET" }
96
+ );
97
+ await handler.GET(request);
98
+ expect(initOf(fetchSpy).body).toBeUndefined();
99
+ });
100
+
101
+ it("does not set body for empty POST", async () => {
102
+ const { handler, fetchSpy } = setup();
103
+ const request = new Request("https://app.example.com/api/auth/sign-out", {
104
+ method: "POST",
105
+ });
106
+ await handler.POST(request);
107
+ expect(initOf(fetchSpy).body).toBeUndefined();
108
+ });
109
+ });
@@ -40,17 +40,36 @@ const parseConvexSiteUrl = (url: string) => {
40
40
  return url;
41
41
  };
42
42
 
43
- const handler = (request: Request, siteUrl: string) => {
43
+ const handler = async (request: Request, siteUrl: string) => {
44
44
  const requestUrl = new URL(request.url);
45
45
  const nextUrl = `${siteUrl}${requestUrl.pathname}${requestUrl.search}`;
46
- const newRequest = new Request(nextUrl, request);
47
- newRequest.headers.set("accept-encoding", "application/json");
48
- newRequest.headers.set("host", new URL(siteUrl).host);
49
- newRequest.headers.set("x-forwarded-host", requestUrl.host);
50
- newRequest.headers.set("x-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
51
- newRequest.headers.set("x-better-auth-forwarded-host", requestUrl.host);
52
- newRequest.headers.set("x-better-auth-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
53
- return fetch(newRequest, { method: request.method, redirect: "manual" });
46
+
47
+ const headers = new Headers(request.headers);
48
+ // Strip hop-by-hop headers; undici rejects outbound `transfer-encoding: chunked`.
49
+ headers.delete("transfer-encoding");
50
+ headers.delete("content-length");
51
+ headers.delete("connection");
52
+ headers.set("accept-encoding", "application/json");
53
+ headers.set("host", new URL(siteUrl).host);
54
+ headers.set("x-forwarded-host", requestUrl.host);
55
+ headers.set("x-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
56
+ headers.set("x-better-auth-forwarded-host", requestUrl.host);
57
+ headers.set("x-better-auth-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
58
+
59
+ const init: RequestInit = {
60
+ headers,
61
+ method: request.method,
62
+ redirect: "manual",
63
+ };
64
+
65
+ if (request.method !== "GET" && request.method !== "HEAD") {
66
+ const body = await request.arrayBuffer();
67
+ if (body.byteLength > 0) {
68
+ init.body = body;
69
+ }
70
+ }
71
+
72
+ return fetch(nextUrl, init);
54
73
  };
55
74
 
56
75
  const nextJsHandler = (siteUrl: string) => ({