better-auth 1.7.0-rc.2 → 1.7.0-rc.3

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 (28) hide show
  1. package/dist/api/index.d.mts +30 -0
  2. package/dist/api/routes/sign-out.d.mts +16 -0
  3. package/dist/api/routes/sign-out.mjs +68 -2
  4. package/dist/client/path-to-object.d.mts +3 -2
  5. package/dist/package.mjs +1 -1
  6. package/dist/plugins/device-authorization/index.d.mts +41 -2
  7. package/dist/plugins/device-authorization/index.mjs +3 -3
  8. package/dist/plugins/device-authorization/routes.mjs +81 -10
  9. package/dist/plugins/device-authorization/schema.mjs +47 -39
  10. package/dist/plugins/generic-oauth/index.d.mts +1 -1
  11. package/dist/plugins/generic-oauth/index.mjs +22 -0
  12. package/dist/plugins/generic-oauth/providers/auth0.mjs +3 -0
  13. package/dist/plugins/generic-oauth/providers/gumroad.mjs +3 -0
  14. package/dist/plugins/generic-oauth/providers/hubspot.mjs +3 -0
  15. package/dist/plugins/generic-oauth/providers/keycloak.mjs +3 -0
  16. package/dist/plugins/generic-oauth/providers/line.mjs +3 -0
  17. package/dist/plugins/generic-oauth/providers/microsoft-entra-id.mjs +35 -9
  18. package/dist/plugins/generic-oauth/providers/okta.mjs +3 -0
  19. package/dist/plugins/generic-oauth/providers/patreon.mjs +3 -0
  20. package/dist/plugins/generic-oauth/providers/slack.mjs +3 -0
  21. package/dist/plugins/generic-oauth/providers/yandex.mjs +3 -0
  22. package/dist/plugins/generic-oauth/types.d.mts +27 -0
  23. package/dist/plugins/jwt/adapter.mjs +7 -6
  24. package/dist/plugins/one-tap/client.d.mts +3 -2
  25. package/dist/test-utils/http-test-instance.mjs +2 -1
  26. package/dist/test-utils/test-instance.d.mts +1 -0
  27. package/dist/test-utils/test-instance.mjs +4 -2
  28. package/package.json +8 -8
@@ -290,6 +290,11 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
290
290
  } | null>;
291
291
  readonly signOut: import("better-call").StrictEndpoint<"/sign-out", {
292
292
  method: "POST";
293
+ body: import("zod").ZodOptional<import("zod").ZodObject<{
294
+ callbackURL: import("zod").ZodOptional<import("zod").ZodString>;
295
+ disableRedirect: import("zod").ZodOptional<import("zod").ZodBoolean>;
296
+ state: import("zod").ZodOptional<import("zod").ZodString>;
297
+ }, import("zod/v4/core").$strip>>;
293
298
  operationId: string;
294
299
  requireHeaders: true;
295
300
  metadata: {
@@ -307,6 +312,14 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
307
312
  success: {
308
313
  type: string;
309
314
  };
315
+ url: {
316
+ type: string;
317
+ description: string;
318
+ };
319
+ redirect: {
320
+ type: string;
321
+ description: string;
322
+ };
310
323
  };
311
324
  };
312
325
  };
@@ -317,6 +330,8 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
317
330
  };
318
331
  }, {
319
332
  success: boolean;
333
+ url: string | undefined;
334
+ redirect: boolean | undefined;
320
335
  }>;
321
336
  readonly signUpEmail: import("better-call").StrictEndpoint<"/sign-up/email", {
322
337
  method: "POST";
@@ -2297,6 +2312,11 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2297
2312
  } | null>;
2298
2313
  readonly signOut: import("better-call").StrictEndpoint<"/sign-out", {
2299
2314
  method: "POST";
2315
+ body: import("zod").ZodOptional<import("zod").ZodObject<{
2316
+ callbackURL: import("zod").ZodOptional<import("zod").ZodString>;
2317
+ disableRedirect: import("zod").ZodOptional<import("zod").ZodBoolean>;
2318
+ state: import("zod").ZodOptional<import("zod").ZodString>;
2319
+ }, import("zod/v4/core").$strip>>;
2300
2320
  operationId: string;
2301
2321
  requireHeaders: true;
2302
2322
  metadata: {
@@ -2314,6 +2334,14 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2314
2334
  success: {
2315
2335
  type: string;
2316
2336
  };
2337
+ url: {
2338
+ type: string;
2339
+ description: string;
2340
+ };
2341
+ redirect: {
2342
+ type: string;
2343
+ description: string;
2344
+ };
2317
2345
  };
2318
2346
  };
2319
2347
  };
@@ -2324,6 +2352,8 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2324
2352
  };
2325
2353
  }, {
2326
2354
  success: boolean;
2355
+ url: string | undefined;
2356
+ redirect: boolean | undefined;
2327
2357
  }>;
2328
2358
  readonly signUpEmail: import("better-call").StrictEndpoint<"/sign-up/email", {
2329
2359
  method: "POST";
@@ -1,6 +1,12 @@
1
+ import * as z from "zod";
1
2
  //#region src/api/routes/sign-out.d.ts
2
3
  declare const signOut: import("better-call").StrictEndpoint<"/sign-out", {
3
4
  method: "POST";
5
+ body: z.ZodOptional<z.ZodObject<{
6
+ callbackURL: z.ZodOptional<z.ZodString>;
7
+ disableRedirect: z.ZodOptional<z.ZodBoolean>;
8
+ state: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>>;
4
10
  operationId: string;
5
11
  requireHeaders: true;
6
12
  metadata: {
@@ -18,6 +24,14 @@ declare const signOut: import("better-call").StrictEndpoint<"/sign-out", {
18
24
  success: {
19
25
  type: string;
20
26
  };
27
+ url: {
28
+ type: string;
29
+ description: string;
30
+ };
31
+ redirect: {
32
+ type: string;
33
+ description: string;
34
+ };
21
35
  };
22
36
  };
23
37
  };
@@ -28,6 +42,8 @@ declare const signOut: import("better-call").StrictEndpoint<"/sign-out", {
28
42
  };
29
43
  }, {
30
44
  success: boolean;
45
+ url: string | undefined;
46
+ redirect: boolean | undefined;
31
47
  }>;
32
48
  //#endregion
33
49
  export { signOut };
@@ -1,8 +1,20 @@
1
1
  import { deleteSessionCookie } from "../../cookies/index.mjs";
2
2
  import { createAuthEndpoint } from "@better-auth/core/api";
3
+ import * as z from "zod";
3
4
  //#region src/api/routes/sign-out.ts
5
+ const signOutBodySchema = z.object({
6
+ callbackURL: z.string().meta({ description: "The URL to redirect to after provider logout" }).optional(),
7
+ disableRedirect: z.boolean().meta({ description: "Return the provider logout URL without redirecting" }).optional(),
8
+ state: z.string().meta({ description: "State to pass to the provider logout endpoint" }).optional()
9
+ }).optional();
10
+ const signOutResponse = (providerLogout) => ({
11
+ success: true,
12
+ url: providerLogout?.url,
13
+ redirect: providerLogout?.redirect
14
+ });
4
15
  const signOut = createAuthEndpoint("/sign-out", {
5
16
  method: "POST",
17
+ body: signOutBodySchema,
6
18
  operationId: "signOut",
7
19
  requireHeaders: true,
8
20
  metadata: { openapi: {
@@ -12,19 +24,73 @@ const signOut = createAuthEndpoint("/sign-out", {
12
24
  description: "Success",
13
25
  content: { "application/json": { schema: {
14
26
  type: "object",
15
- properties: { success: { type: "boolean" } }
27
+ properties: {
28
+ success: { type: "boolean" },
29
+ url: {
30
+ type: "string",
31
+ description: "Provider logout URL when RP-initiated logout is available"
32
+ },
33
+ redirect: {
34
+ type: "boolean",
35
+ description: "Whether the client should redirect to the provider logout URL"
36
+ }
37
+ }
16
38
  } } }
17
39
  } }
18
40
  } }
19
41
  }, async (ctx) => {
20
42
  const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret);
43
+ let currentSession = null;
44
+ if (sessionCookieToken) try {
45
+ currentSession = await ctx.context.internalAdapter.findSession(sessionCookieToken);
46
+ } catch (e) {
47
+ ctx.context.logger.error("Failed to read session from database", e);
48
+ }
21
49
  if (sessionCookieToken) try {
22
50
  await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
23
51
  } catch (e) {
24
52
  ctx.context.logger.error("Failed to delete session from database", e);
25
53
  }
26
54
  deleteSessionCookie(ctx);
27
- return ctx.json({ success: true });
55
+ const providerLogoutResult = await (async () => {
56
+ try {
57
+ if (!currentSession) return null;
58
+ const accounts = await ctx.context.internalAdapter.findAccounts(currentSession.user.id);
59
+ const providersById = new Map(ctx.context.socialProviders.map((provider) => [provider.id, provider]));
60
+ const logoutAccounts = accounts.filter((account) => Boolean(providersById.get(account.providerId)?.createEndSessionURL)).sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
61
+ if (logoutAccounts.length === 0) return null;
62
+ const seenProviderIds = /* @__PURE__ */ new Set();
63
+ const postLogoutRedirectURI = ctx.body?.callbackURL ? new URL(ctx.body.callbackURL, ctx.context.baseURL).toString() : void 0;
64
+ for (const account of logoutAccounts) {
65
+ if (seenProviderIds.has(account.providerId)) continue;
66
+ seenProviderIds.add(account.providerId);
67
+ const provider = providersById.get(account.providerId);
68
+ try {
69
+ const url = await provider?.createEndSessionURL?.({
70
+ idToken: account.idToken,
71
+ postLogoutRedirectURI,
72
+ state: ctx.body?.state
73
+ });
74
+ if (url) return url.toString();
75
+ } catch (e) {
76
+ ctx.context.logger.error(`Failed to create logout URL for provider "${account.providerId}"`, e);
77
+ }
78
+ }
79
+ return null;
80
+ } catch (e) {
81
+ ctx.context.logger.error("Failed to create provider logout URL", e);
82
+ return null;
83
+ }
84
+ })();
85
+ if (providerLogoutResult) {
86
+ const shouldRedirect = !ctx.body?.disableRedirect;
87
+ if (shouldRedirect) ctx.setHeader("Location", providerLogoutResult);
88
+ return ctx.json(signOutResponse({
89
+ url: providerLogoutResult,
90
+ redirect: shouldRedirect
91
+ }));
92
+ }
93
+ return ctx.json(signOutResponse());
28
94
  });
29
95
  //#endregion
30
96
  export { signOut };
@@ -77,7 +77,8 @@ type InferBodyCtx<Body, FetchOptions extends ClientFetchOption> = Body extends R
77
77
  } : never;
78
78
  type PrettifyUnion<T> = T extends unknown ? Prettify$1<T> : never;
79
79
  type HasRequiredKeysInUnion<T> = true extends (T extends unknown ? HasRequiredKeys<T> : never) ? true : false;
80
- type InferCtx<C extends InputContext<any, any>, FetchOptions extends ClientFetchOption> = IsAny<C["body"]> extends true ? InferCtxQuery<C, FetchOptions> : C["body"] extends Record<string, any> ? InferBodyCtx<C["body"], FetchOptions> : InferCtxQuery<C, FetchOptions>;
80
+ type HasRequiredCtx<C extends InputContext<any, any>, FetchOptions extends ClientFetchOption> = IsAny<C["body"]> extends true ? HasRequiredKeysInUnion<InferCtx<C, FetchOptions>> : undefined extends C["body"] ? HasRequiredKeysInUnion<InferCtxQuery<C, FetchOptions>> : HasRequiredKeysInUnion<InferCtx<C, FetchOptions>>;
81
+ type InferCtx<C extends InputContext<any, any>, FetchOptions extends ClientFetchOption> = IsAny<C["body"]> extends true ? InferCtxQuery<C, FetchOptions> : [NonNullable<C["body"]>] extends [never] ? InferCtxQuery<C, FetchOptions> : NonNullable<C["body"]> extends Record<string, any> ? InferBodyCtx<NonNullable<C["body"]>, FetchOptions> : InferCtxQuery<C, FetchOptions>;
81
82
  type MergeRoutes<T> = UnionToIntersection<T>;
82
83
  type InferRoute<API, COpts extends BetterAuthClientOptions> = API extends Record<string, infer T> ? T extends Endpoint ? T["options"]["metadata"] extends {
83
84
  isAction: false;
@@ -87,7 +88,7 @@ type InferRoute<API, COpts extends BetterAuthClientOptions> = API extends Record
87
88
  scope: "http";
88
89
  } | {
89
90
  scope: "server";
90
- } ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data: HasRequiredKeysInUnion<InferCtx<C, FetchOptions>> extends true ? [PrettifyUnion<T["path"] extends `/sign-up/email` ? InferSignUpEmailCtx<COpts, FetchOptions> : T["path"] extends `/sign-in/social` ? Omit<InferCtx<C, FetchOptions>, "provider"> & {
91
+ } ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data: HasRequiredCtx<C, FetchOptions> extends true ? [PrettifyUnion<T["path"] extends `/sign-up/email` ? InferSignUpEmailCtx<COpts, FetchOptions> : T["path"] extends `/sign-in/social` ? Omit<InferCtx<C, FetchOptions>, "provider"> & {
91
92
  provider: SocialProviderList[number] | InferGenericOAuthProviderIds<COpts> | (string & {});
92
93
  fetchOptions?: FetchOptions | undefined;
93
94
  } : InferCtx<C, FetchOptions>>, FetchOptions?] : [PrettifyUnion<T["path"] extends `/update-user` ? InferUserUpdateCtx<COpts, FetchOptions> : T["path"] extends `/update-session` ? InferSessionUpdateCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>?, FetchOptions?]) => Promise<BetterFetchResponse<T["options"]["metadata"] extends {
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.7.0-rc.2";
2
+ var version = "1.7.0-rc.3";
3
3
  //#endregion
4
4
  export { version };
@@ -16,7 +16,7 @@ declare const deviceAuthorizationOptionsSchema: z.ZodObject<{
16
16
  generateDeviceCode: z.ZodOptional<z.ZodCustom<() => string | Promise<string>, () => string | Promise<string>>>;
17
17
  generateUserCode: z.ZodOptional<z.ZodCustom<() => string | Promise<string>, () => string | Promise<string>>>;
18
18
  validateClient: z.ZodOptional<z.ZodCustom<(clientId: string) => boolean | Promise<boolean>, (clientId: string) => boolean | Promise<boolean>>>;
19
- onDeviceAuthRequest: z.ZodOptional<z.ZodCustom<(clientId: string, scope: string | undefined) => void | Promise<void>, (clientId: string, scope: string | undefined) => void | Promise<void>>>;
19
+ onDeviceAuthRequest: z.ZodOptional<z.ZodCustom<(clientId: string, scope: string | undefined, resource?: string | string[]) => void | Promise<void>, (clientId: string, scope: string | undefined, resource?: string | string[]) => void | Promise<void>>>;
20
20
  verificationUri: z.ZodOptional<z.ZodString>;
21
21
  schema: z.ZodOptional<z.ZodCustom<{
22
22
  deviceCode?: {
@@ -31,6 +31,7 @@ declare const deviceAuthorizationOptionsSchema: z.ZodObject<{
31
31
  pollingInterval?: string | undefined;
32
32
  clientId?: string | undefined;
33
33
  scope?: string | undefined;
34
+ resource?: string | undefined;
34
35
  } | undefined;
35
36
  } | undefined;
36
37
  }, {
@@ -46,6 +47,7 @@ declare const deviceAuthorizationOptionsSchema: z.ZodObject<{
46
47
  pollingInterval?: string | undefined;
47
48
  clientId?: string | undefined;
48
49
  scope?: string | undefined;
50
+ resource?: string | undefined;
49
51
  } | undefined;
50
52
  } | undefined;
51
53
  }>>;
@@ -93,26 +95,39 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
93
95
  type: "string";
94
96
  required: false;
95
97
  };
98
+ resource: {
99
+ type: "string";
100
+ required: false;
101
+ };
96
102
  };
103
+ indexes: {
104
+ fields: [string];
105
+ }[];
97
106
  };
98
107
  };
99
108
  endpoints: {
100
109
  deviceCode: import("better-call").StrictEndpoint<"/device/code", {
101
110
  method: "POST";
111
+ cloneRequest: true;
102
112
  body: z.ZodObject<{
103
113
  client_id: z.ZodString;
104
114
  user_id: z.ZodOptional<z.ZodString>;
105
115
  scope: z.ZodOptional<z.ZodString>;
116
+ resource: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
106
117
  }, z.core.$strip>;
107
118
  error: z.ZodObject<{
108
119
  error: z.ZodEnum<{
109
120
  invalid_request: "invalid_request";
110
121
  invalid_client: "invalid_client";
122
+ unauthorized_client: "unauthorized_client";
123
+ invalid_scope: "invalid_scope";
124
+ invalid_target: "invalid_target";
111
125
  }>;
112
126
  error_description: z.ZodString;
113
127
  }, z.core.$strip>;
114
128
  metadata: {
115
129
  noStore: boolean;
130
+ allowedMediaTypes: string[];
116
131
  openapi: {
117
132
  description: string;
118
133
  responses: {
@@ -284,6 +299,26 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
284
299
  enum: string[];
285
300
  description: string;
286
301
  };
302
+ client_id: {
303
+ type: string;
304
+ description: string;
305
+ };
306
+ scope: {
307
+ type: string;
308
+ description: string;
309
+ };
310
+ resource: {
311
+ oneOf: ({
312
+ type: string;
313
+ items?: undefined;
314
+ } | {
315
+ type: string;
316
+ items: {
317
+ type: string;
318
+ };
319
+ })[];
320
+ description: string;
321
+ };
287
322
  };
288
323
  };
289
324
  };
@@ -293,6 +328,9 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
293
328
  };
294
329
  };
295
330
  }, {
331
+ client_id?: string | undefined;
332
+ scope?: string | undefined;
333
+ resource?: string | string[] | undefined;
296
334
  user_code: string;
297
335
  status: string;
298
336
  }>;
@@ -401,7 +439,7 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
401
439
  generateDeviceCode?: (() => string | Promise<string>) | undefined;
402
440
  generateUserCode?: (() => string | Promise<string>) | undefined;
403
441
  validateClient?: ((clientId: string) => boolean | Promise<boolean>) | undefined;
404
- onDeviceAuthRequest?: ((clientId: string, scope: string | undefined) => void | Promise<void>) | undefined;
442
+ onDeviceAuthRequest?: ((clientId: string, scope: string | undefined, resource?: string | string[]) => void | Promise<void>) | undefined;
405
443
  verificationUri?: string | undefined;
406
444
  schema?: {
407
445
  deviceCode?: {
@@ -416,6 +454,7 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
416
454
  pollingInterval?: string | undefined;
417
455
  clientId?: string | undefined;
418
456
  scope?: string | undefined;
457
+ resource?: string | undefined;
419
458
  } | undefined;
420
459
  } | undefined;
421
460
  } | undefined;
@@ -2,8 +2,8 @@ import { mergeSchema } from "../../db/schema.mjs";
2
2
  import { ms } from "../../utils/time.mjs";
3
3
  import { PACKAGE_VERSION } from "../../version.mjs";
4
4
  import { DEVICE_AUTHORIZATION_ERROR_CODES } from "./error-codes.mjs";
5
- import { deviceApprove, deviceCode, deviceDeny, deviceToken, deviceVerify } from "./routes.mjs";
6
5
  import { schema } from "./schema.mjs";
6
+ import { deviceApprove, deviceCode, deviceDeny, deviceToken, deviceVerify } from "./routes.mjs";
7
7
  import * as z from "zod";
8
8
  //#region src/plugins/device-authorization/index.ts
9
9
  const timeStringSchema = z.custom((val) => {
@@ -18,8 +18,8 @@ const timeStringSchema = z.custom((val) => {
18
18
  const deviceAuthorizationOptionsSchema = z.object({
19
19
  expiresIn: timeStringSchema.default("30m").describe("Time in seconds until the device code expires. Use formats like '30m', '5s', '1h', etc."),
20
20
  interval: timeStringSchema.default("5s").describe("Time in seconds between polling attempts. Use formats like '30m', '5s', '1h', etc."),
21
- deviceCodeLength: z.number().int().positive().default(40).describe("Length of the device code to be generated. Default is 40 characters."),
22
- userCodeLength: z.number().int().positive().default(8).describe("Length of the user code to be generated. Default is 8 characters."),
21
+ deviceCodeLength: z.number().int().positive().max(191).default(40).describe(`Length of the device code to be generated. Must be at most 191 characters. Default is 40 characters.`),
22
+ userCodeLength: z.number().int().positive().max(191).default(8).describe(`Length of the user code to be generated. Must be at most 191 characters. Default is 8 characters.`),
23
23
  generateDeviceCode: z.custom((val) => typeof val === "function", { message: "generateDeviceCode must be a function that returns a string or a promise that resolves to a string." }).optional().describe("Function to generate a device code. If not provided, a default random string generator will be used."),
24
24
  generateUserCode: z.custom((val) => typeof val === "function", { message: "generateUserCode must be a function that returns a string or a promise that resolves to a string." }).optional().describe("Function to generate a user code. If not provided, a default random string generator will be used."),
25
25
  validateClient: z.custom((val) => typeof val === "function", { message: "validateClient must be a function that returns a boolean or a promise that resolves to a boolean." }).optional().describe("Function to validate the client ID. If not provided, no validation will be performed."),
@@ -7,30 +7,71 @@ import { createAuthEndpoint } from "@better-auth/core/api";
7
7
  import * as z from "zod";
8
8
  //#region src/plugins/device-authorization/routes.ts
9
9
  const defaultCharset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
10
+ function validateGeneratedCode(code, label) {
11
+ if (typeof code !== "string") throw new APIError("BAD_REQUEST", {
12
+ error: "invalid_request",
13
+ error_description: `Generated ${label} code must be a string`
14
+ });
15
+ if (Array.from(code).length > 191) throw new APIError("BAD_REQUEST", {
16
+ error: "invalid_request",
17
+ error_description: `Generated ${label} code must be at most 191 characters`
18
+ });
19
+ return code;
20
+ }
21
+ function serializeResource(resource) {
22
+ return typeof resource === "string" ? resource : JSON.stringify(resource);
23
+ }
24
+ function parseStoredResource(resource) {
25
+ if (!resource) return void 0;
26
+ if (!resource.startsWith("[")) return resource;
27
+ try {
28
+ const parsed = JSON.parse(resource);
29
+ if (Array.isArray(parsed) && parsed.every((value) => typeof value === "string")) return parsed;
30
+ } catch {}
31
+ return resource;
32
+ }
33
+ async function extractFormResources(request) {
34
+ const contentType = request?.headers.get("content-type")?.toLowerCase() ?? "";
35
+ if (!request || !contentType.includes("application/x-www-form-urlencoded")) return;
36
+ try {
37
+ const params = new URLSearchParams(await request.text());
38
+ if (!params.has("resource")) return void 0;
39
+ return params.getAll("resource").filter(Boolean);
40
+ } catch {
41
+ return;
42
+ }
43
+ }
10
44
  const deviceCodeBodySchema = z.object({
11
45
  client_id: z.string().meta({ description: "The client ID of the application" }),
12
46
  user_id: z.string().meta({ description: "The user ID to which the device code should be pre-bound." }).optional(),
13
- scope: z.string().meta({ description: "Space-separated list of scopes" }).optional()
47
+ scope: z.string().meta({ description: "Space-separated list of scopes" }).optional(),
48
+ resource: z.union([z.string(), z.array(z.string())]).meta({ description: "RFC 8707 resource indicator(s) to bind to this authorization request" }).optional()
14
49
  });
15
50
  const deviceCodeErrorSchema = z.object({
16
- error: z.enum(["invalid_request", "invalid_client"]).meta({ description: "Error code" }),
51
+ error: z.enum([
52
+ "invalid_request",
53
+ "invalid_client",
54
+ "unauthorized_client",
55
+ "invalid_scope",
56
+ "invalid_target"
57
+ ]).meta({ description: "Error code" }),
17
58
  error_description: z.string().meta({ description: "Detailed error description" })
18
59
  });
19
60
  const deviceCode = (opts) => {
20
61
  const generateDeviceCode = async () => {
21
- if (opts.generateDeviceCode) return opts.generateDeviceCode();
22
- return defaultGenerateDeviceCode(opts.deviceCodeLength);
62
+ return validateGeneratedCode(opts.generateDeviceCode ? await opts.generateDeviceCode() : defaultGenerateDeviceCode(opts.deviceCodeLength), "device");
23
63
  };
24
64
  const generateUserCode = async () => {
25
- if (opts.generateUserCode) return opts.generateUserCode();
26
- return defaultGenerateUserCode(opts.userCodeLength);
65
+ return validateGeneratedCode(opts.generateUserCode ? await opts.generateUserCode() : defaultGenerateUserCode(opts.userCodeLength), "user");
27
66
  };
28
67
  return createAuthEndpoint("/device/code", {
29
68
  method: "POST",
69
+ cloneRequest: true,
30
70
  body: deviceCodeBodySchema,
31
71
  error: deviceCodeErrorSchema,
32
72
  metadata: {
33
73
  noStore: true,
74
+ allowedMediaTypes: ["application/json", "application/x-www-form-urlencoded"],
34
75
  openapi: {
35
76
  description: `Request a device and user code
36
77
 
@@ -77,7 +118,13 @@ Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#secti
77
118
  properties: {
78
119
  error: {
79
120
  type: "string",
80
- enum: ["invalid_request", "invalid_client"]
121
+ enum: [
122
+ "invalid_request",
123
+ "invalid_client",
124
+ "unauthorized_client",
125
+ "invalid_scope",
126
+ "invalid_target"
127
+ ]
81
128
  },
82
129
  error_description: { type: "string" }
83
130
  }
@@ -87,13 +134,15 @@ Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#secti
87
134
  }
88
135
  }
89
136
  }, async (ctx) => {
137
+ const formResources = await extractFormResources(ctx.request);
138
+ if (formResources) ctx.body.resource = formResources.length === 1 ? formResources[0] : formResources;
90
139
  if (opts.validateClient) {
91
140
  if (!await opts.validateClient(ctx.body.client_id)) throw new APIError("BAD_REQUEST", {
92
141
  error: "invalid_client",
93
142
  error_description: "Invalid client ID"
94
143
  });
95
144
  }
96
- if (opts.onDeviceAuthRequest) await opts.onDeviceAuthRequest(ctx.body.client_id, ctx.body.scope);
145
+ if (opts.onDeviceAuthRequest) await opts.onDeviceAuthRequest(ctx.body.client_id, ctx.body.scope, ctx.body.resource);
97
146
  const deviceCode = await generateDeviceCode();
98
147
  const userCode = await generateUserCode();
99
148
  const expiresIn = ms(opts.expiresIn);
@@ -108,7 +157,8 @@ Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#secti
108
157
  status: "pending",
109
158
  pollingInterval: ms(opts.interval),
110
159
  clientId: ctx.body.client_id,
111
- scope: ctx.body.scope
160
+ scope: ctx.body.scope,
161
+ resource: ctx.body.resource ? serializeResource(ctx.body.resource) : null
112
162
  }
113
163
  });
114
164
  const { verificationUri, verificationUriComplete } = buildVerificationUris(opts.verificationUri, ctx.context.baseURL, userCode);
@@ -331,6 +381,21 @@ const deviceVerify = createAuthEndpoint("/device", {
331
381
  "denied"
332
382
  ],
333
383
  description: "Current status of the device authorization"
384
+ },
385
+ client_id: {
386
+ type: "string",
387
+ description: "The client requesting authorization, returned only to the authenticated user who owns this request"
388
+ },
389
+ scope: {
390
+ type: "string",
391
+ description: "The requested scopes, returned only to the authenticated user who owns this request"
392
+ },
393
+ resource: {
394
+ oneOf: [{ type: "string" }, {
395
+ type: "array",
396
+ items: { type: "string" }
397
+ }],
398
+ description: "The requested resource indicators, returned only to the authenticated user who owns this request"
334
399
  }
335
400
  }
336
401
  } } }
@@ -377,9 +442,15 @@ const deviceVerify = createAuthEndpoint("/device", {
377
442
  set: { userId: session.user.id }
378
443
  })) deviceCodeRecord.userId = session.user.id;
379
444
  }
445
+ const canReviewRequest = session?.user.id !== void 0 && deviceCodeRecord.userId === session.user.id;
380
446
  return ctx.json({
381
447
  user_code,
382
- status: deviceCodeRecord.status
448
+ status: deviceCodeRecord.status,
449
+ ...canReviewRequest ? {
450
+ client_id: deviceCodeRecord.clientId,
451
+ scope: deviceCodeRecord.scope,
452
+ resource: parseStoredResource(deviceCodeRecord.resource)
453
+ } : {}
383
454
  });
384
455
  });
385
456
  const deviceApprove = createAuthEndpoint("/device/approve", {
@@ -1,43 +1,50 @@
1
1
  import * as z from "zod";
2
2
  //#region src/plugins/device-authorization/schema.ts
3
- const schema = { deviceCode: { fields: {
4
- deviceCode: {
5
- type: "string",
6
- required: true
7
- },
8
- userCode: {
9
- type: "string",
10
- required: true
11
- },
12
- userId: {
13
- type: "string",
14
- required: false
15
- },
16
- expiresAt: {
17
- type: "date",
18
- required: true
19
- },
20
- status: {
21
- type: "string",
22
- required: true
23
- },
24
- lastPolledAt: {
25
- type: "date",
26
- required: false
27
- },
28
- pollingInterval: {
29
- type: "number",
30
- required: false
31
- },
32
- clientId: {
33
- type: "string",
34
- required: false
35
- },
36
- scope: {
37
- type: "string",
38
- required: false
39
- }
40
- } } };
3
+ const schema = { deviceCode: {
4
+ fields: {
5
+ deviceCode: {
6
+ type: "string",
7
+ required: true
8
+ },
9
+ userCode: {
10
+ type: "string",
11
+ required: true
12
+ },
13
+ userId: {
14
+ type: "string",
15
+ required: false
16
+ },
17
+ expiresAt: {
18
+ type: "date",
19
+ required: true
20
+ },
21
+ status: {
22
+ type: "string",
23
+ required: true
24
+ },
25
+ lastPolledAt: {
26
+ type: "date",
27
+ required: false
28
+ },
29
+ pollingInterval: {
30
+ type: "number",
31
+ required: false
32
+ },
33
+ clientId: {
34
+ type: "string",
35
+ required: false
36
+ },
37
+ scope: {
38
+ type: "string",
39
+ required: false
40
+ },
41
+ resource: {
42
+ type: "string",
43
+ required: false
44
+ }
45
+ },
46
+ indexes: [{ fields: ["deviceCode"] }, { fields: ["userCode"] }]
47
+ } };
41
48
  z.object({
42
49
  id: z.string(),
43
50
  deviceCode: z.string(),
@@ -48,7 +55,8 @@ z.object({
48
55
  lastPolledAt: z.date().optional(),
49
56
  pollingInterval: z.number().optional(),
50
57
  clientId: z.string().optional(),
51
- scope: z.string().optional()
58
+ scope: z.string().optional(),
59
+ resource: z.string().optional()
52
60
  });
53
61
  //#endregion
54
62
  export { schema };
@@ -24,7 +24,7 @@ declare module "@better-auth/core" {
24
24
  * Base type for OAuth provider options.
25
25
  * Extracts common fields from GenericOAuthConfig for provider helpers.
26
26
  */
27
- type BaseOAuthProviderOptions = Pick<GenericOAuthConfig, "clientId" | "clientSecret" | "tokenEndpointAuth" | "scopes" | "redirectURI" | "pkce" | "disableImplicitSignUp" | "disableSignUp" | "overrideUserInfo">;
27
+ type BaseOAuthProviderOptions = Pick<GenericOAuthConfig, "clientId" | "clientSecret" | "tokenEndpointAuth" | "scopes" | "redirectURI" | "endSessionEndpoint" | "postLogoutRedirectURI" | "disableProviderLogout" | "pkce" | "disableImplicitSignUp" | "disableSignUp" | "overrideUserInfo">;
28
28
  /**
29
29
  * A generic OAuth plugin that registers any OAuth/OIDC provider
30
30
  * as a first-class social provider.
@@ -83,6 +83,7 @@ const genericOAuth = (options) => {
83
83
  let authorizationUrl = c.authorizationUrl;
84
84
  let tokenUrl = c.tokenUrl;
85
85
  let userInfoUrl = c.userInfoUrl;
86
+ let endSessionEndpoint = c.endSessionEndpoint;
86
87
  let issuer;
87
88
  let isOidc = false;
88
89
  let idTokenConfig;
@@ -96,6 +97,7 @@ const genericOAuth = (options) => {
96
97
  authorizationUrl ??= discovered.authorization_endpoint;
97
98
  tokenUrl ??= discovered.token_endpoint;
98
99
  userInfoUrl ??= discovered.userinfo_endpoint;
100
+ endSessionEndpoint ??= discovered.end_session_endpoint;
99
101
  issuer = discovered.issuer;
100
102
  const signingAlgs = discovered.id_token_signing_alg_values_supported;
101
103
  isOidc = Array.isArray(signingAlgs) && signingAlgs.length > 0;
@@ -116,6 +118,7 @@ const genericOAuth = (options) => {
116
118
  } else if (!c.accountIssuer) throw new Error(`Provider "${c.providerId}": discovery returned no valid data. Provider initialization stopped to keep its account issuer stable.`);
117
119
  else if (!authorizationUrl || !tokenUrl) ctx.logger.error(`Provider "${c.providerId}": discovery returned no data and no explicit endpoints configured. OAuth sign-in will fail for this provider.`);
118
120
  }
121
+ if (c.requireIdTokenVerification && !idTokenConfig) throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
119
122
  const tokenEndpointAuth = c.tokenEndpointAuth;
120
123
  if (c.clientSecret && isSecretlessTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" cannot be combined with clientSecret`);
121
124
  if (!c.clientSecret && isClientSecretTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" requires clientSecret`);
@@ -141,6 +144,25 @@ const genericOAuth = (options) => {
141
144
  idToken: idTokenConfig,
142
145
  requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
143
146
  allowIdpInitiated: c.allowIdpInitiated,
147
+ async createEndSessionURL(data) {
148
+ if (c.disableProviderLogout) return null;
149
+ if (!endSessionEndpoint) return null;
150
+ let url;
151
+ try {
152
+ url = new URL(endSessionEndpoint);
153
+ } catch {
154
+ return null;
155
+ }
156
+ if (data.idToken) url.searchParams.set("id_token_hint", data.idToken);
157
+ const configuredRedirectURI = data.postLogoutRedirectURI ?? c.postLogoutRedirectURI;
158
+ const postLogoutRedirectURI = configuredRedirectURI ? new URL(configuredRedirectURI, ctx.baseURL).toString() : void 0;
159
+ if (postLogoutRedirectURI) {
160
+ url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectURI);
161
+ url.searchParams.set("client_id", c.clientId);
162
+ if (data.state) url.searchParams.set("state", data.state);
163
+ } else if (!data.idToken) url.searchParams.set("client_id", c.clientId);
164
+ return url;
165
+ },
144
166
  createAuthorizationURL(data) {
145
167
  if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
146
168
  return createAuthorizationURL({
@@ -38,6 +38,9 @@ function auth0(options) {
38
38
  tokenEndpointAuth: options.tokenEndpointAuth,
39
39
  scopes: options.scopes ?? defaultScopes,
40
40
  redirectURI: options.redirectURI,
41
+ endSessionEndpoint: options.endSessionEndpoint,
42
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
43
+ disableProviderLogout: options.disableProviderLogout,
41
44
  pkce: options.pkce,
42
45
  disableImplicitSignUp: options.disableImplicitSignUp,
43
46
  disableSignUp: options.disableSignUp,
@@ -48,6 +48,9 @@ function gumroad(options) {
48
48
  tokenEndpointAuth: options.tokenEndpointAuth,
49
49
  scopes: options.scopes ?? ["view_profile"],
50
50
  redirectURI: options.redirectURI,
51
+ endSessionEndpoint: options.endSessionEndpoint,
52
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
53
+ disableProviderLogout: options.disableProviderLogout,
51
54
  pkce: options.pkce,
52
55
  disableImplicitSignUp: options.disableImplicitSignUp,
53
56
  disableSignUp: options.disableSignUp,
@@ -47,6 +47,9 @@ function hubspot(options) {
47
47
  tokenEndpointAuth: options.tokenEndpointAuth,
48
48
  scopes: options.scopes ?? defaultScopes,
49
49
  redirectURI: options.redirectURI,
50
+ endSessionEndpoint: options.endSessionEndpoint,
51
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
52
+ disableProviderLogout: options.disableProviderLogout,
50
53
  authentication: "post",
51
54
  pkce: options.pkce,
52
55
  disableImplicitSignUp: options.disableImplicitSignUp,
@@ -37,6 +37,9 @@ function keycloak(options) {
37
37
  tokenEndpointAuth: options.tokenEndpointAuth,
38
38
  scopes: options.scopes ?? defaultScopes,
39
39
  redirectURI: options.redirectURI,
40
+ endSessionEndpoint: options.endSessionEndpoint,
41
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
42
+ disableProviderLogout: options.disableProviderLogout,
40
43
  pkce: options.pkce,
41
44
  disableImplicitSignUp: options.disableImplicitSignUp,
42
45
  disableSignUp: options.disableSignUp,
@@ -80,6 +80,9 @@ function line(options) {
80
80
  tokenEndpointAuth: options.tokenEndpointAuth,
81
81
  scopes: options.scopes ?? defaultScopes,
82
82
  redirectURI: options.redirectURI,
83
+ endSessionEndpoint: options.endSessionEndpoint,
84
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
85
+ disableProviderLogout: options.disableProviderLogout,
83
86
  pkce: options.pkce,
84
87
  disableImplicitSignUp: options.disableImplicitSignUp,
85
88
  disableSignUp: options.disableSignUp,
@@ -1,5 +1,9 @@
1
+ import { decodeJwt } from "jose";
1
2
  import { betterFetch } from "@better-fetch/fetch";
2
3
  //#region src/plugins/generic-oauth/providers/microsoft-entra-id.ts
4
+ function getMicrosoftProfileName(profile) {
5
+ return profile.name ?? (`${profile.given_name ?? profile.givenname ?? ""} ${profile.family_name ?? profile.familyname ?? ""}`.trim() || void 0);
6
+ }
3
7
  /**
4
8
  * Microsoft Entra ID (Azure AD) OAuth provider helper
5
9
  *
@@ -32,23 +36,42 @@ function microsoftEntraId(options) {
32
36
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(tenantId)) throw new Error("The generic microsoftEntraId helper requires a concrete Microsoft Entra tenant GUID. Use the built-in Microsoft provider for common, organizations, or consumers.");
33
37
  const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
34
38
  const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
39
+ const discoveryUrl = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`;
35
40
  const userInfoUrl = "https://graph.microsoft.com/oidc/userinfo";
36
- const tenantIssuer = `https://login.microsoftonline.com/${tenantId}/v2.0`;
37
41
  const getUserInfo = async (tokens) => {
42
+ if (!tokens.idToken) return null;
43
+ let tokenProfile;
44
+ try {
45
+ tokenProfile = decodeJwt(tokens.idToken);
46
+ } catch {
47
+ return null;
48
+ }
49
+ if (!(typeof tokenProfile.oid === "string" && tokenProfile.oid.trim().length > 0 ? tokenProfile.oid : void 0)) return null;
50
+ const tokenUserInfo = {
51
+ ...tokenProfile,
52
+ name: getMicrosoftProfileName(tokenProfile),
53
+ email: tokenProfile.email ?? tokenProfile.preferred_username ?? void 0,
54
+ image: tokenProfile.picture,
55
+ emailVerified: tokenProfile.email_verified ?? false
56
+ };
57
+ if (!tokens.accessToken) return tokenUserInfo;
38
58
  const { data: profile, error } = await betterFetch(userInfoUrl, { headers: { Authorization: `Bearer ${tokens.accessToken}` } });
39
- if (error || !profile) return null;
59
+ if (error || !profile) return tokenUserInfo;
60
+ if (typeof tokenProfile.sub !== "string" || profile.sub !== tokenProfile.sub) return tokenUserInfo;
40
61
  return {
41
- sub: profile.sub,
42
- name: profile.name ?? (`${profile.given_name ?? ""} ${profile.family_name ?? ""}`.trim() || void 0),
43
- email: profile.email ?? profile.preferred_username ?? void 0,
44
- image: profile.picture,
45
- emailVerified: profile.email_verified ?? false
62
+ ...profile,
63
+ ...tokenProfile,
64
+ name: getMicrosoftProfileName(tokenProfile) ?? getMicrosoftProfileName(profile),
65
+ email: tokenProfile.email ?? profile.email ?? tokenProfile.preferred_username ?? profile.preferred_username ?? void 0,
66
+ image: tokenProfile.picture ?? profile.picture,
67
+ emailVerified: tokenProfile.email_verified ?? profile.email_verified ?? false
46
68
  };
47
69
  };
48
70
  return {
49
71
  providerId: "microsoft-entra-id",
50
- accountSubject: ({ profile }) => profile.sub ?? "",
51
- accountIssuer: tenantIssuer,
72
+ accountSubject: ({ profile }) => typeof profile.oid === "string" ? profile.oid : "",
73
+ discoveryUrl,
74
+ requireIdTokenVerification: true,
52
75
  authorizationUrl,
53
76
  tokenUrl,
54
77
  userInfoUrl,
@@ -57,6 +80,9 @@ function microsoftEntraId(options) {
57
80
  tokenEndpointAuth: options.tokenEndpointAuth,
58
81
  scopes: options.scopes ?? defaultScopes,
59
82
  redirectURI: options.redirectURI,
83
+ endSessionEndpoint: options.endSessionEndpoint,
84
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
85
+ disableProviderLogout: options.disableProviderLogout,
60
86
  pkce: options.pkce,
61
87
  disableImplicitSignUp: options.disableImplicitSignUp,
62
88
  disableSignUp: options.disableSignUp,
@@ -37,6 +37,9 @@ function okta(options) {
37
37
  tokenEndpointAuth: options.tokenEndpointAuth,
38
38
  scopes: options.scopes ?? defaultScopes,
39
39
  redirectURI: options.redirectURI,
40
+ endSessionEndpoint: options.endSessionEndpoint,
41
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
42
+ disableProviderLogout: options.disableProviderLogout,
40
43
  pkce: options.pkce,
41
44
  disableImplicitSignUp: options.disableImplicitSignUp,
42
45
  disableSignUp: options.disableSignUp,
@@ -47,6 +47,9 @@ function patreon(options) {
47
47
  tokenEndpointAuth: options.tokenEndpointAuth,
48
48
  scopes: options.scopes ?? defaultScopes,
49
49
  redirectURI: options.redirectURI,
50
+ endSessionEndpoint: options.endSessionEndpoint,
51
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
52
+ disableProviderLogout: options.disableProviderLogout,
50
53
  pkce: options.pkce,
51
54
  disableImplicitSignUp: options.disableImplicitSignUp,
52
55
  disableSignUp: options.disableSignUp,
@@ -50,6 +50,9 @@ function slack(options) {
50
50
  tokenEndpointAuth: options.tokenEndpointAuth,
51
51
  scopes: options.scopes ?? defaultScopes,
52
52
  redirectURI: options.redirectURI,
53
+ endSessionEndpoint: options.endSessionEndpoint,
54
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
55
+ disableProviderLogout: options.disableProviderLogout,
53
56
  pkce: options.pkce,
54
57
  disableImplicitSignUp: options.disableImplicitSignUp,
55
58
  disableSignUp: options.disableSignUp,
@@ -53,6 +53,9 @@ function yandex(options) {
53
53
  tokenEndpointAuth: options.tokenEndpointAuth,
54
54
  scopes: options.scopes ?? defaultScopes,
55
55
  redirectURI: options.redirectURI,
56
+ endSessionEndpoint: options.endSessionEndpoint,
57
+ postLogoutRedirectURI: options.postLogoutRedirectURI,
58
+ disableProviderLogout: options.disableProviderLogout,
56
59
  pkce: options.pkce,
57
60
  disableImplicitSignUp: options.disableImplicitSignUp,
58
61
  disableSignUp: options.disableSignUp,
@@ -49,6 +49,17 @@ interface GenericOAuthConfig<ID extends string = string> {
49
49
  * If provided, the authorization and token endpoints will be fetched from this URL.
50
50
  */
51
51
  discoveryUrl?: string | undefined;
52
+ /**
53
+ * Require discovery to provide the issuer and JWKS metadata needed to verify
54
+ * ID tokens before this provider is registered.
55
+ *
56
+ * Enable this when provider identity is derived from ID-token claims. This
57
+ * prevents an unavailable or incomplete discovery document from silently
58
+ * downgrading the provider to unverified token decoding.
59
+ *
60
+ * @default false
61
+ */
62
+ requireIdTokenVerification?: boolean | undefined;
52
63
  /**
53
64
  * URL for the authorization endpoint.
54
65
  * Optional if using discoveryUrl.
@@ -64,6 +75,22 @@ interface GenericOAuthConfig<ID extends string = string> {
64
75
  * Optional if using discoveryUrl.
65
76
  */
66
77
  userInfoUrl?: string | undefined;
78
+ /**
79
+ * URL for the OIDC RP-Initiated Logout endpoint.
80
+ * Optional if using discoveryUrl and the discovery document includes
81
+ * `end_session_endpoint`.
82
+ */
83
+ endSessionEndpoint?: string | undefined;
84
+ /**
85
+ * URL the provider should redirect back to after logout.
86
+ * This must also be registered with the provider as a post-logout redirect URI.
87
+ */
88
+ postLogoutRedirectURI?: string | undefined;
89
+ /**
90
+ * Disable automatic provider logout on `authClient.signOut()`.
91
+ * When set, sign out only clears the Better Auth session.
92
+ */
93
+ disableProviderLogout?: boolean | undefined;
67
94
  /** OAuth client ID */
68
95
  clientId: string;
69
96
  /** OAuth client secret */
@@ -1,15 +1,16 @@
1
+ import { getCurrentAdapter } from "@better-auth/core/context";
1
2
  //#region src/plugins/jwt/adapter.ts
2
- const getJwksAdapter = (adapter, options) => {
3
+ const getJwksAdapter = (baseAdapter, options) => {
3
4
  return {
4
5
  getAllKeys: async (ctx) => {
5
6
  if (options?.adapter?.getJwks) return await options.adapter.getJwks(ctx);
6
- return await adapter.findMany({ model: "jwks" });
7
+ return await (await getCurrentAdapter(baseAdapter)).findMany({ model: "jwks" });
7
8
  },
8
9
  getLatestKey: async (ctx) => {
9
10
  const now = /* @__PURE__ */ new Date();
10
11
  const isLive = (k) => !k.expiresAt || k.expiresAt > now;
11
12
  if (options?.adapter?.getJwks) return (await options.adapter.getJwks(ctx))?.filter(isLive).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
12
- return (await adapter.findMany({ model: "jwks" }))?.filter(isLive).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
13
+ return (await (await getCurrentAdapter(baseAdapter)).findMany({ model: "jwks" }))?.filter(isLive).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
13
14
  },
14
15
  /**
15
16
  * Look up a key by its `id` (matches the JWS `kid` header). Returns
@@ -21,7 +22,7 @@ const getJwksAdapter = (adapter, options) => {
21
22
  */
22
23
  getKeyById: async (ctx, id) => {
23
24
  if (options?.adapter?.getJwks) return (await options.adapter.getJwks(ctx))?.find((k) => k.id === id);
24
- return await adapter.findOne({
25
+ return await (await getCurrentAdapter(baseAdapter)).findOne({
25
26
  model: "jwks",
26
27
  where: [{
27
28
  field: "id",
@@ -45,7 +46,7 @@ const getJwksAdapter = (adapter, options) => {
45
46
  * rationale.
46
47
  */
47
48
  getLatestKeyByAlg: async (ctx, alg) => {
48
- const candidates = options?.adapter?.getJwks ? await options.adapter.getJwks(ctx) : await adapter.findMany({ model: "jwks" });
49
+ const candidates = options?.adapter?.getJwks ? await options.adapter.getJwks(ctx) : await (await getCurrentAdapter(baseAdapter)).findMany({ model: "jwks" });
49
50
  if (!candidates) return void 0;
50
51
  const configAlg = options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
51
52
  const now = /* @__PURE__ */ new Date();
@@ -53,7 +54,7 @@ const getJwksAdapter = (adapter, options) => {
53
54
  },
54
55
  createJwk: async (ctx, webKey) => {
55
56
  if (options?.adapter?.createJwk) return await options.adapter.createJwk(webKey, ctx);
56
- return await adapter.create({
57
+ return await (await getCurrentAdapter(baseAdapter)).create({
57
58
  model: "jwks",
58
59
  data: {
59
60
  ...webKey,
@@ -1,4 +1,5 @@
1
1
  import { ClientFetchOption } from "@better-auth/core";
2
+ import { BetterFetch } from "@better-fetch/fetch";
2
3
  //#region src/plugins/one-tap/client.d.ts
3
4
  declare global {
4
5
  interface Window {
@@ -162,10 +163,10 @@ declare const oneTapClient: (options: GoogleOneTapOptions) => {
162
163
  onResponse(ctx: import("@better-fetch/fetch").ResponseContext): Promise<void>;
163
164
  };
164
165
  }[];
165
- getActions: ($fetch: import("@better-fetch/fetch").BetterFetch, _: import("@better-auth/core").ClientStore) => {
166
+ getActions: ($fetch: BetterFetch, _: import("@better-auth/core").ClientStore) => {
166
167
  oneTap: (opts?: GoogleOneTapActionOptions | undefined, fetchOptions?: ClientFetchOption | undefined) => Promise<void>;
167
168
  };
168
- getAtoms($fetch: import("@better-fetch/fetch").BetterFetch): {};
169
+ getAtoms($fetch: BetterFetch): {};
169
170
  };
170
171
  //#endregion
171
172
  export { GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, oneTapClient };
@@ -88,7 +88,8 @@ async function getHttpTestInstance(options, config) {
88
88
  clientOptions: config?.clientOptions,
89
89
  disableTestUser: config?.disableTestUser,
90
90
  testUser: config?.testUser,
91
- testWith: config?.testWith
91
+ testWith: config?.testWith,
92
+ transaction: config?.transaction
92
93
  });
93
94
  server.setRequestHandler(config?.handler ? config.handler(instance.auth) : toNodeHandler(instance.auth.handler));
94
95
  return {
@@ -15,6 +15,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
15
15
  disableTestUser?: boolean;
16
16
  testUser?: Partial<User>;
17
17
  testWith?: "sqlite" | "postgres" | "mongodb" | "mysql";
18
+ transaction?: boolean;
18
19
  } | undefined): Promise<{
19
20
  auth: Auth<O>;
20
21
  client: AuthClient<(C extends undefined ? {} : C) & {
@@ -67,10 +67,12 @@ async function getTestInstance(options, config) {
67
67
  secret: "better-auth-secret-that-is-long-enough-for-validation-test",
68
68
  database: testWith === "postgres" ? {
69
69
  db: await getPostgres(),
70
- type: "postgres"
70
+ type: "postgres",
71
+ transaction: config?.transaction
71
72
  } : testWith === "mongodb" ? await Promise.all([mongodbClient(), await import("../adapters/mongodb-adapter/index.mjs")]).then(([db, { mongodbAdapter }]) => mongodbAdapter(db)) : testWith === "mysql" ? {
72
73
  db: await getMysql(),
73
- type: "mysql"
74
+ type: "mysql",
75
+ transaction: config?.transaction
74
76
  } : await getSqlite(),
75
77
  emailAndPassword: { enabled: true },
76
78
  rateLimit: { enabled: false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.7.0-rc.2",
3
+ "version": "1.7.0-rc.3",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -465,13 +465,13 @@
465
465
  "kysely": "^0.28.17 || ^0.29.0",
466
466
  "nanostores": "^1.3.0",
467
467
  "zod": "^4.3.6",
468
- "@better-auth/core": "1.7.0-rc.2",
469
- "@better-auth/drizzle-adapter": "1.7.0-rc.2",
470
- "@better-auth/kysely-adapter": "1.7.0-rc.2",
471
- "@better-auth/memory-adapter": "1.7.0-rc.2",
472
- "@better-auth/mongo-adapter": "1.7.0-rc.2",
473
- "@better-auth/prisma-adapter": "1.7.0-rc.2",
474
- "@better-auth/telemetry": "1.7.0-rc.2"
468
+ "@better-auth/core": "1.7.0-rc.3",
469
+ "@better-auth/drizzle-adapter": "1.7.0-rc.3",
470
+ "@better-auth/kysely-adapter": "1.7.0-rc.3",
471
+ "@better-auth/memory-adapter": "1.7.0-rc.3",
472
+ "@better-auth/mongo-adapter": "1.7.0-rc.3",
473
+ "@better-auth/prisma-adapter": "1.7.0-rc.3",
474
+ "@better-auth/telemetry": "1.7.0-rc.3"
475
475
  },
476
476
  "devDependencies": {
477
477
  "@lynx-js/react": "^0.121.2",