@better-auth/core 1.7.0-beta.7 → 1.7.0-beta.8

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 (140) 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 +1 -1
  4. package/dist/db/get-tables.mjs +3 -3
  5. package/dist/db/schema/account.d.mts +1 -1
  6. package/dist/db/schema/account.mjs +1 -1
  7. package/dist/error/codes.d.mts +0 -5
  8. package/dist/error/codes.mjs +0 -5
  9. package/dist/instrumentation/tracer.mjs +1 -1
  10. package/dist/oauth2/create-authorization-url.d.mts +1 -4
  11. package/dist/oauth2/create-authorization-url.mjs +1 -4
  12. package/dist/oauth2/index.d.mts +3 -4
  13. package/dist/oauth2/index.mjs +2 -3
  14. package/dist/oauth2/oauth-provider.d.mts +12 -50
  15. package/dist/oauth2/refresh-access-token.mjs +2 -1
  16. package/dist/oauth2/utils.d.mts +6 -1
  17. package/dist/oauth2/utils.mjs +24 -2
  18. package/dist/oauth2/verify-id-token.d.mts +6 -5
  19. package/dist/oauth2/verify-id-token.mjs +2 -2
  20. package/dist/social-providers/apple.d.mts +1 -5
  21. package/dist/social-providers/apple.mjs +5 -5
  22. package/dist/social-providers/atlassian.d.mts +1 -5
  23. package/dist/social-providers/atlassian.mjs +4 -4
  24. package/dist/social-providers/cognito.d.mts +1 -5
  25. package/dist/social-providers/cognito.mjs +11 -18
  26. package/dist/social-providers/discord.d.mts +1 -5
  27. package/dist/social-providers/discord.mjs +6 -7
  28. package/dist/social-providers/dropbox.d.mts +1 -5
  29. package/dist/social-providers/dropbox.mjs +5 -5
  30. package/dist/social-providers/facebook.d.mts +1 -5
  31. package/dist/social-providers/facebook.mjs +5 -5
  32. package/dist/social-providers/figma.d.mts +1 -5
  33. package/dist/social-providers/figma.mjs +5 -5
  34. package/dist/social-providers/github.d.mts +1 -5
  35. package/dist/social-providers/github.mjs +4 -4
  36. package/dist/social-providers/gitlab.d.mts +1 -5
  37. package/dist/social-providers/gitlab.mjs +6 -6
  38. package/dist/social-providers/google.d.mts +8 -10
  39. package/dist/social-providers/google.mjs +12 -13
  40. package/dist/social-providers/huggingface.d.mts +1 -5
  41. package/dist/social-providers/huggingface.mjs +8 -8
  42. package/dist/social-providers/index.d.mts +35 -177
  43. package/dist/social-providers/kakao.d.mts +1 -5
  44. package/dist/social-providers/kakao.mjs +8 -8
  45. package/dist/social-providers/kick.d.mts +1 -5
  46. package/dist/social-providers/kick.mjs +4 -4
  47. package/dist/social-providers/line.d.mts +1 -5
  48. package/dist/social-providers/line.mjs +10 -10
  49. package/dist/social-providers/linear.d.mts +1 -5
  50. package/dist/social-providers/linear.mjs +4 -4
  51. package/dist/social-providers/linkedin.d.mts +1 -5
  52. package/dist/social-providers/linkedin.mjs +10 -10
  53. package/dist/social-providers/microsoft-entra-id.d.mts +1 -5
  54. package/dist/social-providers/microsoft-entra-id.mjs +10 -11
  55. package/dist/social-providers/naver.d.mts +1 -5
  56. package/dist/social-providers/naver.mjs +4 -4
  57. package/dist/social-providers/notion.d.mts +1 -5
  58. package/dist/social-providers/notion.mjs +4 -4
  59. package/dist/social-providers/paybin.d.mts +1 -5
  60. package/dist/social-providers/paybin.mjs +10 -10
  61. package/dist/social-providers/paypal.d.mts +1 -5
  62. package/dist/social-providers/paypal.mjs +2 -8
  63. package/dist/social-providers/polar.d.mts +1 -5
  64. package/dist/social-providers/polar.mjs +8 -8
  65. package/dist/social-providers/railway.d.mts +1 -5
  66. package/dist/social-providers/railway.mjs +9 -9
  67. package/dist/social-providers/reddit.d.mts +1 -5
  68. package/dist/social-providers/reddit.mjs +5 -5
  69. package/dist/social-providers/roblox.d.mts +1 -5
  70. package/dist/social-providers/roblox.mjs +5 -5
  71. package/dist/social-providers/salesforce.d.mts +1 -5
  72. package/dist/social-providers/salesforce.mjs +8 -8
  73. package/dist/social-providers/slack.d.mts +1 -5
  74. package/dist/social-providers/slack.mjs +9 -9
  75. package/dist/social-providers/spotify.d.mts +1 -5
  76. package/dist/social-providers/spotify.mjs +5 -5
  77. package/dist/social-providers/tiktok.d.mts +1 -5
  78. package/dist/social-providers/tiktok.mjs +5 -9
  79. package/dist/social-providers/twitch.d.mts +1 -5
  80. package/dist/social-providers/twitch.mjs +4 -4
  81. package/dist/social-providers/twitter.d.mts +1 -5
  82. package/dist/social-providers/twitter.mjs +9 -9
  83. package/dist/social-providers/vercel.d.mts +1 -5
  84. package/dist/social-providers/vercel.mjs +7 -4
  85. package/dist/social-providers/vk.d.mts +1 -5
  86. package/dist/social-providers/vk.mjs +5 -5
  87. package/dist/social-providers/wechat.d.mts +1 -5
  88. package/dist/social-providers/wechat.mjs +5 -9
  89. package/dist/social-providers/zoom.d.mts +1 -6
  90. package/dist/social-providers/zoom.mjs +9 -15
  91. package/dist/types/context.d.mts +6 -2
  92. package/package.json +1 -1
  93. package/src/db/get-tables.ts +3 -8
  94. package/src/db/schema/account.ts +5 -14
  95. package/src/error/codes.ts +0 -5
  96. package/src/oauth2/create-authorization-url.ts +1 -1
  97. package/src/oauth2/index.ts +2 -12
  98. package/src/oauth2/oauth-provider.ts +11 -56
  99. package/src/oauth2/refresh-access-token.ts +3 -2
  100. package/src/oauth2/utils.ts +39 -1
  101. package/src/oauth2/verify-id-token.ts +7 -5
  102. package/src/social-providers/apple.ts +8 -13
  103. package/src/social-providers/atlassian.ts +8 -12
  104. package/src/social-providers/cognito.ts +11 -18
  105. package/src/social-providers/discord.ts +8 -19
  106. package/src/social-providers/dropbox.ts +7 -13
  107. package/src/social-providers/facebook.ts +9 -13
  108. package/src/social-providers/figma.ts +9 -13
  109. package/src/social-providers/github.ts +8 -12
  110. package/src/social-providers/gitlab.ts +8 -14
  111. package/src/social-providers/google.ts +23 -29
  112. package/src/social-providers/huggingface.ts +8 -12
  113. package/src/social-providers/kakao.ts +8 -16
  114. package/src/social-providers/kick.ts +7 -12
  115. package/src/social-providers/line.ts +10 -14
  116. package/src/social-providers/linear.ts +6 -12
  117. package/src/social-providers/linkedin.ts +10 -14
  118. package/src/social-providers/microsoft-entra-id.ts +8 -18
  119. package/src/social-providers/naver.ts +6 -12
  120. package/src/social-providers/notion.ts +6 -12
  121. package/src/social-providers/paybin.ts +11 -14
  122. package/src/social-providers/paypal.ts +8 -6
  123. package/src/social-providers/polar.ts +8 -12
  124. package/src/social-providers/railway.ts +9 -13
  125. package/src/social-providers/reddit.ts +7 -18
  126. package/src/social-providers/roblox.ts +7 -18
  127. package/src/social-providers/salesforce.ts +8 -12
  128. package/src/social-providers/slack.ts +9 -18
  129. package/src/social-providers/spotify.ts +7 -13
  130. package/src/social-providers/tiktok.ts +7 -13
  131. package/src/social-providers/twitch.ts +8 -12
  132. package/src/social-providers/twitter.ts +8 -17
  133. package/src/social-providers/vercel.ts +10 -16
  134. package/src/social-providers/vk.ts +7 -13
  135. package/src/social-providers/wechat.ts +8 -20
  136. package/src/social-providers/zoom.ts +6 -19
  137. package/src/types/context.ts +8 -2
  138. package/dist/oauth2/scopes.d.mts +0 -76
  139. package/dist/oauth2/scopes.mjs +0 -96
  140. package/src/oauth2/scopes.ts +0 -118
@@ -10,7 +10,7 @@ import { BetterAuthOptions, BetterAuthRateLimitOptions, UserProvisioningSource }
10
10
  import { Account } from "../db/schema/account.mjs";
11
11
  import { BetterAuthCookie, BetterAuthCookies } from "./cookie.mjs";
12
12
  import { SecretConfig } from "./secret.mjs";
13
- import { UpstreamProvider } from "../oauth2/oauth-provider.mjs";
13
+ import { OAuthProvider } from "../oauth2/oauth-provider.mjs";
14
14
  import { CookieOptions, EndpointContext } from "better-call";
15
15
 
16
16
  //#region src/types/context.d.ts
@@ -54,6 +54,10 @@ type GenericEndpointContext<Options extends BetterAuthOptions = BetterAuthOption
54
54
  context: AuthContext<Options>;
55
55
  };
56
56
  interface InternalAdapter<_Options extends BetterAuthOptions = BetterAuthOptions> {
57
+ createOAuthUser(user: Omit<User, "id" | "createdAt" | "updatedAt">, account: Omit<Account, "userId" | "id" | "createdAt" | "updatedAt"> & Partial<Account>): Promise<{
58
+ user: User;
59
+ account: Account;
60
+ }>;
57
61
  createUser<T extends Record<string, any>>(user: Omit<User, "id" | "createdAt" | "updatedAt" | "emailVerified"> & Partial<User> & Record<string, any>,
58
62
  /**
59
63
  * Provisioning source. The creation seam adds `action: "create-user"` and
@@ -232,7 +236,7 @@ type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> = Plugin
232
236
  session: Session & Record<string, any>;
233
237
  user: User & Record<string, any>;
234
238
  } | null) => void;
235
- socialProviders: UpstreamProvider[];
239
+ socialProviders: OAuthProvider[];
236
240
  authCookies: BetterAuthCookies;
237
241
  logger: ReturnType<typeof createLogger>;
238
242
  rateLimit: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.7.0-beta.7",
3
+ "version": "1.7.0-beta.8",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -261,15 +261,10 @@ export const getAuthTables = (
261
261
  options.account?.fields?.refreshTokenExpiresAt ||
262
262
  "refreshTokenExpiresAt",
263
263
  },
264
- // Renamed from the legacy `scope` column. The migration generator
265
- // only adds this column; it does not transform the legacy `scope`
266
- // value. Upgrading installs need a manual data migration (split
267
- // legacy `scope` on comma/space, trim, drop empties, dedupe). Order
268
- // is insignificant per RFC 6749 §3.3.
269
- grantedScopes: {
270
- type: "string[]",
264
+ scope: {
265
+ type: "string",
271
266
  required: false,
272
- fieldName: options.account?.fields?.grantedScopes || "grantedScopes",
267
+ fieldName: options.account?.fields?.scope || "scope",
273
268
  },
274
269
  password: {
275
270
  type: "string",
@@ -23,21 +23,12 @@ export const accountSchema = coreSchema.extend({
23
23
  */
24
24
  refreshTokenExpiresAt: z.date().nullish(),
25
25
  /**
26
- * The scopes the user has granted, as last observed (durable, per-account,
27
- * the unit of revocation and the refresh ceiling). A native array, not a
28
- * delimited string: scope order is insignificant per RFC 6749 §3.3, so the
29
- * value is normalized (trimmed, deduped, sorted) on write.
30
- *
31
- * Renamed from the legacy comma-joined `scope` string. Breaking, with no
32
- * automatic data migration (and no read-time shim): the migration generator
33
- * only adds the new `grantedScopes` column, so legacy accounts read as empty
34
- * here until an upgrade backfills `grantedScopes` from the old `scope` values
35
- * (split on comma/space, trim, drop empties, dedupe). See the release
36
- * changeset for the backfill.
37
- *
38
- * @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
26
+ * The set of OAuth scopes the user has granted to this account, stored
27
+ * as a comma-separated list. Represents the accumulated grant rather
28
+ * than the latest token's `scope` claim, since per RFC 6749 Section 1.5 a
29
+ * token's scope may be narrower than the user's grant.
39
30
  */
40
- grantedScopes: z.array(z.string()).nullish(),
31
+ scope: z.string().nullish(),
41
32
  /**
42
33
  * Password is only stored in the credential provider
43
34
  */
@@ -29,11 +29,6 @@ export const BASE_ERROR_CODES = defineErrorCodes({
29
29
  TOKEN_EXPIRED: "Token expired",
30
30
  ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
31
31
  FAILED_TO_GET_USER_INFO: "Failed to get user info",
32
- PROVIDER_NOT_SUPPORTED: "Provider not supported",
33
- TOKEN_REFRESH_NOT_SUPPORTED: "Token refresh not supported",
34
- REFRESH_TOKEN_NOT_FOUND: "Refresh token not found",
35
- FAILED_TO_GET_ACCESS_TOKEN: "Failed to get a valid access token",
36
- FAILED_TO_REFRESH_ACCESS_TOKEN: "Failed to refresh access token",
37
32
  USER_EMAIL_NOT_FOUND: "User email not found",
38
33
  EMAIL_NOT_VERIFIED: "Email not verified",
39
34
  PASSWORD_TOO_SHORT: "Password too short",
@@ -111,5 +111,5 @@ export async function createAuthorizationURL({
111
111
  url.searchParams.set(key, value);
112
112
  }
113
113
  }
114
- return { url, requestedScopes: scopes ?? [] };
114
+ return url;
115
115
  }
@@ -63,28 +63,17 @@ export {
63
63
  verifyDpopProof,
64
64
  } from "./dpop";
65
65
  export type {
66
- AuthorizationURLResult,
67
- GrantAuthority,
68
66
  OAuth2Tokens,
69
67
  OAuth2UserInfo,
70
68
  OAuthIdTokenConfig,
69
+ OAuthProvider,
71
70
  OAuthRefreshContext,
72
- ProviderGrantAuthority,
73
71
  ProviderOptions,
74
- UpstreamProvider,
75
72
  } from "./oauth-provider";
76
73
  export {
77
74
  refreshAccessToken,
78
75
  refreshAccessTokenRequest,
79
76
  } from "./refresh-access-token";
80
- export {
81
- includesGrantedScope,
82
- normalizeScopes,
83
- parseScopeField,
84
- readGrantedScopes,
85
- resolveRequestedScopes,
86
- unionGrantedScopes,
87
- } from "./scopes";
88
77
  export type {
89
78
  TokenEndpointAuth,
90
79
  TokenEndpointAuthMethod,
@@ -95,6 +84,7 @@ export {
95
84
  generateCodeChallenge,
96
85
  getOAuth2Tokens,
97
86
  getPrimaryClientId,
87
+ mergeScopes,
98
88
  } from "./utils";
99
89
  export {
100
90
  authorizationCodeRequest,
@@ -89,64 +89,22 @@ export interface OAuthRefreshContext {
89
89
  request?: Request | undefined;
90
90
  }
91
91
 
92
- /**
93
- * The result of building a provider authorization URL.
94
- *
95
- * `requestedScopes` is the effective set of scopes encoded in the URL (the
96
- * provider's built-in defaults + configured `options.scope` + per-request
97
- * `scopes`, composed by `resolveRequestedScopes`). Callers persist it so the
98
- * callback can fall back to the request when the provider omits `scope` from
99
- * its token response (RFC 6749 §5.1).
100
- */
101
- export interface AuthorizationURLResult {
102
- url: URL;
103
- requestedScopes: string[];
104
- }
105
-
106
- /**
107
- * How much an RP trusts a provider's echoed token-response `scope` when
108
- * persisting `account.grantedScopes`.
109
- *
110
- * - `"full-grant"`: the echo is the user's complete current grant, so the seam
111
- * replaces the stored grant with it. This is the only path that may narrow
112
- * the grant. Declare it only for providers whose token response reports the
113
- * full combined grant, e.g. Google with `include_granted_scopes`.
114
- * - `"projection"`: the echo is this request's subset, so the seam unions it
115
- * onto the stored grant. The safe default for every provider.
116
- * - `"absent-echo"`: the provider omitted `scope`, so the grant equals what was
117
- * requested (RFC 6749 §5.1) and the seam unions the requested set. Resolved
118
- * at runtime by the persistence seam, never declared by a provider.
119
- *
120
- * @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
121
- */
122
- export type GrantAuthority = "full-grant" | "projection" | "absent-echo";
123
-
124
- /**
125
- * The authority a provider may declare for its own echoed scope. `"absent-echo"`
126
- * is excluded because it is a runtime condition (an omitted echo), not a
127
- * provider trait.
128
- */
129
- export type ProviderGrantAuthority = Exclude<GrantAuthority, "absent-echo">;
130
-
131
- export interface UpstreamProvider<
92
+ export interface OAuthProvider<
132
93
  T extends Record<string, any> = Record<string, any>,
133
94
  O extends Record<string, any> = Partial<ProviderOptions>,
134
95
  > {
135
96
  id: LiteralString;
136
97
  /**
137
- * The path the provider redirects back to, relative to the app base URL,
138
- * e.g. `/callback/google`.
139
- */
140
- callbackPath: string;
141
- /**
142
- * How the persistence seam treats this provider's echoed token-response
143
- * `scope`. Declare `"full-grant"` only when the echo is the user's complete
144
- * current grant (e.g. Google with `include_granted_scopes`); otherwise the
145
- * echo is unioned onto the stored grant.
98
+ * Optional path under the resolved per-request `baseURL` where this
99
+ * provider's OAuth callback handler is mounted. Providers that use the
100
+ * shared `/callback/<id>` route can omit this.
101
+ *
102
+ * Custom paths must start with `/`.
146
103
  *
147
- * @default "projection"
104
+ * Endpoints compose `redirectURI = ctx.context.baseURL + callbackPath` per
105
+ * request, so the provider must not hardcode an origin or `baseURL` here.
148
106
  */
149
- grantAuthority?: ProviderGrantAuthority | undefined;
107
+ callbackPath?: string | undefined;
150
108
  createAuthorizationURL: (data: {
151
109
  state: string;
152
110
  codeVerifier: string;
@@ -167,7 +125,7 @@ export interface UpstreamProvider<
167
125
  * before applying them.
168
126
  */
169
127
  additionalParams?: Record<string, string> | undefined;
170
- }) => Awaitable<AuthorizationURLResult>;
128
+ }) => Awaitable<URL>;
171
129
  name: string;
172
130
  validateAuthorizationCode: (data: {
173
131
  code: string;
@@ -214,6 +172,7 @@ export interface UpstreamProvider<
214
172
  ctx?: OAuthRefreshContext,
215
173
  ) => Promise<OAuth2Tokens>)
216
174
  | undefined;
175
+ revokeToken?: ((token: string) => Promise<void>) | undefined;
217
176
  /**
218
177
  * Declarative id_token verification config consumed by the shared
219
178
  * `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
@@ -321,10 +280,6 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
321
280
  emailVerified: boolean;
322
281
  [key: string]: any;
323
282
  };
324
- // TODO: type as `Profile` once provider getUserInfo paths that return a
325
- // narrower data shape than their declared profile are reconciled; today
326
- // `any` is load-bearing for those (e.g. facebook) and tightening it ripples
327
- // across ~10 providers, out of scope for the grant refactor.
328
283
  data: any;
329
284
  } | null>)
330
285
  | undefined;
@@ -6,6 +6,7 @@ import type {
6
6
  TokenEndpointSecretAuthentication,
7
7
  } from "./token-endpoint-auth";
8
8
  import { applyTokenEndpointAuth } from "./token-endpoint-auth";
9
+ import { parseScopeField } from "./utils";
9
10
 
10
11
  interface RefreshAccessTokenRequestInput {
11
12
  refreshToken: string;
@@ -143,7 +144,7 @@ export async function refreshAccessToken({
143
144
  expires_in?: number | undefined;
144
145
  refresh_token_expires_in?: number | undefined;
145
146
  token_type?: string | undefined;
146
- scope?: (string | string[]) | undefined;
147
+ scope?: unknown;
147
148
  id_token?: string | undefined;
148
149
  }>(tokenEndpoint, {
149
150
  method: "POST",
@@ -157,7 +158,7 @@ export async function refreshAccessToken({
157
158
  accessToken: data.access_token,
158
159
  refreshToken: data.refresh_token,
159
160
  tokenType: data.token_type,
160
- scopes: Array.isArray(data.scope) ? data.scope : data.scope?.split(" "),
161
+ scopes: parseScopeField(data.scope),
161
162
  idToken: data.id_token,
162
163
  };
163
164
 
@@ -1,6 +1,26 @@
1
1
  import { base64Url } from "@better-auth/utils/base64";
2
2
  import type { OAuth2Tokens } from "./oauth-provider";
3
- import { parseScopeField } from "./scopes";
3
+
4
+ /**
5
+ * Parse a provider's `scope` token-response field into a string array.
6
+ *
7
+ * RFC 6749 Section 3.3 defines `scope` as a space-delimited string, but
8
+ * providers vary: some return an already-split array. Accept both forms and
9
+ * drop empty or non-string entries.
10
+ *
11
+ * @see https://github.com/better-auth/better-auth/issues/9076
12
+ */
13
+ export function parseScopeField(scope: unknown): string[] {
14
+ if (Array.isArray(scope)) {
15
+ return scope
16
+ .map((s) => (typeof s === "string" ? s.trim() : ""))
17
+ .filter(Boolean);
18
+ }
19
+ if (typeof scope === "string") {
20
+ return scope.trim().split(/\s+/).filter(Boolean);
21
+ }
22
+ return [];
23
+ }
4
24
 
5
25
  export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
6
26
  const getDate = (seconds: number) => {
@@ -44,6 +64,24 @@ export function applyDefaultAccessTokenExpiry(
44
64
  return tokens;
45
65
  }
46
66
 
67
+ /**
68
+ * Compute the union of stored and incoming OAuth scopes, preserving
69
+ * stored insertion order and dropping duplicates.
70
+ */
71
+ export function mergeScopes(
72
+ stored: string | null | undefined,
73
+ incoming: string[] | undefined,
74
+ ): string {
75
+ const existing = stored
76
+ ? stored
77
+ .split(",")
78
+ .map((scope) => scope.trim())
79
+ .filter(Boolean)
80
+ : [];
81
+ const next = (incoming ?? []).map((scope) => scope.trim()).filter(Boolean);
82
+ return [...new Set([...existing, ...next])].join(",");
83
+ }
84
+
47
85
  /**
48
86
  * Return the provider's primary Client ID: the single string, or the entry at
49
87
  * array index 0 for the cross-platform form used by ID token audience
@@ -1,5 +1,7 @@
1
1
  import { decodeProtectedHeader, jwtVerify } from "jose";
2
- import type { ProviderOptions, UpstreamProvider } from "./oauth-provider";
2
+ import type { OAuthProvider, ProviderOptions } from "./oauth-provider";
3
+
4
+ type ProviderWithIdTokenConfig = Pick<OAuthProvider, "idToken" | "options">;
3
5
 
4
6
  async function sha256Hex(value: string) {
5
7
  const data = new TextEncoder().encode(value);
@@ -29,12 +31,12 @@ async function nonceMatches(
29
31
  /**
30
32
  * Whether a provider can verify a client-submitted id_token.
31
33
  *
32
- * A provider supports id_token sign-in when it declares an {@link UpstreamProvider.idToken}
34
+ * A provider supports id_token sign-in when it declares an {@link OAuthProvider.idToken}
33
35
  * verification config, or when the integrator supplies a `verifyIdToken` override on the
34
36
  * provider options. A provider whose options set `disableIdTokenSignIn`, or that declares
35
37
  * neither, rejects the client id_token sign-in path with `ID_TOKEN_NOT_SUPPORTED`.
36
38
  */
37
- export function supportsIdTokenSignIn(provider: UpstreamProvider<any, any>) {
39
+ export function supportsIdTokenSignIn(provider: ProviderWithIdTokenConfig) {
38
40
  const options = (provider.options ?? {}) as Partial<ProviderOptions>;
39
41
  if (options.disableIdTokenSignIn) {
40
42
  return false;
@@ -46,7 +48,7 @@ export function supportsIdTokenSignIn(provider: UpstreamProvider<any, any>) {
46
48
  * Verify a client-submitted id_token against a provider's verification config.
47
49
  *
48
50
  * This is the single id_token verifier for every social provider. Providers no longer
49
- * implement their own boolean `verifyIdToken`; they declare an {@link UpstreamProvider.idToken}
51
+ * implement their own boolean `verifyIdToken`; they declare an {@link OAuthProvider.idToken}
50
52
  * config and this function performs the cryptographic check. The contract is fail-closed: a
51
53
  * provider without a config (and without an integrator `verifyIdToken` override) returns
52
54
  * `false`, so a forged token can never be accepted by omission.
@@ -54,7 +56,7 @@ export function supportsIdTokenSignIn(provider: UpstreamProvider<any, any>) {
54
56
  * @returns `true` only when the token is authentic for the provider.
55
57
  */
56
58
  export async function verifyProviderIdToken(
57
- provider: UpstreamProvider<any, any>,
59
+ provider: ProviderWithIdTokenConfig,
58
60
  token: string,
59
61
  nonce?: string,
60
62
  ): Promise<boolean> {
@@ -3,12 +3,11 @@ import { betterFetch } from "@better-fetch/fetch";
3
3
  import { decodeJwt, importJWK } from "jose";
4
4
  import { logger } from "../env";
5
5
  import { APIError, BetterAuthError } from "../error";
6
- import type { ProviderOptions, UpstreamProvider } from "../oauth2";
6
+ import type { OAuthProvider, ProviderOptions } from "../oauth2";
7
7
  import {
8
8
  createAuthorizationURL,
9
9
  getPrimaryClientId,
10
10
  refreshAccessToken,
11
- resolveRequestedScopes,
12
11
  validateAuthorizationCode,
13
12
  } from "../oauth2";
14
13
  export interface AppleProfile {
@@ -78,14 +77,11 @@ export interface AppleOptions extends ProviderOptions<AppleProfile> {
78
77
  audience?: (string | string[]) | undefined;
79
78
  }
80
79
 
81
- const APPLE_DEFAULT_SCOPES = ["email", "name"];
82
-
83
80
  export const apple = (options: AppleOptions) => {
84
81
  const tokenEndpoint = "https://appleid.apple.com/auth/token";
85
82
  return {
86
83
  id: "apple",
87
84
  name: "Apple",
88
- callbackPath: "/callback/apple",
89
85
  async createAuthorizationURL({
90
86
  state,
91
87
  scopes,
@@ -98,22 +94,21 @@ export const apple = (options: AppleOptions) => {
98
94
  );
99
95
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
100
96
  }
101
- const requestedScopes = resolveRequestedScopes(
102
- options,
103
- APPLE_DEFAULT_SCOPES,
104
- scopes,
105
- );
106
- return createAuthorizationURL({
97
+ const _scope = options.disableDefaultScope ? [] : ["email", "name"];
98
+ if (options.scope) _scope.push(...options.scope);
99
+ if (scopes) _scope.push(...scopes);
100
+ const url = await createAuthorizationURL({
107
101
  id: "apple",
108
102
  options,
109
103
  authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
110
- scopes: requestedScopes,
104
+ scopes: _scope,
111
105
  state,
112
106
  redirectURI,
113
107
  responseMode: "form_post",
114
108
  responseType: "code id_token",
115
109
  additionalParams,
116
110
  });
111
+ return url;
117
112
  },
118
113
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
119
114
  return validateAuthorizationCode({
@@ -189,7 +184,7 @@ export const apple = (options: AppleOptions) => {
189
184
  };
190
185
  },
191
186
  options,
192
- } satisfies UpstreamProvider<AppleProfile>;
187
+ } satisfies OAuthProvider<AppleProfile>;
193
188
  };
194
189
 
195
190
  export const getApplePublicKey = async (kid: string) => {
@@ -1,11 +1,10 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
2
  import { logger } from "../env";
3
3
  import { BetterAuthError } from "../error";
4
- import type { ProviderOptions, UpstreamProvider } from "../oauth2";
4
+ import type { OAuthProvider, ProviderOptions } from "../oauth2";
5
5
  import {
6
6
  createAuthorizationURL,
7
7
  refreshAccessToken,
8
- resolveRequestedScopes,
9
8
  validateAuthorizationCode,
10
9
  } from "../oauth2";
11
10
 
@@ -30,14 +29,11 @@ export interface AtlassianOptions extends ProviderOptions<AtlassianProfile> {
30
29
  clientId: string;
31
30
  }
32
31
 
33
- const ATLASSIAN_DEFAULT_SCOPES = ["read:jira-user", "offline_access"];
34
-
35
32
  export const atlassian = (options: AtlassianOptions) => {
36
33
  const tokenEndpoint = "https://auth.atlassian.com/oauth/token";
37
34
  return {
38
35
  id: "atlassian",
39
36
  name: "Atlassian",
40
- callbackPath: "/callback/atlassian",
41
37
 
42
38
  async createAuthorizationURL({
43
39
  state,
@@ -54,17 +50,17 @@ export const atlassian = (options: AtlassianOptions) => {
54
50
  throw new BetterAuthError("codeVerifier is required for Atlassian");
55
51
  }
56
52
 
57
- const requestedScopes = resolveRequestedScopes(
58
- options,
59
- ATLASSIAN_DEFAULT_SCOPES,
60
- scopes,
61
- );
53
+ const _scopes = options.disableDefaultScope
54
+ ? []
55
+ : ["read:jira-user", "offline_access"];
56
+ if (options.scope) _scopes.push(...options.scope);
57
+ if (scopes) _scopes.push(...scopes);
62
58
 
63
59
  return createAuthorizationURL({
64
60
  id: "atlassian",
65
61
  options,
66
62
  authorizationEndpoint: "https://auth.atlassian.com/authorize",
67
- scopes: requestedScopes,
63
+ scopes: _scopes,
68
64
  state,
69
65
  codeVerifier,
70
66
  redirectURI,
@@ -140,5 +136,5 @@ export const atlassian = (options: AtlassianOptions) => {
140
136
  },
141
137
 
142
138
  options,
143
- } satisfies UpstreamProvider<AtlassianProfile>;
139
+ } satisfies OAuthProvider<AtlassianProfile>;
144
140
  };
@@ -2,12 +2,11 @@ import { betterFetch } from "@better-fetch/fetch";
2
2
  import { decodeJwt, importJWK } from "jose";
3
3
  import { logger } from "../env";
4
4
  import { APIError, BetterAuthError } from "../error";
5
- import type { ProviderOptions, UpstreamProvider } from "../oauth2";
5
+ import type { OAuthProvider, ProviderOptions } from "../oauth2";
6
6
  import {
7
7
  createAuthorizationURL,
8
8
  getPrimaryClientId,
9
9
  refreshAccessToken,
10
- resolveRequestedScopes,
11
10
  validateAuthorizationCode,
12
11
  } from "../oauth2";
13
12
 
@@ -58,8 +57,6 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
58
57
  identityProvider?: string | undefined;
59
58
  }
60
59
 
61
- const COGNITO_DEFAULT_SCOPES = ["openid", "profile", "email"];
62
-
63
60
  export const cognito = (options: CognitoOptions) => {
64
61
  if (!options.domain || !options.region || !options.userPoolId) {
65
62
  logger.error(
@@ -76,7 +73,6 @@ export const cognito = (options: CognitoOptions) => {
76
73
  return {
77
74
  id: "cognito",
78
75
  name: "Cognito",
79
- callbackPath: "/callback/cognito",
80
76
  async createAuthorizationURL({
81
77
  state,
82
78
  scopes,
@@ -97,19 +93,19 @@ export const cognito = (options: CognitoOptions) => {
97
93
  );
98
94
  throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
99
95
  }
100
- const requestedScopes = resolveRequestedScopes(
101
- options,
102
- COGNITO_DEFAULT_SCOPES,
103
- scopes,
104
- );
96
+ const _scopes = options.disableDefaultScope
97
+ ? []
98
+ : ["openid", "profile", "email"];
99
+ if (options.scope) _scopes.push(...options.scope);
100
+ if (scopes) _scopes.push(...scopes);
105
101
 
106
- const { url } = await createAuthorizationURL({
102
+ const url = await createAuthorizationURL({
107
103
  id: "cognito",
108
104
  options: {
109
105
  ...options,
110
106
  },
111
107
  authorizationEndpoint,
112
- scopes: requestedScopes,
108
+ scopes: _scopes,
113
109
  state,
114
110
  codeVerifier,
115
111
  redirectURI,
@@ -130,12 +126,9 @@ export const cognito = (options: CognitoOptions) => {
130
126
  // Manually append the scope with proper encoding to the URL
131
127
  const urlString = url.toString();
132
128
  const separator = urlString.includes("?") ? "&" : "?";
133
- return {
134
- url: new URL(`${urlString}${separator}scope=${encodedScope}`),
135
- requestedScopes,
136
- };
129
+ return new URL(`${urlString}${separator}scope=${encodedScope}`);
137
130
  }
138
- return { url, requestedScopes };
131
+ return url;
139
132
  },
140
133
 
141
134
  validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -243,7 +236,7 @@ export const cognito = (options: CognitoOptions) => {
243
236
  },
244
237
 
245
238
  options,
246
- } satisfies UpstreamProvider<CognitoProfile>;
239
+ } satisfies OAuthProvider<CognitoProfile>;
247
240
  };
248
241
 
249
242
  export const getCognitoPublicKey = async (
@@ -1,9 +1,8 @@
1
1
  import { betterFetch } from "@better-fetch/fetch";
2
- import type { ProviderOptions, UpstreamProvider } from "../oauth2";
2
+ import type { OAuthProvider, ProviderOptions } from "../oauth2";
3
3
  import {
4
4
  createAuthorizationURL,
5
5
  refreshAccessToken,
6
- resolveRequestedScopes,
7
6
  validateAuthorizationCode,
8
7
  } from "../oauth2";
9
8
  export interface DiscordProfile extends Record<string, any> {
@@ -84,31 +83,21 @@ export interface DiscordOptions extends ProviderOptions<DiscordProfile> {
84
83
  permissions?: number | undefined;
85
84
  }
86
85
 
87
- const DISCORD_DEFAULT_SCOPES = ["identify", "email"];
88
-
89
86
  export const discord = (options: DiscordOptions) => {
90
87
  const tokenEndpoint = "https://discord.com/api/oauth2/token";
91
88
  return {
92
89
  id: "discord",
93
90
  name: "Discord",
94
- callbackPath: "/callback/discord",
95
- async createAuthorizationURL({
96
- state,
97
- scopes,
98
- redirectURI,
99
- additionalParams,
100
- }) {
101
- const requestedScopes = resolveRequestedScopes(
102
- options,
103
- DISCORD_DEFAULT_SCOPES,
104
- scopes,
105
- );
106
- const hasBotScope = requestedScopes.includes("bot");
91
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
92
+ const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
93
+ if (scopes) _scopes.push(...scopes);
94
+ if (options.scope) _scopes.push(...options.scope);
95
+ const hasBotScope = _scopes.includes("bot");
107
96
  return createAuthorizationURL({
108
97
  id: "discord",
109
98
  options,
110
99
  authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
111
- scopes: requestedScopes,
100
+ scopes: _scopes,
112
101
  state,
113
102
  redirectURI,
114
103
  prompt: options.prompt || "none",
@@ -181,5 +170,5 @@ export const discord = (options: DiscordOptions) => {
181
170
  };
182
171
  },
183
172
  options,
184
- } satisfies UpstreamProvider<DiscordProfile>;
173
+ } satisfies OAuthProvider<DiscordProfile>;
185
174
  };