better-auth 1.7.7 → 1.7.9

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.
@@ -9,10 +9,12 @@ import { OktaOptions, okta } from "./providers/okta.mjs";
9
9
  import { PatreonOptions, patreon } from "./providers/patreon.mjs";
10
10
  import { SlackOptions, slack } from "./providers/slack.mjs";
11
11
  import { YandexOptions, yandex } from "./providers/yandex.mjs";
12
+ import * as _$_better_auth_core0 from "@better-auth/core";
12
13
  import { AuthContext } from "@better-auth/core";
13
14
  import * as _$_better_auth_core_oauth20 from "@better-auth/core/oauth2";
14
15
  import { OAuthProvider } from "@better-auth/core/oauth2";
15
16
  import * as _$_better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
17
+ import * as _$better_call0 from "better-call";
16
18
 
17
19
  //#region src/plugins/generic-oauth/index.d.ts
18
20
  declare module "@better-auth/core" {
@@ -27,6 +29,10 @@ declare module "@better-auth/core" {
27
29
  * Extracts common fields from GenericOAuthConfig for provider helpers.
28
30
  */
29
31
  type BaseOAuthProviderOptions = Pick<GenericOAuthConfig, "clientId" | "clientSecret" | "tokenEndpointAuth" | "scopes" | "redirectURI" | "endSessionEndpoint" | "postLogoutRedirectURI" | "disableProviderLogout" | "pkce" | "disableImplicitSignUp" | "disableSignUp" | "overrideUserInfo">;
32
+ type PendingDiscovery = Map<string, {
33
+ config: GenericOAuthConfig;
34
+ attempt?: Promise<void> | undefined;
35
+ }>;
30
36
  /**
31
37
  * A generic OAuth plugin that registers any OAuth/OIDC provider
32
38
  * as a first-class social provider.
@@ -40,8 +46,15 @@ declare const genericOAuth: <const ID extends string>(options: GenericOAuthOptio
40
46
  init: (ctx: AuthContext) => Promise<{
41
47
  context: {
42
48
  socialProviders: OAuthProvider<object, Partial<_$_better_auth_core_oauth20.ProviderOptions<object>>>[];
49
+ genericOAuthPendingDiscovery: PendingDiscovery;
43
50
  };
44
51
  }>;
52
+ hooks: {
53
+ before: {
54
+ matcher: (context: _$_better_auth_core0.HookEndpointContext) => boolean;
55
+ handler: _$better_call0.Middleware<_$better_call0.MiddlewareOptions, (inputContext: _$better_call0.MiddlewareInputContext<_$better_call0.MiddlewareOptions>) => Promise<void>>;
56
+ }[];
57
+ };
45
58
  options: GenericOAuthOptions<ID>;
46
59
  $ERROR_CODES: {
47
60
  INVALID_OAUTH_CONFIGURATION: _$_better_auth_core_utils_error_codes0.RawError<"INVALID_OAUTH_CONFIGURATION">;
@@ -12,6 +12,7 @@ import { slack } from "./providers/slack.mjs";
12
12
  import { yandex } from "./providers/yandex.mjs";
13
13
  import { APIError } from "@better-auth/core/error";
14
14
  import { applyDefaultAccessTokenExpiry, createAuthorizationURL, refreshAccessToken, validateAuthorizationCode, verifyProviderIdToken } from "@better-auth/core/oauth2";
15
+ import { createAuthMiddleware } from "@better-auth/core/api";
15
16
  import { createRemoteJWKSet, decodeJwt } from "jose";
16
17
  import { betterFetch } from "@better-fetch/fetch";
17
18
  //#region src/plugins/generic-oauth/index.ts
@@ -21,10 +22,20 @@ function isSecretlessTokenEndpointAuth(tokenEndpointAuth) {
21
22
  function isClientSecretTokenEndpointAuth(tokenEndpointAuth) {
22
23
  return tokenEndpointAuth?.method === "client_secret_basic" || tokenEndpointAuth?.method === "client_secret_post";
23
24
  }
24
- async function fetchDiscovery(url, headers) {
25
+ /** A request waits on a discovery retry, so bound it (the SSO plugin uses 10s too). */
26
+ const DISCOVERY_RETRY_TIMEOUT_MS = 1e4;
27
+ /**
28
+ * Providers skipped because startup discovery failed. Init stores them on each auth
29
+ * instance's context, so one plugin object can serve several instances. It is a context
30
+ * field rather than a WeakMap because requests see copies of the context, and a later
31
+ * plugin may replace `socialProviders`.
32
+ */
33
+ const getPendingDiscovery = (ctx) => ctx.genericOAuthPendingDiscovery;
34
+ async function fetchDiscovery(url, headers, timeout) {
25
35
  const result = await betterFetch(url, {
26
36
  method: "GET",
27
- headers
37
+ headers,
38
+ signal: timeout ? AbortSignal.timeout(timeout) : void 0
28
39
  });
29
40
  if (result.error || !result.data) return null;
30
41
  if (result.data.issuer) try {
@@ -74,203 +85,235 @@ const genericOAuth = (options) => {
74
85
  seenIds.add(id);
75
86
  }
76
87
  if (nonUniqueIds.size > 0) console.warn(`Duplicate provider IDs found: ${Array.from(nonUniqueIds).join(", ")}`);
77
- return {
78
- id: "generic-oauth",
79
- version: PACKAGE_VERSION,
80
- init: async (ctx) => {
81
- const genericProviders = [];
82
- for (const c of options.config) {
83
- let authorizationUrl = c.authorizationUrl;
84
- let tokenUrl = c.tokenUrl;
85
- let userInfoUrl = c.userInfoUrl;
86
- let endSessionEndpoint = c.endSessionEndpoint;
87
- let issuer;
88
- let isOidc = false;
89
- let idTokenConfig;
90
- if (c.discoveryUrl) {
91
- const discovered = await fetchDiscovery(c.discoveryUrl, c.discoveryHeaders).catch((err) => {
92
- ctx.logger.error(`Discovery fetch failed for "${c.providerId}": ${err}`);
93
- return null;
94
- });
95
- if (discovered) {
96
- authorizationUrl ??= discovered.authorization_endpoint;
97
- tokenUrl ??= discovered.token_endpoint;
98
- userInfoUrl ??= discovered.userinfo_endpoint;
99
- endSessionEndpoint ??= discovered.end_session_endpoint;
100
- issuer = discovered.issuer;
101
- const signingAlgs = discovered.id_token_signing_alg_values_supported;
102
- isOidc = Array.isArray(signingAlgs) && signingAlgs.length > 0;
103
- if (discovered.jwks_uri && discovered.issuer) {
104
- let jwksUrl;
105
- try {
106
- jwksUrl = new URL(discovered.jwks_uri, c.discoveryUrl);
107
- } catch {
108
- ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
109
- continue;
110
- }
111
- idTokenConfig = {
112
- jwks: createRemoteJWKSet(jwksUrl),
113
- issuer: discovered.issuer,
114
- audience: c.clientId,
115
- algorithms: isOidc ? signingAlgs : void 0
116
- };
88
+ const buildProviders = async (configs, ctx, discoveryTimeout) => {
89
+ const genericProviders = [];
90
+ for (const c of configs) {
91
+ let authorizationUrl = c.authorizationUrl;
92
+ let tokenUrl = c.tokenUrl;
93
+ let userInfoUrl = c.userInfoUrl;
94
+ let endSessionEndpoint = c.endSessionEndpoint;
95
+ let issuer;
96
+ let isOidc = false;
97
+ let idTokenConfig;
98
+ if (c.discoveryUrl) {
99
+ const discovered = await fetchDiscovery(c.discoveryUrl, c.discoveryHeaders, discoveryTimeout).catch((err) => {
100
+ ctx.logger.error(`Discovery fetch failed for "${c.providerId}": ${err}`);
101
+ return null;
102
+ });
103
+ if (discovered) {
104
+ authorizationUrl ??= discovered.authorization_endpoint;
105
+ tokenUrl ??= discovered.token_endpoint;
106
+ userInfoUrl ??= discovered.userinfo_endpoint;
107
+ endSessionEndpoint ??= discovered.end_session_endpoint;
108
+ issuer = discovered.issuer;
109
+ const signingAlgs = discovered.id_token_signing_alg_values_supported;
110
+ isOidc = Array.isArray(signingAlgs) && signingAlgs.length > 0;
111
+ if (discovered.jwks_uri && discovered.issuer) {
112
+ let jwksUrl;
113
+ try {
114
+ jwksUrl = new URL(discovered.jwks_uri, c.discoveryUrl);
115
+ } catch {
116
+ ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
117
+ continue;
117
118
  }
118
- }
119
- if (!authorizationUrl || !tokenUrl && !c.getToken) {
120
- ctx.logger.error(`Provider "${c.providerId}": discovery left no usable authorization endpoint or token exchange. Provider skipped.`);
121
- continue;
119
+ idTokenConfig = {
120
+ jwks: createRemoteJWKSet(jwksUrl),
121
+ issuer: discovered.issuer,
122
+ audience: c.clientId,
123
+ algorithms: isOidc ? signingAlgs : void 0
124
+ };
122
125
  }
123
126
  }
124
- if (c.requireIdTokenVerification && !idTokenConfig) {
125
- if (c.discoveryUrl) {
126
- ctx.logger.error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri. Provider skipped.`);
127
- continue;
128
- }
129
- throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
127
+ if (!authorizationUrl || !tokenUrl && !c.getToken) {
128
+ ctx.logger.error(`Provider "${c.providerId}": discovery left no usable authorization endpoint or token exchange. Provider skipped.`);
129
+ continue;
130
130
  }
131
- const tokenEndpointAuth = c.tokenEndpointAuth;
132
- if (c.clientSecret && isSecretlessTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" cannot be combined with clientSecret`);
133
- if (!c.clientSecret && isClientSecretTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" requires clientSecret`);
134
- if (!c.clientSecret && !tokenEndpointAuth && c.authentication === "basic") throw new Error(`Provider "${c.providerId}": authentication "basic" requires clientSecret`);
135
- const accountSubject = c.accountSubject;
136
- const provider = {
137
- id: c.providerId,
138
- name: c.name ?? c.providerId,
139
- issuer,
140
- accountSubject: ({ tokens, profile }) => {
141
- const genericProfile = profile;
142
- if (accountSubject) return accountSubject({
143
- tokens,
144
- profile: genericProfile
145
- });
146
- return isOidc ? genericProfile.sub ?? "" : genericProfile.id ?? "";
147
- },
148
- idToken: idTokenConfig,
149
- requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
150
- allowIdpInitiated: c.allowIdpInitiated,
151
- async createEndSessionURL(data) {
152
- if (c.disableProviderLogout) return null;
153
- if (!endSessionEndpoint) return null;
154
- let url;
155
- try {
156
- url = new URL(endSessionEndpoint);
157
- } catch {
131
+ }
132
+ if (c.requireIdTokenVerification && !idTokenConfig) {
133
+ if (c.discoveryUrl) {
134
+ ctx.logger.error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri. Provider skipped.`);
135
+ continue;
136
+ }
137
+ throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
138
+ }
139
+ const tokenEndpointAuth = c.tokenEndpointAuth;
140
+ if (c.clientSecret && isSecretlessTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" cannot be combined with clientSecret`);
141
+ if (!c.clientSecret && isClientSecretTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" requires clientSecret`);
142
+ if (!c.clientSecret && !tokenEndpointAuth && c.authentication === "basic") throw new Error(`Provider "${c.providerId}": authentication "basic" requires clientSecret`);
143
+ const accountSubject = c.accountSubject;
144
+ const provider = {
145
+ id: c.providerId,
146
+ name: c.name ?? c.providerId,
147
+ issuer,
148
+ accountSubject: ({ tokens, profile }) => {
149
+ const genericProfile = profile;
150
+ if (accountSubject) return accountSubject({
151
+ tokens,
152
+ profile: genericProfile
153
+ });
154
+ return isOidc ? genericProfile.sub ?? "" : genericProfile.id ?? "";
155
+ },
156
+ idToken: idTokenConfig,
157
+ requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
158
+ allowIdpInitiated: c.allowIdpInitiated,
159
+ async createEndSessionURL(data) {
160
+ if (c.disableProviderLogout) return null;
161
+ if (!endSessionEndpoint) return null;
162
+ let url;
163
+ try {
164
+ url = new URL(endSessionEndpoint);
165
+ } catch {
166
+ return null;
167
+ }
168
+ if (data.idToken) url.searchParams.set("id_token_hint", data.idToken);
169
+ const configuredRedirectURI = data.postLogoutRedirectURI ?? c.postLogoutRedirectURI;
170
+ const postLogoutRedirectURI = configuredRedirectURI ? new URL(configuredRedirectURI, ctx.baseURL).toString() : void 0;
171
+ if (postLogoutRedirectURI) {
172
+ url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectURI);
173
+ url.searchParams.set("client_id", c.clientId);
174
+ if (data.state) url.searchParams.set("state", data.state);
175
+ } else if (!data.idToken) url.searchParams.set("client_id", c.clientId);
176
+ return url;
177
+ },
178
+ createAuthorizationURL(data) {
179
+ if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
180
+ return createAuthorizationURL({
181
+ id: c.providerId,
182
+ options: {
183
+ clientId: c.clientId,
184
+ clientSecret: c.clientSecret,
185
+ redirectURI: c.redirectURI
186
+ },
187
+ authorizationEndpoint: authorizationUrl,
188
+ state: data.state,
189
+ codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
190
+ scopes: (() => {
191
+ const merged = [...data.scopes ?? [], ...c.scopes ?? []];
192
+ if (isOidc && !merged.includes("openid")) merged.unshift("openid");
193
+ return merged;
194
+ })(),
195
+ redirectURI: data.redirectURI,
196
+ prompt: c.prompt,
197
+ accessType: c.accessType,
198
+ responseType: c.responseType,
199
+ responseMode: c.responseMode,
200
+ nonce: data.idTokenNonce,
201
+ additionalParams: {
202
+ ...c.authorizationUrlParams ?? {},
203
+ ...data.additionalParams ?? {}
204
+ },
205
+ loginHint: data.loginHint
206
+ });
207
+ },
208
+ async validateAuthorizationCode(data) {
209
+ if (c.getToken) return applyDefaultAccessTokenExpiry(await c.getToken(data), c.accessTokenExpiresIn);
210
+ if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
211
+ return applyDefaultAccessTokenExpiry(await validateAuthorizationCode({
212
+ headers: c.authorizationHeaders,
213
+ code: data.code,
214
+ codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
215
+ redirectURI: data.redirectURI,
216
+ options: {
217
+ clientId: c.clientId,
218
+ clientSecret: c.clientSecret,
219
+ redirectURI: c.redirectURI
220
+ },
221
+ tokenEndpoint: tokenUrl,
222
+ authentication: c.authentication,
223
+ tokenEndpointAuth,
224
+ additionalParams: c.tokenUrlParams
225
+ }), c.accessTokenExpiresIn);
226
+ },
227
+ async getUserInfo(tokens) {
228
+ const { expectedIdTokenNonce, ...oauthTokens } = tokens;
229
+ if (oauthTokens.idToken && provider.idToken) {
230
+ if (!await verifyProviderIdToken(provider, oauthTokens.idToken, expectedIdTokenNonce)) {
231
+ ctx.logger.error(`Provider "${c.providerId}": id_token failed verification against the discovery JWKS or expected nonce`);
158
232
  return null;
159
233
  }
160
- if (data.idToken) url.searchParams.set("id_token_hint", data.idToken);
161
- const configuredRedirectURI = data.postLogoutRedirectURI ?? c.postLogoutRedirectURI;
162
- const postLogoutRedirectURI = configuredRedirectURI ? new URL(configuredRedirectURI, ctx.baseURL).toString() : void 0;
163
- if (postLogoutRedirectURI) {
164
- url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectURI);
165
- url.searchParams.set("client_id", c.clientId);
166
- if (data.state) url.searchParams.set("state", data.state);
167
- } else if (!data.idToken) url.searchParams.set("client_id", c.clientId);
168
- return url;
169
- },
170
- createAuthorizationURL(data) {
171
- if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
172
- return createAuthorizationURL({
173
- id: c.providerId,
174
- options: {
175
- clientId: c.clientId,
176
- clientSecret: c.clientSecret,
177
- redirectURI: c.redirectURI
178
- },
179
- authorizationEndpoint: authorizationUrl,
180
- state: data.state,
181
- codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
182
- scopes: (() => {
183
- const merged = [...data.scopes ?? [], ...c.scopes ?? []];
184
- if (isOidc && !merged.includes("openid")) merged.unshift("openid");
185
- return merged;
186
- })(),
187
- redirectURI: data.redirectURI,
188
- prompt: c.prompt,
189
- accessType: c.accessType,
190
- responseType: c.responseType,
191
- responseMode: c.responseMode,
192
- nonce: data.idTokenNonce,
193
- additionalParams: {
194
- ...c.authorizationUrlParams ?? {},
195
- ...data.additionalParams ?? {}
196
- },
197
- loginHint: data.loginHint
198
- });
199
- },
200
- async validateAuthorizationCode(data) {
201
- if (c.getToken) return applyDefaultAccessTokenExpiry(await c.getToken(data), c.accessTokenExpiresIn);
202
- if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
203
- return applyDefaultAccessTokenExpiry(await validateAuthorizationCode({
204
- headers: c.authorizationHeaders,
205
- code: data.code,
206
- codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
207
- redirectURI: data.redirectURI,
208
- options: {
209
- clientId: c.clientId,
210
- clientSecret: c.clientSecret,
211
- redirectURI: c.redirectURI
212
- },
213
- tokenEndpoint: tokenUrl,
214
- authentication: c.authentication,
215
- tokenEndpointAuth,
216
- additionalParams: c.tokenUrlParams
217
- }), c.accessTokenExpiresIn);
218
- },
219
- async getUserInfo(tokens) {
220
- const { expectedIdTokenNonce, ...oauthTokens } = tokens;
221
- if (oauthTokens.idToken && provider.idToken) {
222
- if (!await verifyProviderIdToken(provider, oauthTokens.idToken, expectedIdTokenNonce)) {
223
- ctx.logger.error(`Provider "${c.providerId}": id_token failed verification against the discovery JWKS or expected nonce`);
224
- return null;
225
- }
226
- }
227
- const raw = c.getUserInfo ? await c.getUserInfo(oauthTokens) : await fetchUserInfo(oauthTokens, userInfoUrl);
228
- if (!raw) return null;
229
- const mapped = c.mapProfileToUser ? await c.mapProfileToUser(raw) : {};
230
- const user = {
231
- email: raw.email,
232
- emailVerified: raw.emailVerified,
233
- image: raw.image,
234
- name: raw.name,
235
- ...mapped
236
- };
237
- return {
238
- user: {
239
- ...user,
240
- image: user.image ?? void 0
241
- },
242
- data: raw
243
- };
244
- },
245
- async refreshAccessToken(refreshToken, refreshCtx) {
246
- if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
247
- const resolvedRefreshParams = typeof c.refreshTokenParams === "function" ? await c.refreshTokenParams(refreshCtx) : c.refreshTokenParams;
248
- return applyDefaultAccessTokenExpiry(await refreshAccessToken({
249
- refreshToken,
250
- options: {
251
- clientId: c.clientId,
252
- clientSecret: c.clientSecret
253
- },
254
- authentication: c.authentication,
255
- tokenEndpointAuth,
256
- tokenEndpoint: tokenUrl,
257
- extraParams: resolvedRefreshParams
258
- }), c.accessTokenExpiresIn);
259
- },
260
- disableImplicitSignUp: c.disableImplicitSignUp,
261
- disableSignUp: c.disableSignUp,
262
- options: {
263
- disableSignUp: c.disableSignUp,
264
- overrideUserInfoOnSignIn: c.overrideUserInfo,
265
- requireEmailVerification: c.requireEmailVerification
266
234
  }
267
- };
268
- genericProviders.push(provider);
269
- }
235
+ const raw = c.getUserInfo ? await c.getUserInfo(oauthTokens) : await fetchUserInfo(oauthTokens, userInfoUrl);
236
+ if (!raw) return null;
237
+ const mapped = c.mapProfileToUser ? await c.mapProfileToUser(raw) : {};
238
+ const user = {
239
+ email: raw.email,
240
+ emailVerified: raw.emailVerified,
241
+ image: raw.image,
242
+ name: raw.name,
243
+ ...mapped
244
+ };
245
+ return {
246
+ user: {
247
+ ...user,
248
+ image: user.image ?? void 0
249
+ },
250
+ data: raw
251
+ };
252
+ },
253
+ async refreshAccessToken(refreshToken, refreshCtx) {
254
+ if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
255
+ const resolvedRefreshParams = typeof c.refreshTokenParams === "function" ? await c.refreshTokenParams(refreshCtx) : c.refreshTokenParams;
256
+ return applyDefaultAccessTokenExpiry(await refreshAccessToken({
257
+ refreshToken,
258
+ options: {
259
+ clientId: c.clientId,
260
+ clientSecret: c.clientSecret
261
+ },
262
+ authentication: c.authentication,
263
+ tokenEndpointAuth,
264
+ tokenEndpoint: tokenUrl,
265
+ extraParams: resolvedRefreshParams
266
+ }), c.accessTokenExpiresIn);
267
+ },
268
+ disableImplicitSignUp: c.disableImplicitSignUp,
269
+ disableSignUp: c.disableSignUp,
270
+ options: {
271
+ disableSignUp: c.disableSignUp,
272
+ overrideUserInfoOnSignIn: c.overrideUserInfo,
273
+ requireEmailVerification: c.requireEmailVerification
274
+ }
275
+ };
276
+ genericProviders.push(provider);
277
+ }
278
+ return genericProviders;
279
+ };
280
+ const retryDiscovery = (ctx, providerId) => {
281
+ const pendingDiscovery = getPendingDiscovery(ctx);
282
+ const pending = pendingDiscovery?.get(providerId);
283
+ if (!pending) return;
284
+ pending.attempt ??= buildProviders([pending.config], ctx, DISCOVERY_RETRY_TIMEOUT_MS).then(([provider]) => {
285
+ if (!provider) return;
286
+ pendingDiscovery?.delete(providerId);
287
+ ctx.socialProviders.unshift(provider);
288
+ }).finally(() => {
289
+ pending.attempt = void 0;
290
+ });
291
+ return pending.attempt;
292
+ };
293
+ const requestedProviderId = (context) => {
294
+ const id = context.body?.provider ?? context.body?.providerId ?? context.query?.provider ?? context.params?.id;
295
+ return typeof id === "string" ? id : void 0;
296
+ };
297
+ return {
298
+ id: "generic-oauth",
299
+ version: PACKAGE_VERSION,
300
+ init: async (ctx) => {
301
+ const genericProviders = await buildProviders(options.config, ctx);
302
+ const built = new Set(genericProviders.map((p) => p.id));
303
+ const genericOAuthPendingDiscovery = new Map(options.config.filter((c) => !built.has(c.providerId)).map((c) => [c.providerId, { config: c }]));
270
304
  const existingIds = new Set(ctx.socialProviders.map((p) => p.id));
271
305
  for (const gp of genericProviders) if (existingIds.has(gp.id)) ctx.logger.warn(`Generic OAuth provider "${gp.id}" shadows a built-in social provider with the same ID`);
272
- return { context: { socialProviders: genericProviders.concat(ctx.socialProviders) } };
306
+ return { context: {
307
+ socialProviders: genericProviders.concat(ctx.socialProviders),
308
+ genericOAuthPendingDiscovery
309
+ } };
273
310
  },
311
+ hooks: { before: [{
312
+ matcher: (context) => getPendingDiscovery(context.context)?.has(requestedProviderId(context) ?? "") === true,
313
+ handler: createAuthMiddleware(async (ctx) => {
314
+ await retryDiscovery(ctx.context, requestedProviderId(ctx) ?? "");
315
+ })
316
+ }] },
274
317
  options,
275
318
  $ERROR_CODES: GENERIC_OAUTH_ERROR_CODES
276
319
  };
@@ -27,9 +27,9 @@ const lastLoginMethod = (userConfig) => {
27
27
  return config.customResolveMethod?.(resolveContext) ?? defaultResolveMethod(resolveContext);
28
28
  };
29
29
  const config = {
30
- cookieName: "better-auth.last_used_login_method",
31
- maxAge: 3600 * 24 * 30,
32
- ...userConfig
30
+ ...userConfig,
31
+ cookieName: userConfig?.cookieName ?? "better-auth.last_used_login_method",
32
+ maxAge: userConfig?.maxAge ?? 3600 * 24 * 30
33
33
  };
34
34
  return {
35
35
  id: "last-login-method",
@@ -132,15 +132,9 @@ const magicLink = (options) => {
132
132
  method: "GET",
133
133
  query: magicLinkVerifyQuerySchema,
134
134
  use: [
135
- originCheck((ctx) => {
136
- return ctx.query.callbackURL ? decodeURIComponent(ctx.query.callbackURL) : "/";
137
- }),
138
- originCheck((ctx) => {
139
- return ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : "/";
140
- }),
141
- originCheck((ctx) => {
142
- return ctx.query.errorCallbackURL ? decodeURIComponent(ctx.query.errorCallbackURL) : "/";
143
- })
135
+ originCheck((ctx) => ctx.query.callbackURL || "/"),
136
+ originCheck((ctx) => ctx.query.newUserCallbackURL || "/"),
137
+ originCheck((ctx) => ctx.query.errorCallbackURL || "/")
144
138
  ],
145
139
  requireHeaders: true,
146
140
  metadata: { openapi: {
@@ -159,14 +153,14 @@ const magicLink = (options) => {
159
153
  } }
160
154
  }, async (ctx) => {
161
155
  const token = ctx.query.token;
162
- const callbackURL = new URL(ctx.query.callbackURL ? decodeURIComponent(ctx.query.callbackURL) : "/", ctx.context.baseURL).toString();
163
- const errorCallbackURL = new URL(ctx.query.errorCallbackURL ? decodeURIComponent(ctx.query.errorCallbackURL) : callbackURL, ctx.context.baseURL);
156
+ const callbackURL = new URL(ctx.query.callbackURL || "/", ctx.context.baseURL).toString();
157
+ const errorCallbackURL = new URL(ctx.query.errorCallbackURL || callbackURL, ctx.context.baseURL);
164
158
  function redirectWithError(error, description) {
165
159
  errorCallbackURL.searchParams.set("error", error);
166
160
  if (description) errorCallbackURL.searchParams.set("error_description", description);
167
161
  throw ctx.redirect(errorCallbackURL.toString());
168
162
  }
169
- const newUserCallbackURL = new URL(ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : callbackURL, ctx.context.baseURL).toString();
163
+ const newUserCallbackURL = new URL(ctx.query.newUserCallbackURL || callbackURL, ctx.context.baseURL).toString();
170
164
  const identifier = magicLinkIdentifier(await storeToken(ctx, token));
171
165
  const pendingValue = await ctx.context.internalAdapter.findVerificationValue(identifier);
172
166
  if (!pendingValue || !parseMagicLinkRecord(pendingValue.value)?.success) redirectWithError("INVALID_TOKEN");
@@ -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";
@@ -1009,48 +1009,7 @@ declare const getActiveMemberRole: <O extends OrganizationOptions>(options: O) =
1009
1009
  };
1010
1010
  }>>)[];
1011
1011
  }, {
1012
- role: (((O["teams"] extends {
1013
- enabled: true;
1014
- } ? {
1015
- id: string;
1016
- organizationId: string;
1017
- role: InferOrganizationRolesFromOption<O>;
1018
- createdAt: Date;
1019
- userId: string;
1020
- teamId?: string | undefined;
1021
- user: {
1022
- id: string;
1023
- email: string;
1024
- name: string;
1025
- image?: string | undefined;
1026
- };
1027
- } : {
1028
- id: string;
1029
- organizationId: string;
1030
- role: InferOrganizationRolesFromOption<O>;
1031
- createdAt: Date;
1032
- userId: string;
1033
- user: {
1034
- id: string;
1035
- email: string;
1036
- name: string;
1037
- image?: string | undefined;
1038
- };
1039
- }) & (O["schema"] extends {
1040
- member?: {
1041
- additionalFields: infer Field extends Record<string, _$_better_auth_core_db0.DBFieldAttribute>;
1042
- } | undefined;
1043
- } ? FieldAttributeToObject<Field> : {}) extends infer T ? { [K in keyof T]: T[K] } : never) & {
1044
- user: {
1045
- id: string;
1046
- createdAt: Date;
1047
- updatedAt: Date;
1048
- email: string;
1049
- emailVerified: boolean;
1050
- name: string;
1051
- image?: string | null | undefined;
1052
- };
1053
- })["role"];
1012
+ role: InferOrganizationRolesFromOption<O>;
1054
1013
  }>;
1055
1014
  //#endregion
1056
1015
  export { addMember, getActiveMember, getActiveMemberRole, leaveOrganization, listMembers, removeMember, updateMemberRole };
@@ -512,7 +512,7 @@ const getActiveMemberRole = (options) => createAuthEndpoint("/organization/get-a
512
512
  organizationId
513
513
  });
514
514
  if (!member) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
515
- return ctx.json({ role: member?.role });
515
+ return ctx.json({ role: member.role });
516
516
  });
517
517
  //#endregion
518
518
  export { addMember, getActiveMember, getActiveMemberRole, leaveOrganization, listMembers, removeMember, updateMemberRole };