@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.5

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 (188) hide show
  1. package/dist/api/index.d.mts +3 -3
  2. package/dist/context/global.mjs +1 -1
  3. package/dist/db/adapter/factory.mjs +62 -0
  4. package/dist/db/adapter/index.d.mts +35 -1
  5. package/dist/db/adapter/types.d.mts +1 -1
  6. package/dist/db/get-tables.mjs +3 -3
  7. package/dist/db/schema/account.d.mts +1 -1
  8. package/dist/db/schema/account.mjs +1 -1
  9. package/dist/db/type.d.mts +12 -0
  10. package/dist/env/env-impl.mjs +1 -1
  11. package/dist/error/codes.d.mts +6 -0
  12. package/dist/error/codes.mjs +6 -0
  13. package/dist/index.d.mts +2 -2
  14. package/dist/instrumentation/tracer.mjs +1 -1
  15. package/dist/oauth2/authorization-params.d.mts +12 -0
  16. package/dist/oauth2/authorization-params.mjs +12 -0
  17. package/dist/oauth2/basic-credentials.d.mts +30 -0
  18. package/dist/oauth2/basic-credentials.mjs +64 -0
  19. package/dist/oauth2/client-assertion.d.mts +38 -22
  20. package/dist/oauth2/client-assertion.mjs +63 -28
  21. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  22. package/dist/oauth2/client-credentials-token.mjs +18 -29
  23. package/dist/oauth2/create-authorization-url.d.mts +13 -2
  24. package/dist/oauth2/create-authorization-url.mjs +28 -7
  25. package/dist/oauth2/index.d.mts +13 -8
  26. package/dist/oauth2/index.mjs +11 -7
  27. package/dist/oauth2/oauth-provider.d.mts +149 -11
  28. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  29. package/dist/oauth2/refresh-access-token.mjs +20 -33
  30. package/dist/oauth2/scopes.d.mts +76 -0
  31. package/dist/oauth2/scopes.mjs +96 -0
  32. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  33. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  34. package/dist/oauth2/utils.d.mts +9 -1
  35. package/dist/oauth2/utils.mjs +14 -2
  36. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  37. package/dist/oauth2/validate-authorization-code.mjs +17 -30
  38. package/dist/oauth2/verify-id-token.d.mts +26 -0
  39. package/dist/oauth2/verify-id-token.mjs +62 -0
  40. package/dist/oauth2/verify.d.mts +14 -0
  41. package/dist/oauth2/verify.mjs +38 -12
  42. package/dist/social-providers/apple.d.mts +18 -20
  43. package/dist/social-providers/apple.mjs +15 -28
  44. package/dist/social-providers/atlassian.d.mts +8 -2
  45. package/dist/social-providers/atlassian.mjs +9 -6
  46. package/dist/social-providers/cognito.d.mts +29 -3
  47. package/dist/social-providers/cognito.mjs +30 -34
  48. package/dist/social-providers/discord.d.mts +8 -2
  49. package/dist/social-providers/discord.mjs +20 -6
  50. package/dist/social-providers/dropbox.d.mts +8 -2
  51. package/dist/social-providers/dropbox.mjs +10 -9
  52. package/dist/social-providers/facebook.d.mts +24 -3
  53. package/dist/social-providers/facebook.mjs +51 -24
  54. package/dist/social-providers/figma.d.mts +8 -2
  55. package/dist/social-providers/figma.mjs +8 -7
  56. package/dist/social-providers/github.d.mts +8 -2
  57. package/dist/social-providers/github.mjs +9 -8
  58. package/dist/social-providers/gitlab.d.mts +8 -2
  59. package/dist/social-providers/gitlab.mjs +8 -7
  60. package/dist/social-providers/google.d.mts +32 -4
  61. package/dist/social-providers/google.mjs +26 -29
  62. package/dist/social-providers/huggingface.d.mts +8 -2
  63. package/dist/social-providers/huggingface.mjs +11 -10
  64. package/dist/social-providers/index.d.mts +322 -75
  65. package/dist/social-providers/kakao.d.mts +8 -2
  66. package/dist/social-providers/kakao.mjs +11 -10
  67. package/dist/social-providers/kick.d.mts +8 -2
  68. package/dist/social-providers/kick.mjs +7 -6
  69. package/dist/social-providers/line.d.mts +11 -3
  70. package/dist/social-providers/line.mjs +14 -15
  71. package/dist/social-providers/linear.d.mts +8 -2
  72. package/dist/social-providers/linear.mjs +7 -6
  73. package/dist/social-providers/linkedin.d.mts +8 -2
  74. package/dist/social-providers/linkedin.mjs +12 -11
  75. package/dist/social-providers/microsoft-entra-id.d.mts +33 -7
  76. package/dist/social-providers/microsoft-entra-id.mjs +28 -38
  77. package/dist/social-providers/naver.d.mts +8 -2
  78. package/dist/social-providers/naver.mjs +7 -6
  79. package/dist/social-providers/notion.d.mts +8 -2
  80. package/dist/social-providers/notion.mjs +9 -6
  81. package/dist/social-providers/paybin.d.mts +8 -2
  82. package/dist/social-providers/paybin.mjs +12 -11
  83. package/dist/social-providers/paypal.d.mts +8 -3
  84. package/dist/social-providers/paypal.mjs +10 -14
  85. package/dist/social-providers/polar.d.mts +8 -2
  86. package/dist/social-providers/polar.mjs +11 -10
  87. package/dist/social-providers/railway.d.mts +8 -2
  88. package/dist/social-providers/railway.mjs +11 -10
  89. package/dist/social-providers/reddit.d.mts +8 -2
  90. package/dist/social-providers/reddit.mjs +11 -9
  91. package/dist/social-providers/roblox.d.mts +8 -2
  92. package/dist/social-providers/roblox.mjs +15 -5
  93. package/dist/social-providers/salesforce.d.mts +8 -2
  94. package/dist/social-providers/salesforce.mjs +11 -10
  95. package/dist/social-providers/slack.d.mts +8 -2
  96. package/dist/social-providers/slack.mjs +18 -15
  97. package/dist/social-providers/spotify.d.mts +8 -2
  98. package/dist/social-providers/spotify.mjs +7 -6
  99. package/dist/social-providers/tiktok.d.mts +8 -2
  100. package/dist/social-providers/tiktok.mjs +21 -5
  101. package/dist/social-providers/twitch.d.mts +8 -2
  102. package/dist/social-providers/twitch.mjs +7 -6
  103. package/dist/social-providers/twitter.d.mts +7 -2
  104. package/dist/social-providers/twitter.mjs +11 -10
  105. package/dist/social-providers/vercel.d.mts +8 -2
  106. package/dist/social-providers/vercel.mjs +7 -9
  107. package/dist/social-providers/vk.d.mts +8 -2
  108. package/dist/social-providers/vk.mjs +7 -6
  109. package/dist/social-providers/wechat.d.mts +8 -2
  110. package/dist/social-providers/wechat.mjs +16 -6
  111. package/dist/social-providers/zoom.d.mts +10 -3
  112. package/dist/social-providers/zoom.mjs +14 -15
  113. package/dist/types/context.d.mts +33 -11
  114. package/dist/types/index.d.mts +1 -1
  115. package/dist/types/init-options.d.mts +121 -6
  116. package/dist/utils/ip.d.mts +5 -4
  117. package/dist/utils/ip.mjs +3 -3
  118. package/dist/utils/redirect-uri.d.mts +20 -0
  119. package/dist/utils/redirect-uri.mjs +48 -0
  120. package/dist/utils/string.d.mts +5 -1
  121. package/dist/utils/string.mjs +20 -1
  122. package/dist/utils/url.d.mts +18 -1
  123. package/dist/utils/url.mjs +30 -1
  124. package/package.json +13 -12
  125. package/src/db/adapter/factory.ts +126 -0
  126. package/src/db/adapter/index.ts +32 -0
  127. package/src/db/adapter/types.ts +1 -0
  128. package/src/db/get-tables.ts +8 -3
  129. package/src/db/schema/account.ts +14 -2
  130. package/src/db/type.ts +12 -0
  131. package/src/env/env-impl.ts +1 -2
  132. package/src/error/codes.ts +6 -0
  133. package/src/oauth2/authorization-params.ts +28 -0
  134. package/src/oauth2/basic-credentials.ts +87 -0
  135. package/src/oauth2/client-assertion.ts +131 -58
  136. package/src/oauth2/client-credentials-token.ts +48 -72
  137. package/src/oauth2/create-authorization-url.ts +30 -8
  138. package/src/oauth2/index.ts +42 -10
  139. package/src/oauth2/oauth-provider.ts +161 -12
  140. package/src/oauth2/refresh-access-token.ts +52 -78
  141. package/src/oauth2/scopes.ts +118 -0
  142. package/src/oauth2/token-endpoint-auth.ts +221 -0
  143. package/src/oauth2/utils.ts +21 -5
  144. package/src/oauth2/validate-authorization-code.ts +55 -85
  145. package/src/oauth2/verify-id-token.ts +111 -0
  146. package/src/oauth2/verify.ts +82 -15
  147. package/src/social-providers/apple.ts +32 -45
  148. package/src/social-providers/atlassian.ts +20 -9
  149. package/src/social-providers/cognito.ts +51 -48
  150. package/src/social-providers/discord.ts +37 -22
  151. package/src/social-providers/dropbox.ts +20 -12
  152. package/src/social-providers/facebook.ts +108 -57
  153. package/src/social-providers/figma.ts +21 -10
  154. package/src/social-providers/github.ts +16 -10
  155. package/src/social-providers/gitlab.ts +16 -8
  156. package/src/social-providers/google.ts +67 -46
  157. package/src/social-providers/huggingface.ts +20 -9
  158. package/src/social-providers/kakao.ts +18 -9
  159. package/src/social-providers/kick.ts +20 -8
  160. package/src/social-providers/line.ts +39 -37
  161. package/src/social-providers/linear.ts +20 -7
  162. package/src/social-providers/linkedin.ts +16 -10
  163. package/src/social-providers/microsoft-entra-id.ts +66 -64
  164. package/src/social-providers/naver.ts +14 -7
  165. package/src/social-providers/notion.ts +20 -7
  166. package/src/social-providers/paybin.ts +16 -11
  167. package/src/social-providers/paypal.ts +12 -25
  168. package/src/social-providers/polar.ts +20 -9
  169. package/src/social-providers/railway.ts +20 -9
  170. package/src/social-providers/reddit.ts +22 -10
  171. package/src/social-providers/roblox.ts +31 -15
  172. package/src/social-providers/salesforce.ts +21 -10
  173. package/src/social-providers/slack.ts +31 -16
  174. package/src/social-providers/spotify.ts +20 -7
  175. package/src/social-providers/tiktok.ts +32 -13
  176. package/src/social-providers/twitch.ts +14 -9
  177. package/src/social-providers/twitter.ts +18 -8
  178. package/src/social-providers/vercel.ts +24 -11
  179. package/src/social-providers/vk.ts +20 -7
  180. package/src/social-providers/wechat.ts +28 -8
  181. package/src/social-providers/zoom.ts +28 -19
  182. package/src/types/context.ts +33 -12
  183. package/src/types/index.ts +7 -0
  184. package/src/types/init-options.ts +148 -5
  185. package/src/utils/ip.ts +12 -13
  186. package/src/utils/redirect-uri.ts +54 -0
  187. package/src/utils/string.ts +37 -0
  188. package/src/utils/url.ts +28 -0
@@ -1,61 +1,41 @@
1
- import { ClientAssertionConfig } from "./client-assertion.mjs";
2
1
  import { AwaitableFunction } from "../types/helper.mjs";
3
2
  import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
3
+ import { TokenEndpointAuth, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
4
4
 
5
5
  //#region src/oauth2/refresh-access-token.d.ts
6
- declare function refreshAccessTokenRequest({
7
- refreshToken,
8
- options,
9
- authentication,
10
- clientAssertion,
11
- tokenEndpoint,
12
- extraParams,
13
- resource
14
- }: {
6
+ interface RefreshAccessTokenRequestInput {
15
7
  refreshToken: string;
16
8
  options: AwaitableFunction<Partial<ProviderOptions>>;
17
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
18
- clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
9
+ authentication?: TokenEndpointSecretAuthentication | undefined;
10
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
19
11
  tokenEndpoint?: string | undefined;
20
12
  extraParams?: Record<string, string> | undefined;
21
13
  resource?: (string | string[]) | undefined;
22
- }): Promise<{
23
- body: URLSearchParams;
24
- headers: Record<string, any>;
25
- }>;
26
- /**
27
- * @deprecated use async'd refreshAccessTokenRequest instead
28
- */
29
- declare function createRefreshAccessTokenRequest({
14
+ }
15
+ interface RefreshAccessTokenInput extends RefreshAccessTokenRequestInput {
16
+ options: Partial<ProviderOptions>;
17
+ tokenEndpoint: string;
18
+ }
19
+ declare function refreshAccessTokenRequest({
30
20
  refreshToken,
31
21
  options,
32
22
  authentication,
23
+ tokenEndpointAuth,
24
+ tokenEndpoint,
33
25
  extraParams,
34
26
  resource
35
- }: {
36
- refreshToken: string;
37
- options: ProviderOptions;
38
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
39
- extraParams?: Record<string, string> | undefined;
40
- resource?: (string | string[]) | undefined;
41
- }): {
27
+ }: RefreshAccessTokenRequestInput): Promise<{
42
28
  body: URLSearchParams;
43
- headers: Record<string, any>;
44
- };
29
+ headers: Record<string, string>;
30
+ }>;
45
31
  declare function refreshAccessToken({
46
32
  refreshToken,
47
33
  options,
48
34
  tokenEndpoint,
49
35
  authentication,
50
- clientAssertion,
51
- extraParams
52
- }: {
53
- refreshToken: string;
54
- options: Partial<ProviderOptions>;
55
- tokenEndpoint: string;
56
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
57
- clientAssertion?: ClientAssertionConfig | undefined;
58
- extraParams?: Record<string, string> | undefined;
59
- }): Promise<OAuth2Tokens>;
36
+ tokenEndpointAuth,
37
+ extraParams,
38
+ resource
39
+ }: RefreshAccessTokenInput): Promise<OAuth2Tokens>;
60
40
  //#endregion
61
- export { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessTokenRequest };
41
+ export { refreshAccessToken, refreshAccessTokenRequest };
@@ -1,33 +1,26 @@
1
- import { resolveAssertionParams } from "./client-assertion.mjs";
2
- import { base64 } from "@better-auth/utils/base64";
1
+ import { applyTokenEndpointAuth } from "./token-endpoint-auth.mjs";
3
2
  import { betterFetch } from "@better-fetch/fetch";
4
3
  //#region src/oauth2/refresh-access-token.ts
5
- async function refreshAccessTokenRequest({ refreshToken, options, authentication, clientAssertion, tokenEndpoint, extraParams, resource }) {
4
+ async function refreshAccessTokenRequest({ refreshToken, options, authentication, tokenEndpointAuth, tokenEndpoint, extraParams, resource }) {
6
5
  options = typeof options === "function" ? await options() : options;
7
- if (authentication === "private_key_jwt") {
8
- if (!clientAssertion) throw new Error("private_key_jwt authentication requires a clientAssertion configuration");
9
- const assertionParams = await resolveAssertionParams({
10
- clientAssertion,
11
- clientId: Array.isArray(options.clientId) ? options.clientId[0] : options.clientId,
12
- tokenEndpoint
13
- });
14
- extraParams = {
15
- ...extraParams,
16
- ...assertionParams
17
- };
18
- }
19
- return createRefreshAccessTokenRequest({
6
+ const request = buildRefreshAccessTokenRequest({
20
7
  refreshToken,
21
8
  options,
22
- authentication,
23
9
  extraParams,
24
10
  resource
25
11
  });
12
+ await applyTokenEndpointAuth({
13
+ body: request.body,
14
+ headers: request.headers,
15
+ options,
16
+ tokenEndpoint: tokenEndpoint ?? "",
17
+ grantType: "refresh_token",
18
+ tokenEndpointAuth,
19
+ authentication
20
+ });
21
+ return request;
26
22
  }
27
- /**
28
- * @deprecated use async'd refreshAccessTokenRequest instead
29
- */
30
- function createRefreshAccessTokenRequest({ refreshToken, options, authentication, extraParams, resource }) {
23
+ function buildRefreshAccessTokenRequest({ refreshToken, options, extraParams, resource }) {
31
24
  const body = new URLSearchParams();
32
25
  const headers = {
33
26
  "content-type": "application/x-www-form-urlencoded",
@@ -35,13 +28,6 @@ function createRefreshAccessTokenRequest({ refreshToken, options, authentication
35
28
  };
36
29
  body.set("grant_type", "refresh_token");
37
30
  body.set("refresh_token", refreshToken);
38
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
39
- if (authentication === "basic") if (primaryClientId) headers["authorization"] = "Basic " + base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`);
40
- else headers["authorization"] = "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
41
- else {
42
- body.set("client_id", primaryClientId);
43
- if (authentication !== "private_key_jwt" && options.clientSecret) body.set("client_secret", options.clientSecret);
44
- }
45
31
  if (resource) if (typeof resource === "string") body.append("resource", resource);
46
32
  else for (const _resource of resource) body.append("resource", _resource);
47
33
  if (extraParams) for (const [key, value] of Object.entries(extraParams)) body.set(key, value);
@@ -50,14 +36,15 @@ function createRefreshAccessTokenRequest({ refreshToken, options, authentication
50
36
  headers
51
37
  };
52
38
  }
53
- async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication, clientAssertion, extraParams }) {
39
+ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication, tokenEndpointAuth, extraParams, resource }) {
54
40
  const { body, headers } = await refreshAccessTokenRequest({
55
41
  refreshToken,
56
42
  options,
57
43
  authentication,
58
- clientAssertion,
44
+ tokenEndpointAuth,
59
45
  tokenEndpoint,
60
- extraParams
46
+ extraParams,
47
+ resource
61
48
  });
62
49
  const { data, error } = await betterFetch(tokenEndpoint, {
63
50
  method: "POST",
@@ -69,7 +56,7 @@ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authen
69
56
  accessToken: data.access_token,
70
57
  refreshToken: data.refresh_token,
71
58
  tokenType: data.token_type,
72
- scopes: data.scope?.split(" "),
59
+ scopes: Array.isArray(data.scope) ? data.scope : data.scope?.split(" "),
73
60
  idToken: data.id_token
74
61
  };
75
62
  if (data.expires_in) {
@@ -83,4 +70,4 @@ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authen
83
70
  return tokens;
84
71
  }
85
72
  //#endregion
86
- export { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessTokenRequest };
73
+ export { refreshAccessToken, refreshAccessTokenRequest };
@@ -0,0 +1,76 @@
1
+ import { ProviderOptions } from "./oauth-provider.mjs";
2
+
3
+ //#region src/oauth2/scopes.d.ts
4
+ /**
5
+ * Parse a provider's `scope` token-response field into a string array.
6
+ *
7
+ * RFC 6749 §3.3 defines `scope` as a space-delimited string, but providers
8
+ * vary: some (e.g. Twitch) return an already-split array. Accept both, plus the
9
+ * omitted/empty case, without ever calling `.split` on a non-string. Returns
10
+ * `[]` when no scope is present.
11
+ *
12
+ * @see https://github.com/better-auth/better-auth/issues/9076
13
+ */
14
+ declare function parseScopeField(scope: unknown): string[];
15
+ /**
16
+ * Normalize a scope set into a single deduped, sorted array.
17
+ *
18
+ * Scope order is insignificant per RFC 6749 §3.3, so normalize for idempotent
19
+ * writes and trivial comparisons: trim each token, drop empties, dedupe, and
20
+ * sort ascending. Returns `[]` when the union is empty.
21
+ *
22
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
23
+ */
24
+ declare function normalizeScopes(stored: string[] | null | undefined, incoming?: string[] | undefined): string[];
25
+ /**
26
+ * Union the stored granted-scope set with the scopes observed on an
27
+ * authorization or token exchange.
28
+ *
29
+ * The provider's echoed `scope` is authoritative when present. RFC 6749 §3.3
30
+ * and §5.1 say an omitted or empty echo means the grant equals what was
31
+ * requested, so fall back to `requested` in that case. The result unions onto
32
+ * the stored grant (never narrows on a normal write) and is normalized per
33
+ * {@link normalizeScopes}.
34
+ *
35
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
36
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
37
+ */
38
+ declare function unionGrantedScopes(stored: string[] | null | undefined, echoed: string[] | undefined, requested: string[] | undefined): string[];
39
+ /**
40
+ * Coerce a stored granted-scope value into a usable array.
41
+ *
42
+ * `account.grantedScopes` is nullable (legacy rows and non-OAuth accounts read
43
+ * as unset), and on dialects that store the array as a JSON string a malformed
44
+ * operator backfill could deserialize to a non-array. Both collapse to `[]`
45
+ * here so every reader works against a real `string[]` without re-deriving the
46
+ * guard.
47
+ */
48
+ declare function readGrantedScopes(stored: string[] | null | undefined): string[];
49
+ /**
50
+ * Test whether a normalized granted-scope set contains a specific scope.
51
+ *
52
+ * Matching is exact and case-sensitive per RFC 6749 §3.3. The argument is the
53
+ * normalized `account.grantedScopes` array; a raw provider `scope` string must
54
+ * be run through {@link parseScopeField} first.
55
+ *
56
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
57
+ */
58
+ declare function includesGrantedScope(granted: string[] | null | undefined, scope: string): boolean;
59
+ /**
60
+ * Compose the effective scope set to encode in a single authorization URL.
61
+ *
62
+ * Precedence: the provider's built-in defaults (unless `disableDefaultScope`),
63
+ * then the integrator's configured `options.scope`, then the per-request
64
+ * `scopes`. The result is the value persisted into OAuth state as the RFC 6749
65
+ * §5.1 fallback, so it is preserved verbatim (not normalized) to match what is
66
+ * sent to the provider.
67
+ *
68
+ * `defaultScopes` is a parameter rather than a provider-contract field so the
69
+ * runtime-synthesized generic OAuth provider, which has no static default set,
70
+ * can pass its configured scopes here.
71
+ *
72
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
73
+ */
74
+ declare function resolveRequestedScopes(options: Pick<ProviderOptions, "scope" | "disableDefaultScope"> | undefined, defaultScopes: string[], perRequestScopes: string[] | undefined): string[];
75
+ //#endregion
76
+ export { includesGrantedScope, normalizeScopes, parseScopeField, readGrantedScopes, resolveRequestedScopes, unionGrantedScopes };
@@ -0,0 +1,96 @@
1
+ //#region src/oauth2/scopes.ts
2
+ /**
3
+ * Parse a provider's `scope` token-response field into a string array.
4
+ *
5
+ * RFC 6749 §3.3 defines `scope` as a space-delimited string, but providers
6
+ * vary: some (e.g. Twitch) return an already-split array. Accept both, plus the
7
+ * omitted/empty case, without ever calling `.split` on a non-string. Returns
8
+ * `[]` when no scope is present.
9
+ *
10
+ * @see https://github.com/better-auth/better-auth/issues/9076
11
+ */
12
+ function parseScopeField(scope) {
13
+ if (Array.isArray(scope)) return scope.filter((s) => typeof s === "string" && s !== "");
14
+ if (typeof scope === "string") return scope.split(" ").filter(Boolean);
15
+ return [];
16
+ }
17
+ /**
18
+ * Normalize a scope set into a single deduped, sorted array.
19
+ *
20
+ * Scope order is insignificant per RFC 6749 §3.3, so normalize for idempotent
21
+ * writes and trivial comparisons: trim each token, drop empties, dedupe, and
22
+ * sort ascending. Returns `[]` when the union is empty.
23
+ *
24
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
25
+ */
26
+ function normalizeScopes(stored, incoming) {
27
+ const normalized = /* @__PURE__ */ new Set();
28
+ for (const scope of [...stored ?? [], ...incoming ?? []]) {
29
+ const trimmed = scope.trim();
30
+ if (trimmed) normalized.add(trimmed);
31
+ }
32
+ return [...normalized].sort();
33
+ }
34
+ /**
35
+ * Union the stored granted-scope set with the scopes observed on an
36
+ * authorization or token exchange.
37
+ *
38
+ * The provider's echoed `scope` is authoritative when present. RFC 6749 §3.3
39
+ * and §5.1 say an omitted or empty echo means the grant equals what was
40
+ * requested, so fall back to `requested` in that case. The result unions onto
41
+ * the stored grant (never narrows on a normal write) and is normalized per
42
+ * {@link normalizeScopes}.
43
+ *
44
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
45
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
46
+ */
47
+ function unionGrantedScopes(stored, echoed, requested) {
48
+ return normalizeScopes(stored, echoed?.length ? echoed : requested);
49
+ }
50
+ /**
51
+ * Coerce a stored granted-scope value into a usable array.
52
+ *
53
+ * `account.grantedScopes` is nullable (legacy rows and non-OAuth accounts read
54
+ * as unset), and on dialects that store the array as a JSON string a malformed
55
+ * operator backfill could deserialize to a non-array. Both collapse to `[]`
56
+ * here so every reader works against a real `string[]` without re-deriving the
57
+ * guard.
58
+ */
59
+ function readGrantedScopes(stored) {
60
+ return Array.isArray(stored) ? stored : [];
61
+ }
62
+ /**
63
+ * Test whether a normalized granted-scope set contains a specific scope.
64
+ *
65
+ * Matching is exact and case-sensitive per RFC 6749 §3.3. The argument is the
66
+ * normalized `account.grantedScopes` array; a raw provider `scope` string must
67
+ * be run through {@link parseScopeField} first.
68
+ *
69
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
70
+ */
71
+ function includesGrantedScope(granted, scope) {
72
+ return granted?.includes(scope) ?? false;
73
+ }
74
+ /**
75
+ * Compose the effective scope set to encode in a single authorization URL.
76
+ *
77
+ * Precedence: the provider's built-in defaults (unless `disableDefaultScope`),
78
+ * then the integrator's configured `options.scope`, then the per-request
79
+ * `scopes`. The result is the value persisted into OAuth state as the RFC 6749
80
+ * §5.1 fallback, so it is preserved verbatim (not normalized) to match what is
81
+ * sent to the provider.
82
+ *
83
+ * `defaultScopes` is a parameter rather than a provider-contract field so the
84
+ * runtime-synthesized generic OAuth provider, which has no static default set,
85
+ * can pass its configured scopes here.
86
+ *
87
+ * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
88
+ */
89
+ function resolveRequestedScopes(options, defaultScopes, perRequestScopes) {
90
+ const scopes = options?.disableDefaultScope ? [] : [...defaultScopes];
91
+ if (options?.scope) scopes.push(...options.scope);
92
+ if (perRequestScopes) scopes.push(...perRequestScopes);
93
+ return scopes;
94
+ }
95
+ //#endregion
96
+ export { includesGrantedScope, normalizeScopes, parseScopeField, readGrantedScopes, resolveRequestedScopes, unionGrantedScopes };
@@ -0,0 +1,17 @@
1
+ import { ClientAssertionGetter } from "./client-assertion.mjs";
2
+
3
+ //#region src/oauth2/token-endpoint-auth.d.ts
4
+ type TokenEndpointAuth = {
5
+ method: "none";
6
+ } | {
7
+ method: "client_secret_basic";
8
+ } | {
9
+ method: "client_secret_post";
10
+ } | {
11
+ method: "private_key_jwt";
12
+ getClientAssertion: ClientAssertionGetter;
13
+ };
14
+ type TokenEndpointAuthMethod = TokenEndpointAuth["method"];
15
+ type TokenEndpointSecretAuthentication = "basic" | "post";
16
+ //#endregion
17
+ export { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointSecretAuthentication };
@@ -0,0 +1,89 @@
1
+ import { getPrimaryClientId } from "./utils.mjs";
2
+ import { encodeBasicCredentials } from "./basic-credentials.mjs";
3
+ import { resolveClientAssertionParams } from "./client-assertion.mjs";
4
+ //#region src/oauth2/token-endpoint-auth.ts
5
+ function getDefaultTokenEndpointAuth(options, authentication) {
6
+ if (authentication === "basic") return { method: "client_secret_basic" };
7
+ if (options.clientSecret) return { method: "client_secret_post" };
8
+ return { method: "none" };
9
+ }
10
+ function assertNoClientSecret(method, options, body) {
11
+ if (options.clientSecret || body.has("client_secret")) throw new Error(`${method} token endpoint authentication cannot be combined with clientSecret`);
12
+ }
13
+ function setClientId(body, clientId) {
14
+ if (clientId) body.set("client_id", clientId);
15
+ }
16
+ function assertClientSecretConfigured(method, options) {
17
+ if (!options.clientSecret) throw new Error(`${method} token endpoint authentication requires clientSecret`);
18
+ }
19
+ function assertClientIdConfigured(method, clientId) {
20
+ if (!clientId) throw new Error(`${method} token endpoint authentication requires clientId`);
21
+ }
22
+ function setClientSecretPostAuth({ body, options, clientId, requireClientSecret }) {
23
+ if (requireClientSecret) assertClientSecretConfigured("client_secret_post", options);
24
+ if (options.clientSecret) {
25
+ assertClientIdConfigured("client_secret_post", clientId);
26
+ setClientId(body, clientId);
27
+ body.set("client_secret", options.clientSecret);
28
+ }
29
+ }
30
+ function setClientSecretBasicAuth({ headers, options, clientId, body }) {
31
+ if (body.has("client_secret")) throw new Error("client_secret_basic token endpoint authentication cannot be combined with client_secret body parameters");
32
+ assertClientSecretConfigured("client_secret_basic", options);
33
+ assertClientIdConfigured("client_secret_basic", clientId);
34
+ headers.authorization = encodeBasicCredentials(clientId, options.clientSecret);
35
+ }
36
+ function assertCompleteManualClientAssertion(body) {
37
+ if (body.has("client_assertion") !== body.has("client_assertion_type")) throw new Error("client_assertion and client_assertion_type must both be provided");
38
+ }
39
+ async function applyTokenEndpointAuth({ body, headers, options, tokenEndpoint, grantType, tokenEndpointAuth, authentication }) {
40
+ assertCompleteManualClientAssertion(body);
41
+ const clientId = getPrimaryClientId(options.clientId);
42
+ if (body.has("client_assertion")) {
43
+ if (tokenEndpointAuth) throw new Error("client_assertion body parameters cannot be combined with tokenEndpointAuth");
44
+ assertNoClientSecret("private_key_jwt", options, body);
45
+ setClientId(body, clientId);
46
+ return;
47
+ }
48
+ const auth = tokenEndpointAuth ?? getDefaultTokenEndpointAuth(options, authentication);
49
+ if (auth.method === "private_key_jwt") {
50
+ assertNoClientSecret(auth.method, options, body);
51
+ assertClientIdConfigured(auth.method, clientId);
52
+ if (!tokenEndpoint) throw new Error("private_key_jwt token endpoint authentication requires tokenEndpoint");
53
+ const assertionParams = await resolveClientAssertionParams({
54
+ getClientAssertion: auth.getClientAssertion,
55
+ context: {
56
+ clientId,
57
+ tokenEndpoint,
58
+ grantType
59
+ }
60
+ });
61
+ setClientId(body, clientId);
62
+ for (const [key, value] of Object.entries(assertionParams)) body.set(key, value);
63
+ return;
64
+ }
65
+ if (auth.method === "none") {
66
+ assertNoClientSecret(auth.method, options, body);
67
+ if (grantType === "client_credentials") throw new Error("none token endpoint authentication cannot be used with client_credentials grant");
68
+ assertClientIdConfigured(auth.method, clientId);
69
+ setClientId(body, clientId);
70
+ return;
71
+ }
72
+ if (auth.method === "client_secret_basic") {
73
+ setClientSecretBasicAuth({
74
+ headers,
75
+ options,
76
+ clientId,
77
+ body
78
+ });
79
+ return;
80
+ }
81
+ setClientSecretPostAuth({
82
+ body,
83
+ options,
84
+ clientId,
85
+ requireClientSecret: tokenEndpointAuth?.method === "client_secret_post"
86
+ });
87
+ }
88
+ //#endregion
89
+ export { applyTokenEndpointAuth };
@@ -2,6 +2,14 @@ import { OAuth2Tokens } from "./oauth-provider.mjs";
2
2
 
3
3
  //#region src/oauth2/utils.d.ts
4
4
  declare function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens;
5
+ /**
6
+ * Fill in `accessTokenExpiresAt` from the provider's configured
7
+ * `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
8
+ * known expiry, `getAccessToken` cannot tell the token is expired and never
9
+ * refreshes it. No-op when the provider already supplied an expiry or no
10
+ * fallback is configured.
11
+ */
12
+ declare function applyDefaultAccessTokenExpiry(tokens: OAuth2Tokens, accessTokenExpiresIn: number | undefined): OAuth2Tokens;
5
13
  /**
6
14
  * Return the provider's primary Client ID: the single string, or the entry at
7
15
  * array index 0 for the cross-platform form used by ID token audience
@@ -13,4 +21,4 @@ declare function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens;
13
21
  declare function getPrimaryClientId(clientId: unknown): string | undefined;
14
22
  declare function generateCodeChallenge(codeVerifier: string): Promise<string>;
15
23
  //#endregion
16
- export { generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
24
+ export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
@@ -1,3 +1,4 @@
1
+ import { parseScopeField } from "./scopes.mjs";
1
2
  import { base64Url } from "@better-auth/utils/base64";
2
3
  //#region src/oauth2/utils.ts
3
4
  function getOAuth2Tokens(data) {
@@ -11,12 +12,23 @@ function getOAuth2Tokens(data) {
11
12
  refreshToken: data.refresh_token,
12
13
  accessTokenExpiresAt: data.expires_in ? getDate(data.expires_in) : void 0,
13
14
  refreshTokenExpiresAt: data.refresh_token_expires_in ? getDate(data.refresh_token_expires_in) : void 0,
14
- scopes: data?.scope ? typeof data.scope === "string" ? data.scope.split(" ") : data.scope : [],
15
+ scopes: parseScopeField(data.scope),
15
16
  idToken: data.id_token,
16
17
  raw: data
17
18
  };
18
19
  }
19
20
  /**
21
+ * Fill in `accessTokenExpiresAt` from the provider's configured
22
+ * `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
23
+ * known expiry, `getAccessToken` cannot tell the token is expired and never
24
+ * refreshes it. No-op when the provider already supplied an expiry or no
25
+ * fallback is configured.
26
+ */
27
+ function applyDefaultAccessTokenExpiry(tokens, accessTokenExpiresIn) {
28
+ if (!tokens.accessTokenExpiresAt && accessTokenExpiresIn) tokens.accessTokenExpiresAt = new Date(Date.now() + accessTokenExpiresIn * 1e3);
29
+ return tokens;
30
+ }
31
+ /**
20
32
  * Return the provider's primary Client ID: the single string, or the entry at
21
33
  * array index 0 for the cross-platform form used by ID token audience
22
34
  * verification. Index 0 is the designated primary and pairs with
@@ -34,4 +46,4 @@ async function generateCodeChallenge(codeVerifier) {
34
46
  return base64Url.encode(new Uint8Array(hash), { padding: false });
35
47
  }
36
48
  //#endregion
37
- export { generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
49
+ export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
@@ -1,64 +1,41 @@
1
- import { ClientAssertionConfig } from "./client-assertion.mjs";
2
1
  import { AwaitableFunction } from "../types/helper.mjs";
3
2
  import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
3
+ import { TokenEndpointAuth, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
4
4
  import * as jose from "jose";
5
5
 
6
6
  //#region src/oauth2/validate-authorization-code.d.ts
7
- declare function authorizationCodeRequest({
8
- code,
9
- codeVerifier,
10
- redirectURI,
11
- options,
12
- authentication,
13
- clientAssertion,
14
- tokenEndpoint,
15
- deviceId,
16
- headers,
17
- additionalParams,
18
- resource
19
- }: {
7
+ interface AuthorizationCodeRequestInput {
20
8
  code: string;
21
9
  redirectURI: string;
22
10
  options: AwaitableFunction<Partial<ProviderOptions>>;
23
11
  codeVerifier?: string | undefined;
24
12
  deviceId?: string | undefined;
25
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
26
- clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
13
+ authentication?: TokenEndpointSecretAuthentication | undefined;
14
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
27
15
  tokenEndpoint?: string | undefined;
28
16
  headers?: Record<string, string> | undefined;
29
17
  additionalParams?: Record<string, string> | undefined;
30
18
  resource?: (string | string[]) | undefined;
31
- }): Promise<{
32
- body: URLSearchParams;
33
- headers: Record<string, any>;
34
- }>;
35
- /**
36
- * @deprecated use async'd authorizationCodeRequest instead
37
- */
38
- declare function createAuthorizationCodeRequest({
19
+ }
20
+ interface ValidateAuthorizationCodeInput extends AuthorizationCodeRequestInput {
21
+ tokenEndpoint: string;
22
+ }
23
+ declare function authorizationCodeRequest({
39
24
  code,
40
25
  codeVerifier,
41
26
  redirectURI,
42
27
  options,
43
28
  authentication,
29
+ tokenEndpointAuth,
30
+ tokenEndpoint,
44
31
  deviceId,
45
32
  headers,
46
33
  additionalParams,
47
34
  resource
48
- }: {
49
- code: string;
50
- redirectURI: string;
51
- options: Partial<ProviderOptions>;
52
- codeVerifier?: string | undefined;
53
- deviceId?: string | undefined;
54
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
55
- headers?: Record<string, string> | undefined;
56
- additionalParams?: Record<string, string> | undefined;
57
- resource?: (string | string[]) | undefined;
58
- }): {
35
+ }: AuthorizationCodeRequestInput): Promise<{
59
36
  body: URLSearchParams;
60
- headers: Record<string, any>;
61
- };
37
+ headers: Record<string, string>;
38
+ }>;
62
39
  declare function validateAuthorizationCode({
63
40
  code,
64
41
  codeVerifier,
@@ -66,27 +43,15 @@ declare function validateAuthorizationCode({
66
43
  options,
67
44
  tokenEndpoint,
68
45
  authentication,
69
- clientAssertion,
46
+ tokenEndpointAuth,
70
47
  deviceId,
71
48
  headers,
72
49
  additionalParams,
73
50
  resource
74
- }: {
75
- code: string;
76
- redirectURI: string;
77
- options: AwaitableFunction<Partial<ProviderOptions>>;
78
- codeVerifier?: string | undefined;
79
- deviceId?: string | undefined;
80
- tokenEndpoint: string;
81
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
82
- clientAssertion?: ClientAssertionConfig | undefined;
83
- headers?: Record<string, string> | undefined;
84
- additionalParams?: Record<string, string> | undefined;
85
- resource?: (string | string[]) | undefined;
86
- }): Promise<OAuth2Tokens>;
51
+ }: ValidateAuthorizationCodeInput): Promise<OAuth2Tokens>;
87
52
  declare function validateToken(token: string, jwksEndpoint: string, options?: {
88
53
  audience?: string | string[];
89
54
  issuer?: string | string[];
90
55
  }): Promise<jose.JWTVerifyResult<jose.JWTPayload> & jose.ResolvedKey>;
91
56
  //#endregion
92
- export { authorizationCodeRequest, createAuthorizationCodeRequest, validateAuthorizationCode, validateToken };
57
+ export { authorizationCodeRequest, validateAuthorizationCode, validateToken };