better-auth 1.7.2 → 1.7.4

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 (68) hide show
  1. package/dist/api/dispatch.mjs +4 -1
  2. package/dist/api/index.d.mts +6 -22
  3. package/dist/api/index.mjs +5 -1
  4. package/dist/api/routes/account.d.mts +1 -9
  5. package/dist/api/routes/account.mjs +0 -6
  6. package/dist/api/routes/callback.mjs +0 -2
  7. package/dist/api/routes/password.mjs +0 -2
  8. package/dist/api/routes/session.mjs +12 -5
  9. package/dist/api/routes/sign-in.d.mts +2 -2
  10. package/dist/api/routes/sign-in.mjs +1 -5
  11. package/dist/api/routes/sign-up.mjs +0 -2
  12. package/dist/api/routes/update-user.mjs +0 -2
  13. package/dist/api/to-auth-endpoints.mjs +2 -0
  14. package/dist/auth/base.mjs +13 -1
  15. package/dist/auth/trusted-origins.mjs +6 -4
  16. package/dist/client/config.d.mts +2 -0
  17. package/dist/client/config.mjs +1 -0
  18. package/dist/client/vue/index.d.mts +31 -20
  19. package/dist/client/vue/index.mjs +23 -5
  20. package/dist/context/create-context.mjs +2 -0
  21. package/dist/cookies/session-store.d.mts +0 -1
  22. package/dist/cookies/session-store.mjs +16 -5
  23. package/dist/db/adapter-kysely.mjs +1 -2
  24. package/dist/db/get-migration.d.mts +1 -0
  25. package/dist/db/get-migration.mjs +10 -32
  26. package/dist/db/internal-adapter.mjs +14 -17
  27. package/dist/db/schema.d.mts +0 -1
  28. package/dist/db/with-hooks.mjs +2 -1
  29. package/dist/index.mjs +0 -1
  30. package/dist/oauth2/account-key.mjs +1 -5
  31. package/dist/oauth2/link-account.d.mts +0 -1
  32. package/dist/oauth2/link-account.mjs +5 -11
  33. package/dist/package.mjs +1 -1
  34. package/dist/plugins/admin/routes.mjs +0 -3
  35. package/dist/plugins/device-authorization/index.d.mts +11 -11
  36. package/dist/plugins/email-otp/routes.mjs +0 -2
  37. package/dist/plugins/generic-oauth/index.mjs +14 -10
  38. package/dist/plugins/generic-oauth/providers/auth0.mjs +2 -4
  39. package/dist/plugins/generic-oauth/providers/keycloak.mjs +6 -9
  40. package/dist/plugins/generic-oauth/providers/line.mjs +0 -1
  41. package/dist/plugins/generic-oauth/providers/okta.mjs +6 -9
  42. package/dist/plugins/generic-oauth/providers/slack.mjs +0 -1
  43. package/dist/plugins/generic-oauth/types.d.mts +0 -9
  44. package/dist/plugins/haveibeenpwned/index.d.mts +9 -1
  45. package/dist/plugins/haveibeenpwned/index.mjs +31 -11
  46. package/dist/plugins/index.d.mts +3 -3
  47. package/dist/plugins/index.mjs +2 -2
  48. package/dist/plugins/last-login-method/index.mjs +1 -0
  49. package/dist/plugins/oauth-proxy/index.d.mts +17 -0
  50. package/dist/plugins/oauth-proxy/index.mjs +145 -99
  51. package/dist/plugins/one-tap/index.mjs +0 -1
  52. package/dist/plugins/open-api/generator.mjs +15 -1
  53. package/dist/plugins/open-api/index.mjs +0 -1
  54. package/dist/plugins/organization/has-permission.mjs +4 -2
  55. package/dist/plugins/phone-number/routes.mjs +0 -3
  56. package/dist/plugins/siwe/index.mjs +0 -3
  57. package/dist/plugins/test-utils/auth-helpers.mjs +8 -3
  58. package/dist/plugins/test-utils/index.d.mts +1 -1
  59. package/dist/plugins/test-utils/types.d.mts +10 -9
  60. package/dist/plugins/two-factor/client.d.mts +1 -0
  61. package/dist/plugins/two-factor/error-code.d.mts +1 -0
  62. package/dist/plugins/two-factor/error-code.mjs +1 -0
  63. package/dist/plugins/two-factor/index.d.mts +1 -0
  64. package/dist/plugins/two-factor/index.mjs +3 -2
  65. package/dist/state.d.mts +0 -13
  66. package/dist/test-utils/test-instance.mjs +10 -9
  67. package/package.json +12 -12
  68. package/dist/utils/index.mjs +0 -5
@@ -8,22 +8,50 @@ import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/accou
8
8
  import { redirectOnError } from "../../oauth2/errors.mjs";
9
9
  import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
10
10
  import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
11
+ import { setOAuthState } from "../../api/state/oauth.mjs";
11
12
  import { parseGenericState } from "../../state.mjs";
12
13
  import { PACKAGE_VERSION } from "../../version.mjs";
13
14
  import { parseJSON } from "../../client/parser.mjs";
14
15
  import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
16
+ import { accountSchema, userSchema } from "@better-auth/core/db";
15
17
  import { safeJSONParse } from "@better-auth/core/utils/json";
16
18
  import { defu } from "defu";
17
19
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
18
20
  import * as z from "zod";
19
21
  //#region src/plugins/oauth-proxy/index.ts
20
- const consumeOAuthProxyState = async (ctx, state) => {
22
+ /**
23
+ * Passthrough payload containing OAuth profile data.
24
+ * Used to transfer OAuth credentials from production to preview
25
+ * without creating user/session on production.
26
+ * @internal
27
+ */
28
+ const passthroughPayloadSchema = z.looseObject({
29
+ userInfo: z.looseObject(userSchema.omit({
30
+ createdAt: true,
31
+ updatedAt: true
32
+ }).shape),
33
+ account: z.looseObject(accountSchema.omit({
34
+ id: true,
35
+ userId: true,
36
+ createdAt: true,
37
+ updatedAt: true
38
+ }).shape),
39
+ profile: z.record(z.string(), z.unknown()).optional(),
40
+ state: z.string().min(1),
41
+ callbackURL: z.string().min(1),
42
+ newUserURL: z.string().optional(),
43
+ errorURL: z.string().optional(),
44
+ disableSignUp: z.boolean().optional(),
45
+ timestamp: z.number()
46
+ });
47
+ const restoreOAuthProxyState = async (ctx, state) => {
21
48
  try {
22
- await parseGenericState(ctx, state, { skipStateCookieCheck: true });
23
- return true;
49
+ const stateData = await parseGenericState(ctx, state, { skipStateCookieCheck: true });
50
+ await setOAuthState(stateData);
51
+ return stateData;
24
52
  } catch (e) {
25
53
  ctx.context.logger.warn("OAuth proxy state missing or invalid", e);
26
- return false;
54
+ return null;
27
55
  }
28
56
  };
29
57
  const oauthProxyQuerySchema = z.object({
@@ -38,104 +66,122 @@ const oauthCallbackQuerySchema = z.object({
38
66
  const oAuthProxy = (opts) => {
39
67
  const maxAge = opts?.maxAge ?? 60;
40
68
  const getEncryptionKey = (ctx) => opts?.secret ?? ctx.context.secretConfig;
69
+ const oauthProxyCompletion = createAuthEndpoint("/callback/:id/oauth-proxy", {
70
+ method: "GET",
71
+ operationId: "oauthProxyCompletion",
72
+ query: oauthProxyQuerySchema,
73
+ use: [originCheck((ctx) => ctx.query.callbackURL)],
74
+ metadata: { scope: "http" }
75
+ }, async (ctx) => {
76
+ const baseURLStr = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : getOrigin(ctx.context.baseURL) || "";
77
+ const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${stripTrailingSlash(baseURLStr)}/api/auth/error`;
78
+ const encryptedProfile = ctx.query.profile;
79
+ if (!encryptedProfile) {
80
+ ctx.context.logger.error("OAuth proxy callback missing profile data");
81
+ throw redirectOnError(ctx, defaultErrorURL, "missing_profile");
82
+ }
83
+ let decryptedPayload;
84
+ try {
85
+ decryptedPayload = await symmetricDecrypt({
86
+ key: getEncryptionKey(ctx),
87
+ data: encryptedProfile
88
+ });
89
+ } catch (e) {
90
+ ctx.context.logger.error("Failed to decrypt OAuth proxy profile", e);
91
+ throw redirectOnError(ctx, defaultErrorURL, "invalid_profile");
92
+ }
93
+ let payload;
94
+ try {
95
+ payload = passthroughPayloadSchema.parse(parseJSON(decryptedPayload));
96
+ } catch (e) {
97
+ ctx.context.logger.error("Failed to parse OAuth proxy payload", e);
98
+ throw redirectOnError(ctx, defaultErrorURL, "invalid_payload");
99
+ }
100
+ const errorURL = payload.errorURL || defaultErrorURL;
101
+ if (ctx.path?.startsWith("/callback/") && ctx.params.id !== payload.account.providerId) {
102
+ ctx.context.logger.warn("OAuth proxy callback provider mismatch");
103
+ throw redirectOnError(ctx, errorURL, "provider_mismatch");
104
+ }
105
+ const age = (Date.now() - payload.timestamp) / 1e3;
106
+ if (age > maxAge || age < -10) {
107
+ ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
108
+ throw redirectOnError(ctx, errorURL, "payload_expired");
109
+ }
110
+ if (!await restoreOAuthProxyState(ctx, payload.state)) throw redirectOnError(ctx, errorURL, "state_mismatch");
111
+ let result;
112
+ try {
113
+ result = await handleOAuthUserInfo(ctx, {
114
+ userInfo: payload.userInfo,
115
+ account: payload.account,
116
+ callbackURL: payload.callbackURL,
117
+ disableSignUp: payload.disableSignUp,
118
+ source: {
119
+ method: "oauth",
120
+ oauth: {
121
+ providerId: payload.account.providerId,
122
+ profile: payload.profile
123
+ }
124
+ }
125
+ });
126
+ } catch (e) {
127
+ if (isAPIError(e) && e.body?.code) throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
128
+ throw e;
129
+ }
130
+ if (result.error) {
131
+ ctx.context.logger.error("OAuth proxy callback error", result.error);
132
+ throw redirectOnError(ctx, errorURL, result.error.split(" ").join("_"));
133
+ }
134
+ if (!result.data) {
135
+ ctx.context.logger.error("OAuth proxy callback missing session data");
136
+ throw redirectOnError(ctx, errorURL, "user_creation_failed");
137
+ }
138
+ await setSessionCookie(ctx, result.data);
139
+ const finalURL = result.isRegister ? payload.newUserURL || payload.callbackURL : payload.callbackURL;
140
+ throw ctx.redirect(finalURL);
141
+ });
41
142
  return {
42
143
  id: "oauth-proxy",
43
144
  version: PACKAGE_VERSION,
44
145
  options: opts,
45
- endpoints: { oAuthProxy: createAuthEndpoint("/oauth-proxy-callback", {
46
- method: "GET",
47
- operationId: "oauthProxyCallback",
48
- query: oauthProxyQuerySchema,
49
- use: [originCheck((ctx) => ctx.query.callbackURL)],
50
- metadata: { openapi: {
146
+ endpoints: {
147
+ /**
148
+ * @deprecated OAuth proxy callbacks now use `/callback/:id/oauth-proxy`.
149
+ * This endpoint will be removed in the next minor release.
150
+ */
151
+ oAuthProxy: createAuthEndpoint("/oauth-proxy-callback", {
152
+ method: "GET",
51
153
  operationId: "oauthProxyCallback",
52
- description: "OAuth Proxy Callback",
53
- parameters: [{
54
- in: "query",
55
- name: "callbackURL",
56
- required: true,
57
- description: "The URL to redirect to after the proxy"
58
- }, {
59
- in: "query",
60
- name: "profile",
61
- required: false,
62
- description: "Encrypted OAuth profile data"
63
- }],
64
- responses: { 302: {
65
- description: "Redirect",
66
- headers: { Location: {
67
- description: "The URL to redirect to",
68
- schema: { type: "string" }
154
+ query: oauthProxyQuerySchema,
155
+ use: [originCheck((ctx) => ctx.query.callbackURL)],
156
+ metadata: { openapi: {
157
+ operationId: "oauthProxyCallback",
158
+ deprecated: true,
159
+ description: "OAuth Proxy Callback",
160
+ parameters: [{
161
+ in: "query",
162
+ name: "callbackURL",
163
+ required: true,
164
+ description: "The URL to redirect to after the proxy"
165
+ }, {
166
+ in: "query",
167
+ name: "profile",
168
+ required: false,
169
+ description: "Encrypted OAuth profile data"
170
+ }],
171
+ responses: { 302: {
172
+ description: "Redirect",
173
+ headers: { Location: {
174
+ description: "The URL to redirect to",
175
+ schema: { type: "string" }
176
+ } }
69
177
  } }
70
178
  } }
71
- } }
72
- }, async (ctx) => {
73
- const baseURLStr = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : getOrigin(ctx.context.baseURL) || "";
74
- const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${stripTrailingSlash(baseURLStr)}/api/auth/error`;
75
- const encryptedProfile = ctx.query.profile;
76
- if (!encryptedProfile) {
77
- ctx.context.logger.error("OAuth proxy callback missing profile data");
78
- throw redirectOnError(ctx, defaultErrorURL, "missing_profile");
79
- }
80
- let decryptedPayload;
81
- try {
82
- decryptedPayload = await symmetricDecrypt({
83
- key: getEncryptionKey(ctx),
84
- data: encryptedProfile
85
- });
86
- } catch (e) {
87
- ctx.context.logger.error("Failed to decrypt OAuth proxy profile", e);
88
- throw redirectOnError(ctx, defaultErrorURL, "invalid_profile");
89
- }
90
- let payload;
91
- try {
92
- payload = parseJSON(decryptedPayload);
93
- } catch (e) {
94
- ctx.context.logger.error("Failed to parse OAuth proxy payload", e);
95
- throw redirectOnError(ctx, defaultErrorURL, "invalid_payload");
96
- }
97
- if (typeof payload.timestamp !== "number" || !payload.userInfo || !payload.account || !payload.state || !payload.callbackURL) {
98
- ctx.context.logger.error("Failed to parse OAuth proxy payload");
99
- throw redirectOnError(ctx, defaultErrorURL, "invalid_payload");
100
- }
101
- const errorURL = payload.errorURL || defaultErrorURL;
102
- const age = (Date.now() - payload.timestamp) / 1e3;
103
- if (age > maxAge || age < -10) {
104
- ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
105
- throw redirectOnError(ctx, errorURL, "payload_expired");
106
- }
107
- if (!await consumeOAuthProxyState(ctx, payload.state)) throw redirectOnError(ctx, errorURL, "state_mismatch");
108
- let result;
109
- try {
110
- result = await handleOAuthUserInfo(ctx, {
111
- userInfo: payload.userInfo,
112
- account: payload.account,
113
- callbackURL: payload.callbackURL,
114
- disableSignUp: payload.disableSignUp,
115
- source: {
116
- method: "oauth",
117
- oauth: {
118
- providerId: payload.account.providerId,
119
- profile: payload.profile
120
- }
121
- }
122
- });
123
- } catch (e) {
124
- if (isAPIError(e) && e.body?.code) throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
125
- throw e;
126
- }
127
- if (result.error) {
128
- ctx.context.logger.error("OAuth proxy callback error", result.error);
129
- throw redirectOnError(ctx, errorURL, result.error.split(" ").join("_"));
130
- }
131
- if (!result.data) {
132
- ctx.context.logger.error("OAuth proxy callback missing session data");
133
- throw redirectOnError(ctx, errorURL, "user_creation_failed");
134
- }
135
- await setSessionCookie(ctx, result.data);
136
- const finalURL = result.isRegister ? payload.newUserURL || payload.callbackURL : payload.callbackURL;
137
- throw ctx.redirect(finalURL);
138
- }) },
179
+ }, async (ctx) => oauthProxyCompletion({
180
+ ...ctx,
181
+ params: { id: "oauth-proxy" }
182
+ })),
183
+ oAuthProxyCompletion: oauthProxyCompletion
184
+ },
139
185
  hooks: {
140
186
  before: [{
141
187
  matcher(context) {
@@ -143,6 +189,8 @@ const oAuthProxy = (opts) => {
143
189
  },
144
190
  handler: createAuthMiddleware(async (ctx) => {
145
191
  if (checkSkipProxy(ctx, opts)) return;
192
+ const providerId = ctx.body?.provider;
193
+ if (!providerId) return;
146
194
  const currentURL = resolveCurrentURL(ctx, opts);
147
195
  const productionURL = opts?.productionURL;
148
196
  const originalCallbackURL = ctx.body?.callbackURL || ctx.context.baseURL;
@@ -150,8 +198,7 @@ const oAuthProxy = (opts) => {
150
198
  const productionBaseURL = `${stripTrailingSlash(productionURL)}${ctx.context.options.basePath || "/api/auth"}`;
151
199
  ctx.context.baseURL = productionBaseURL;
152
200
  }
153
- const newCallbackURL = `${stripTrailingSlash(currentURL.origin)}${ctx.context.options.basePath || "/api/auth"}/oauth-proxy-callback?callbackURL=${encodeURIComponent(originalCallbackURL)}`;
154
- if (!ctx.body) return;
201
+ const newCallbackURL = `${stripTrailingSlash(currentURL.origin)}${ctx.context.options.basePath || "/api/auth"}/callback/${providerId}/oauth-proxy?callbackURL=${encodeURIComponent(originalCallbackURL)}`;
155
202
  ctx.body.callbackURL = newCallbackURL;
156
203
  })
157
204
  }, {
@@ -261,7 +308,6 @@ const oAuthProxy = (opts) => {
261
308
  },
262
309
  profile: providerProfile,
263
310
  account: {
264
- providerId: provider.id,
265
311
  ...accountKey,
266
312
  accessToken: tokens.accessToken,
267
313
  refreshToken: tokens.refreshToken,
@@ -342,7 +388,7 @@ const oAuthProxy = (opts) => {
342
388
  },
343
389
  handler: createAuthMiddleware(async (ctx) => {
344
390
  const location = ctx.context.responseHeaders?.get("location");
345
- if (!location?.includes("/oauth-proxy-callback?callbackURL") || !location.startsWith("http")) return;
391
+ if (!location?.includes("/oauth-proxy?callbackURL") && !location?.includes("/oauth-proxy-callback?callbackURL") || !location.startsWith("http")) return;
346
392
  const productionOrigin = getOrigin(opts?.productionURL || (typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0) || ctx.context.baseURL);
347
393
  const locationURL = new URL(location);
348
394
  if (locationURL.origin === productionOrigin) {
@@ -71,7 +71,6 @@ const oneTap = (options) => ({
71
71
  },
72
72
  account: {
73
73
  providerId: "google",
74
- issuer: "https://accounts.google.com",
75
74
  accountId: sub,
76
75
  idToken,
77
76
  scope: "openid,profile,email"
@@ -113,6 +113,20 @@ function schemaAcceptsUndefined(zodType) {
113
113
  }
114
114
  return false;
115
115
  }
116
+ /**
117
+ * Resolve input optionality exactly as Zod's JSON Schema emitter does.
118
+ *
119
+ * @see https://github.com/colinhacks/zod/blob/v4.5.4/packages/zod/src/v4/core/json-schema-processors.ts#L294-L308
120
+ */
121
+ function getZodInputOptionality(zodType) {
122
+ const def = zodType._zod.def;
123
+ if (def.type === "pipe") {
124
+ const pipeDef = def;
125
+ if (pipeDef.in._zod.traits.has("$ZodTransform")) return getZodInputOptionality(pipeDef.out);
126
+ }
127
+ if (def.type === "catch") return getZodInputOptionality(def.innerType);
128
+ return zodType._zod.optin;
129
+ }
116
130
  function isUndefinedOnlySchema(zodType) {
117
131
  return zodType instanceof z.ZodUndefined || zodType instanceof z.ZodVoid;
118
132
  }
@@ -252,7 +266,7 @@ function toOpenApiSchema(zodType) {
252
266
  Object.entries(shape).forEach(([key, value]) => {
253
267
  if (value instanceof z.ZodType) {
254
268
  properties[key] = toOpenApiSchema(value);
255
- if (!schemaAcceptsUndefined(value)) required.push(key);
269
+ if (getZodInputOptionality(value) === void 0) required.push(key);
256
270
  }
257
271
  });
258
272
  return withDescription({
@@ -1,5 +1,4 @@
1
1
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
2
- import "../../utils/index.mjs";
3
2
  import { APIError } from "../../api/index.mjs";
4
3
  import { PACKAGE_VERSION } from "../../version.mjs";
5
4
  import { generator } from "./generator.mjs";
@@ -3,6 +3,7 @@ import { defaultRoles } from "./access/statement.mjs";
3
3
  import { cacheAllRoles, hasPermissionFn } from "./permission.mjs";
4
4
  import * as z from "zod";
5
5
  //#region src/plugins/organization/has-permission.ts
6
+ const rolePermissionsSchema = z.record(z.string(), z.array(z.string()));
6
7
  const hasPermission = async (input, ctx) => {
7
8
  let acRoles = { ...input.options.roles || defaultRoles };
8
9
  if (ctx && input.organizationId && input.options.dynamicAccessControl?.enabled && input.options.ac && !input.useMemoryCache) {
@@ -14,9 +15,10 @@ const hasPermission = async (input, ctx) => {
14
15
  }]
15
16
  });
16
17
  for (const { role, permission: permissionsString } of roles) {
17
- const result = z.record(z.string(), z.array(z.string())).safeParse(JSON.parse(permissionsString));
18
+ const permissions = JSON.parse(permissionsString);
19
+ const result = rolePermissionsSchema.safeParse(permissions);
18
20
  if (!result.success) {
19
- ctx.context.logger.error("[hasPermission] Invalid permissions for role " + role, { permissions: JSON.parse(permissionsString) });
21
+ ctx.context.logger.error("[hasPermission] Invalid permissions for role " + role, { permissions });
20
22
  throw new APIError("INTERNAL_SERVER_ERROR", { message: "Invalid permissions for role " + role });
21
23
  }
22
24
  const merged = { ...acRoles[role]?.statements };
@@ -4,9 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
4
4
  import { setSessionCookie } from "../../cookies/index.mjs";
5
5
  import { getSessionFromCtx } from "../../api/routes/session.mjs";
6
6
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
7
- import "../../utils/index.mjs";
8
7
  import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
9
- import { createLocalAccountIssuer } from "@better-auth/core/db";
10
8
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
11
9
  import { createAuthEndpoint } from "@better-auth/core/api";
12
10
  import * as z from "zod";
@@ -476,7 +474,6 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
476
474
  if (!await ctx.context.internalAdapter.findCredentialAccount(user.id)) await ctx.context.internalAdapter.createAccount({
477
475
  userId: user.id,
478
476
  providerId: "credential",
479
- issuer: createLocalAccountIssuer("credential"),
480
477
  accountId: user.id,
481
478
  password: hashedPassword
482
479
  });
@@ -6,7 +6,6 @@ import { PACKAGE_VERSION } from "../../version.mjs";
6
6
  import { toChecksumAddress } from "../../utils/hashing.mjs";
7
7
  import { normalizeSiweDomain, parseSiweMessage } from "./parse-message.mjs";
8
8
  import { schema } from "./schema.mjs";
9
- import { createLocalAccountIssuer } from "@better-auth/core/db";
10
9
  import { createAuthEndpoint } from "@better-auth/core/api";
11
10
  import * as z from "zod";
12
11
  import { createPlaceholderEmail } from "@better-auth/core/utils/email";
@@ -215,7 +214,6 @@ const siwe = (options) => {
215
214
  await ctx.context.internalAdapter.createAccount({
216
215
  userId: user.id,
217
216
  providerId: "siwe",
218
- issuer: createLocalAccountIssuer("siwe"),
219
217
  accountId: `${walletAddress}:${chainId}`,
220
218
  createdAt: /* @__PURE__ */ new Date(),
221
219
  updatedAt: /* @__PURE__ */ new Date()
@@ -234,7 +232,6 @@ const siwe = (options) => {
234
232
  await ctx.context.internalAdapter.createAccount({
235
233
  userId: user.id,
236
234
  providerId: "siwe",
237
- issuer: createLocalAccountIssuer("siwe"),
238
235
  accountId: `${walletAddress}:${chainId}`,
239
236
  createdAt: /* @__PURE__ */ new Date(),
240
237
  updatedAt: /* @__PURE__ */ new Date()
@@ -1,10 +1,15 @@
1
1
  import { createCookieHeaders, createTestCookie } from "./cookie-builder.mjs";
2
+ import { sessionSchema } from "@better-auth/core/db";
2
3
  //#region src/plugins/test-utils/auth-helpers.ts
4
+ function createSession(ctx, opts) {
5
+ const additionalFields = Object.fromEntries(Object.entries(opts.session ?? {}).filter(([key]) => !Object.hasOwn(sessionSchema.shape, key)));
6
+ return ctx.internalAdapter.createSession(opts.userId, false, additionalFields, true);
7
+ }
3
8
  function createLogin(ctx) {
4
9
  return async (opts) => {
5
10
  const user = await ctx.internalAdapter.findUserById(opts.userId);
6
11
  if (!user) throw new Error(`User not found: ${opts.userId}`);
7
- const session = await ctx.internalAdapter.createSession(opts.userId);
12
+ const session = await createSession(ctx, opts);
8
13
  return {
9
14
  session,
10
15
  user,
@@ -16,12 +21,12 @@ function createLogin(ctx) {
16
21
  }
17
22
  function createGetAuthHeaders(ctx) {
18
23
  return async (opts) => {
19
- return createCookieHeaders(ctx, (await ctx.internalAdapter.createSession(opts.userId)).token);
24
+ return createCookieHeaders(ctx, (await createSession(ctx, opts)).token);
20
25
  };
21
26
  }
22
27
  function createGetCookies(ctx) {
23
28
  return async (opts) => {
24
- return createTestCookie(ctx, (await ctx.internalAdapter.createSession(opts.userId)).token, opts.domain);
29
+ return createTestCookie(ctx, (await createSession(ctx, opts)).token, opts.domain);
25
30
  };
26
31
  }
27
32
  //#endregion
@@ -1,4 +1,4 @@
1
- import { LoginResult, TestCookie, TestHelpers, TestUtilsOptions } from "./types.mjs";
1
+ import { LoginResult, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions } from "./types.mjs";
2
2
  import * as _$_better_auth_core0 from "@better-auth/core";
3
3
 
4
4
  //#region src/plugins/test-utils/index.d.ts
@@ -21,6 +21,12 @@ interface LoginResult {
21
21
  cookies: TestCookie[];
22
22
  token: string;
23
23
  }
24
+ /** Options for creating a session with the test auth helpers. */
25
+ interface TestAuthOptions {
26
+ userId: string;
27
+ /** Additional session fields. Standard session fields are ignored. */
28
+ session?: Record<string, unknown>;
29
+ }
24
30
  interface TestHelpers {
25
31
  createUser(overrides?: Partial<User> & Record<string, unknown>): User;
26
32
  createOrganization?(overrides?: Record<string, unknown>): Record<string, unknown>;
@@ -33,18 +39,13 @@ interface TestHelpers {
33
39
  }): Promise<Record<string, unknown>>;
34
40
  deleteUser(userId: string): Promise<void>;
35
41
  deleteOrganization?(orgId: string): Promise<void>;
36
- login(opts: {
37
- userId: string;
38
- }): Promise<LoginResult>;
39
- getAuthHeaders(opts: {
40
- userId: string;
41
- }): Promise<Headers>;
42
- getCookies(opts: {
43
- userId: string;
42
+ login(opts: TestAuthOptions): Promise<LoginResult>;
43
+ getAuthHeaders(opts: TestAuthOptions): Promise<Headers>;
44
+ getCookies(opts: TestAuthOptions & {
44
45
  domain?: string;
45
46
  }): Promise<TestCookie[]>;
46
47
  getOTP?(identifier: string): string | undefined;
47
48
  clearOTPs?(): void;
48
49
  }
49
50
  //#endregion
50
- export { LoginResult, TestCookie, TestHelpers, TestUtilsOptions };
51
+ export { LoginResult, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions };
@@ -61,6 +61,7 @@ declare const twoFactorClient: (options?: {
61
61
  OTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_NOT_CONFIGURED">;
62
62
  OTP_HAS_EXPIRED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_HAS_EXPIRED">;
63
63
  TOTP_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_ENABLED">;
64
+ TOTP_ALREADY_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_ALREADY_ENABLED">;
64
65
  TOTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_CONFIGURED">;
65
66
  TWO_FACTOR_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TWO_FACTOR_NOT_ENABLED">;
66
67
  BACKUP_CODES_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"BACKUP_CODES_NOT_ENABLED">;
@@ -6,6 +6,7 @@ declare const TWO_FACTOR_ERROR_CODES: {
6
6
  OTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_NOT_CONFIGURED">;
7
7
  OTP_HAS_EXPIRED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_HAS_EXPIRED">;
8
8
  TOTP_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_ENABLED">;
9
+ TOTP_ALREADY_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_ALREADY_ENABLED">;
9
10
  TOTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_CONFIGURED">;
10
11
  TWO_FACTOR_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TWO_FACTOR_NOT_ENABLED">;
11
12
  BACKUP_CODES_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"BACKUP_CODES_NOT_ENABLED">;
@@ -5,6 +5,7 @@ const TWO_FACTOR_ERROR_CODES = defineErrorCodes({
5
5
  OTP_NOT_CONFIGURED: "OTP is not available",
6
6
  OTP_HAS_EXPIRED: "OTP has expired",
7
7
  TOTP_NOT_ENABLED: "TOTP not enabled",
8
+ TOTP_ALREADY_ENABLED: "TOTP is already enabled",
8
9
  TOTP_NOT_CONFIGURED: "TOTP is not available",
9
10
  TWO_FACTOR_NOT_ENABLED: "Two factor isn't enabled",
10
11
  BACKUP_CODES_NOT_ENABLED: "Backup codes aren't enabled",
@@ -705,6 +705,7 @@ declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
705
705
  OTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_NOT_CONFIGURED">;
706
706
  OTP_HAS_EXPIRED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_HAS_EXPIRED">;
707
707
  TOTP_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_ENABLED">;
708
+ TOTP_ALREADY_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_ALREADY_ENABLED">;
708
709
  TOTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_CONFIGURED">;
709
710
  TWO_FACTOR_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TWO_FACTOR_NOT_ENABLED">;
710
711
  BACKUP_CODES_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"BACKUP_CODES_NOT_ENABLED">;
@@ -122,7 +122,6 @@ const twoFactor = (options) => {
122
122
  await ctx.context.internalAdapter.deleteSession(ctx.context.session.session.token);
123
123
  return ctx.json({ method: "otp" });
124
124
  }
125
- const backupCodes = await generateBackupCodes(ctx.context.secretConfig, backupCodeOptions);
126
125
  const existingTwoFactor = await ctx.context.adapter.findOne({
127
126
  model: opts.twoFactorTable,
128
127
  where: [{
@@ -130,6 +129,8 @@ const twoFactor = (options) => {
130
129
  value: user.id
131
130
  }]
132
131
  });
132
+ if (existingTwoFactor && existingTwoFactor.verified !== false) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_ALREADY_ENABLED);
133
+ const backupCodes = await generateBackupCodes(ctx.context.secretConfig, backupCodeOptions);
133
134
  const secret = generateRandomString(32);
134
135
  const encryptedSecret = await symmetricEncrypt({
135
136
  key: ctx.context.secretConfig,
@@ -146,7 +147,7 @@ const twoFactor = (options) => {
146
147
  const totpData = {
147
148
  secret: encryptedSecret,
148
149
  backupCodes: backupCodes.encryptedBackupCodes,
149
- verified: existingTwoFactor != null && existingTwoFactor.verified === true || !!options?.skipVerificationOnEnable
150
+ verified: !!options?.skipVerificationOnEnable
150
151
  };
151
152
  if (existingTwoFactor) await ctx.context.adapter.update({
152
153
  model: opts.twoFactorTable,
package/dist/state.d.mts CHANGED
@@ -9,26 +9,13 @@ declare const stateDataSchema: z.ZodObject<{
9
9
  errorURL: z.ZodOptional<z.ZodString>;
10
10
  newUserURL: z.ZodOptional<z.ZodString>;
11
11
  expiresAt: z.ZodNumber;
12
- /**
13
- * CSRF nonce returned to the OAuth provider. When using cookie state storage,
14
- * this must match the callback `state` query parameter.
15
- */
16
12
  oauthState: z.ZodOptional<z.ZodString>;
17
13
  link: z.ZodOptional<z.ZodObject<{
18
14
  email: z.ZodString;
19
15
  userId: z.ZodCoercedString<unknown>;
20
16
  }, z.core.$strip>>;
21
17
  requestSignUp: z.ZodOptional<z.ZodBoolean>;
22
- /**
23
- * OIDC nonce sent as the authorization request `nonce` parameter when the
24
- * provider requires an ID token to be bound to this redirect flow.
25
- */
26
18
  idTokenNonce: z.ZodOptional<z.ZodString>;
27
- /**
28
- * Server-controlled values that ride the state across the provider redirect.
29
- * Populated only by `generateState` from `addOAuthServerContext`, never from
30
- * the request body, so it is safe to trust on the callback.
31
- */
32
19
  serverContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
33
20
  }, z.core.$loose>;
34
21
  type StateData = z.infer<typeof stateDataSchema>;
@@ -90,7 +90,7 @@ async function getTestInstance(options, config) {
90
90
  advanced: { cookies: {} },
91
91
  logger: { level: "debug" }
92
92
  };
93
- const auth = betterAuth({
93
+ const authOptions = {
94
94
  baseURL: "http://localhost:" + (config?.port || 3e3),
95
95
  ...opts,
96
96
  ...options,
@@ -100,7 +100,15 @@ async function getTestInstance(options, config) {
100
100
  password: options?.emailAndPassword?.password ?? testPassword
101
101
  },
102
102
  plugins: [bearer(), ...options?.plugins || []]
103
- });
103
+ };
104
+ if (testWith !== "mongodb") {
105
+ const { runMigrations } = await getMigrations({
106
+ ...authOptions,
107
+ database: opts.database
108
+ });
109
+ await runMigrations();
110
+ }
111
+ const auth = betterAuth(authOptions);
104
112
  const testUser = {
105
113
  email: "test@test.com",
106
114
  password: "test123456",
@@ -117,13 +125,6 @@ async function getTestInstance(options, config) {
117
125
  headers
118
126
  });
119
127
  }
120
- if (testWith !== "mongodb") {
121
- const { runMigrations } = await getMigrations({
122
- ...auth.options,
123
- database: opts.database
124
- });
125
- await runMigrations();
126
- }
127
128
  await createTestUser();
128
129
  const cleanup = async () => {
129
130
  if (testWith === "mongodb") {