@better-auth/core 1.7.0-beta.2 → 1.7.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/context/global.mjs +1 -1
  2. package/dist/db/adapter/factory.mjs +64 -3
  3. package/dist/db/adapter/index.d.mts +35 -1
  4. package/dist/db/adapter/types.d.mts +1 -1
  5. package/dist/db/type.d.mts +12 -0
  6. package/dist/error/codes.d.mts +1 -0
  7. package/dist/error/codes.mjs +1 -0
  8. package/dist/instrumentation/tracer.mjs +1 -1
  9. package/dist/oauth2/authorization-params.d.mts +12 -0
  10. package/dist/oauth2/authorization-params.mjs +12 -0
  11. package/dist/oauth2/basic-credentials.d.mts +30 -0
  12. package/dist/oauth2/basic-credentials.mjs +64 -0
  13. package/dist/oauth2/client-assertion.d.mts +38 -22
  14. package/dist/oauth2/client-assertion.mjs +63 -28
  15. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  16. package/dist/oauth2/client-credentials-token.mjs +18 -29
  17. package/dist/oauth2/create-authorization-url.d.mts +9 -1
  18. package/dist/oauth2/create-authorization-url.mjs +23 -5
  19. package/dist/oauth2/index.d.mts +10 -7
  20. package/dist/oauth2/index.mjs +9 -7
  21. package/dist/oauth2/oauth-provider.d.mts +21 -2
  22. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  23. package/dist/oauth2/refresh-access-token.mjs +19 -32
  24. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  25. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  26. package/dist/oauth2/utils.d.mts +9 -1
  27. package/dist/oauth2/utils.mjs +12 -1
  28. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  29. package/dist/oauth2/validate-authorization-code.mjs +17 -30
  30. package/dist/oauth2/verify.mjs +15 -5
  31. package/dist/social-providers/apple.d.mts +5 -12
  32. package/dist/social-providers/apple.mjs +14 -3
  33. package/dist/social-providers/atlassian.d.mts +3 -1
  34. package/dist/social-providers/atlassian.mjs +5 -2
  35. package/dist/social-providers/cognito.d.mts +16 -1
  36. package/dist/social-providers/cognito.mjs +6 -2
  37. package/dist/social-providers/discord.d.mts +5 -3
  38. package/dist/social-providers/discord.mjs +16 -3
  39. package/dist/social-providers/dropbox.d.mts +3 -1
  40. package/dist/social-providers/dropbox.mjs +5 -4
  41. package/dist/social-providers/facebook.d.mts +5 -3
  42. package/dist/social-providers/facebook.mjs +6 -3
  43. package/dist/social-providers/figma.d.mts +3 -1
  44. package/dist/social-providers/figma.mjs +3 -2
  45. package/dist/social-providers/github.d.mts +4 -2
  46. package/dist/social-providers/github.mjs +5 -4
  47. package/dist/social-providers/gitlab.d.mts +3 -1
  48. package/dist/social-providers/gitlab.mjs +3 -2
  49. package/dist/social-providers/google.d.mts +3 -1
  50. package/dist/social-providers/google.mjs +5 -2
  51. package/dist/social-providers/huggingface.d.mts +3 -1
  52. package/dist/social-providers/huggingface.mjs +3 -2
  53. package/dist/social-providers/index.d.mts +104 -36
  54. package/dist/social-providers/kakao.d.mts +3 -1
  55. package/dist/social-providers/kakao.mjs +3 -2
  56. package/dist/social-providers/kick.d.mts +3 -1
  57. package/dist/social-providers/kick.mjs +3 -2
  58. package/dist/social-providers/line.d.mts +3 -1
  59. package/dist/social-providers/line.mjs +3 -2
  60. package/dist/social-providers/linear.d.mts +3 -1
  61. package/dist/social-providers/linear.mjs +3 -2
  62. package/dist/social-providers/linkedin.d.mts +5 -3
  63. package/dist/social-providers/linkedin.mjs +4 -3
  64. package/dist/social-providers/microsoft-entra-id.d.mts +3 -2
  65. package/dist/social-providers/microsoft-entra-id.mjs +3 -2
  66. package/dist/social-providers/naver.d.mts +3 -1
  67. package/dist/social-providers/naver.mjs +3 -2
  68. package/dist/social-providers/notion.d.mts +3 -1
  69. package/dist/social-providers/notion.mjs +5 -2
  70. package/dist/social-providers/paybin.d.mts +3 -1
  71. package/dist/social-providers/paybin.mjs +3 -2
  72. package/dist/social-providers/paypal.d.mts +3 -1
  73. package/dist/social-providers/paypal.mjs +4 -3
  74. package/dist/social-providers/polar.d.mts +3 -1
  75. package/dist/social-providers/polar.mjs +3 -2
  76. package/dist/social-providers/railway.d.mts +3 -1
  77. package/dist/social-providers/railway.mjs +3 -2
  78. package/dist/social-providers/reddit.d.mts +3 -1
  79. package/dist/social-providers/reddit.mjs +3 -2
  80. package/dist/social-providers/roblox.d.mts +4 -2
  81. package/dist/social-providers/roblox.mjs +12 -2
  82. package/dist/social-providers/salesforce.d.mts +3 -1
  83. package/dist/social-providers/salesforce.mjs +3 -2
  84. package/dist/social-providers/slack.d.mts +4 -2
  85. package/dist/social-providers/slack.mjs +11 -8
  86. package/dist/social-providers/spotify.d.mts +3 -1
  87. package/dist/social-providers/spotify.mjs +3 -2
  88. package/dist/social-providers/tiktok.d.mts +3 -1
  89. package/dist/social-providers/tiktok.mjs +14 -2
  90. package/dist/social-providers/twitch.d.mts +3 -1
  91. package/dist/social-providers/twitch.mjs +3 -2
  92. package/dist/social-providers/twitter.d.mts +5 -2
  93. package/dist/social-providers/twitter.mjs +2 -1
  94. package/dist/social-providers/vercel.d.mts +3 -1
  95. package/dist/social-providers/vercel.mjs +3 -2
  96. package/dist/social-providers/vk.d.mts +3 -1
  97. package/dist/social-providers/vk.mjs +3 -2
  98. package/dist/social-providers/wechat.d.mts +3 -1
  99. package/dist/social-providers/wechat.mjs +7 -1
  100. package/dist/social-providers/zoom.d.mts +4 -2
  101. package/dist/social-providers/zoom.mjs +10 -17
  102. package/dist/types/context.d.mts +30 -4
  103. package/dist/types/init-options.d.mts +29 -5
  104. package/dist/utils/ip.d.mts +5 -4
  105. package/dist/utils/ip.mjs +3 -3
  106. package/dist/utils/redirect-uri.d.mts +20 -0
  107. package/dist/utils/redirect-uri.mjs +48 -0
  108. package/dist/utils/string.d.mts +5 -1
  109. package/dist/utils/string.mjs +20 -1
  110. package/dist/utils/url.d.mts +18 -1
  111. package/dist/utils/url.mjs +30 -1
  112. package/package.json +9 -8
  113. package/src/db/adapter/factory.ts +121 -3
  114. package/src/db/adapter/index.ts +32 -0
  115. package/src/db/adapter/types.ts +1 -0
  116. package/src/db/get-tables.ts +2 -0
  117. package/src/db/schema/user.ts +3 -0
  118. package/src/db/type.ts +12 -0
  119. package/src/error/codes.ts +1 -0
  120. package/src/oauth2/authorization-params.ts +28 -0
  121. package/src/oauth2/basic-credentials.ts +87 -0
  122. package/src/oauth2/client-assertion.ts +131 -58
  123. package/src/oauth2/client-credentials-token.ts +48 -72
  124. package/src/oauth2/create-authorization-url.ts +28 -6
  125. package/src/oauth2/index.ts +25 -9
  126. package/src/oauth2/oauth-provider.ts +21 -2
  127. package/src/oauth2/refresh-access-token.ts +50 -76
  128. package/src/oauth2/token-endpoint-auth.ts +221 -0
  129. package/src/oauth2/utils.ts +19 -0
  130. package/src/oauth2/validate-authorization-code.ts +55 -85
  131. package/src/oauth2/verify.ts +20 -4
  132. package/src/social-providers/apple.ts +27 -3
  133. package/src/social-providers/atlassian.ts +8 -1
  134. package/src/social-providers/cognito.ts +26 -1
  135. package/src/social-providers/discord.ts +22 -18
  136. package/src/social-providers/dropbox.ts +7 -5
  137. package/src/social-providers/facebook.ts +14 -9
  138. package/src/social-providers/figma.ts +8 -1
  139. package/src/social-providers/github.ts +5 -3
  140. package/src/social-providers/gitlab.ts +2 -0
  141. package/src/social-providers/google.ts +2 -0
  142. package/src/social-providers/huggingface.ts +8 -1
  143. package/src/social-providers/kakao.ts +2 -1
  144. package/src/social-providers/kick.ts +8 -1
  145. package/src/social-providers/line.ts +2 -0
  146. package/src/social-providers/linear.ts +8 -1
  147. package/src/social-providers/linkedin.ts +5 -3
  148. package/src/social-providers/microsoft-entra-id.ts +2 -1
  149. package/src/social-providers/naver.ts +2 -1
  150. package/src/social-providers/notion.ts +8 -1
  151. package/src/social-providers/paybin.ts +2 -0
  152. package/src/social-providers/paypal.ts +7 -1
  153. package/src/social-providers/polar.ts +8 -1
  154. package/src/social-providers/railway.ts +8 -1
  155. package/src/social-providers/reddit.ts +2 -1
  156. package/src/social-providers/roblox.ts +16 -11
  157. package/src/social-providers/salesforce.ts +8 -1
  158. package/src/social-providers/slack.ts +15 -9
  159. package/src/social-providers/spotify.ts +8 -1
  160. package/src/social-providers/tiktok.ts +22 -9
  161. package/src/social-providers/twitch.ts +2 -1
  162. package/src/social-providers/twitter.ts +1 -0
  163. package/src/social-providers/vercel.ts +8 -1
  164. package/src/social-providers/vk.ts +8 -1
  165. package/src/social-providers/wechat.ts +9 -1
  166. package/src/social-providers/zoom.ts +15 -19
  167. package/src/types/context.ts +33 -5
  168. package/src/types/init-options.ts +29 -5
  169. package/src/utils/ip.ts +12 -13
  170. package/src/utils/redirect-uri.ts +54 -0
  171. package/src/utils/string.ts +37 -0
  172. package/src/utils/url.ts +28 -0
@@ -2,7 +2,7 @@
2
2
  const symbol = Symbol.for("better-auth:global");
3
3
  let bind = null;
4
4
  const __context = {};
5
- const __betterAuthVersion = "1.7.0-beta.2";
5
+ const __betterAuthVersion = "1.7.0-beta.4";
6
6
  /**
7
7
  * We store context instance in the globalThis.
8
8
  *
@@ -1,9 +1,7 @@
1
1
  import { BetterAuthError } from "../../error/index.mjs";
2
- import { getAuthTables } from "../get-tables.mjs";
3
2
  import { getColorDepth } from "../../env/color-depth.mjs";
4
3
  import { TTY_COLORS, createLogger } from "../../env/logger.mjs";
5
- import { ATTR_DB_COLLECTION_NAME, ATTR_DB_OPERATION_NAME } from "../../instrumentation/attributes.mjs";
6
- import { withSpan } from "../../instrumentation/tracer.mjs";
4
+ import { getAuthTables } from "../get-tables.mjs";
7
5
  import { safeJSONParse } from "../../utils/json.mjs";
8
6
  import { initGetDefaultModelName } from "./get-default-model-name.mjs";
9
7
  import { initGetDefaultFieldName } from "./get-default-field-name.mjs";
@@ -12,6 +10,7 @@ import { initGetFieldAttributes } from "./get-field-attributes.mjs";
12
10
  import { initGetFieldName } from "./get-field-name.mjs";
13
11
  import { initGetModelName } from "./get-model-name.mjs";
14
12
  import { withApplyDefault } from "./utils.mjs";
13
+ import { ATTR_DB_COLLECTION_NAME, ATTR_DB_OPERATION_NAME, withSpan } from "@better-auth/core/instrumentation";
15
14
  //#region src/db/adapter/factory.ts
16
15
  let debugLogs = [];
17
16
  let transactionId = -1;
@@ -58,6 +57,7 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
58
57
  else if (method === "findMany" && !config.debugLogs.findMany) return;
59
58
  else if (method === "delete" && !config.debugLogs.delete) return;
60
59
  else if (method === "deleteMany" && !config.debugLogs.deleteMany) return;
60
+ else if (method === "consumeOne" && !config.debugLogs.consumeOne) return;
61
61
  else if (method === "count" && !config.debugLogs.count) return;
62
62
  }
63
63
  logger.info(`[${config.adapterName}]`, ...args);
@@ -677,6 +677,67 @@ const createAdapterFactory = ({ adapter: customAdapter, config: cfg }) => (optio
677
677
  });
678
678
  return res;
679
679
  },
680
+ consumeOne: async ({ model: unsafeModel, where: unsafeWhere }) => {
681
+ transactionId++;
682
+ const thisTransactionId = transactionId;
683
+ const model = getModelName(unsafeModel);
684
+ const where = transformWhereClause({
685
+ model: unsafeModel,
686
+ where: unsafeWhere,
687
+ action: "consumeOne"
688
+ });
689
+ unsafeModel = getDefaultModelName(unsafeModel);
690
+ debugLog({ method: "consumeOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(1, 3)}`, `${formatMethod("consumeOne")} ${formatAction("ConsumeOne")}:`, {
691
+ model,
692
+ where
693
+ });
694
+ let res;
695
+ let resultNeedsOutputTransform = true;
696
+ if (adapterInstance.consumeOne) res = await withSpan(`db consumeOne ${model}`, {
697
+ [ATTR_DB_OPERATION_NAME]: "consumeOne",
698
+ [ATTR_DB_COLLECTION_NAME]: model
699
+ }, () => adapterInstance.consumeOne({
700
+ model,
701
+ where
702
+ }));
703
+ else {
704
+ res = await withSpan(`db consumeOne ${model}`, {
705
+ [ATTR_DB_OPERATION_NAME]: "consumeOne",
706
+ [ATTR_DB_COLLECTION_NAME]: model
707
+ }, () => adapter.transaction(async (trx) => {
708
+ const target = (await trx.findMany({
709
+ model: unsafeModel,
710
+ where: unsafeWhere,
711
+ limit: 1
712
+ }))[0];
713
+ if (!target) return null;
714
+ const deleted = await trx.deleteMany({
715
+ model: unsafeModel,
716
+ where: [...unsafeWhere, {
717
+ field: "id",
718
+ value: target.id,
719
+ operator: "eq",
720
+ connector: "AND",
721
+ mode: "sensitive"
722
+ }]
723
+ });
724
+ if (typeof deleted !== "number") throw new BetterAuthError(`Adapter "${config.adapterId}" returned a non-numeric value from deleteMany during the consumeOne fallback. Return the number of deleted rows, or implement a native consumeOne for atomic single-use consumption.`);
725
+ return deleted > 0 ? target : null;
726
+ }));
727
+ resultNeedsOutputTransform = false;
728
+ }
729
+ debugLog({ method: "consumeOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`, `${formatMethod("consumeOne")} ${formatAction("DB Result")}:`, {
730
+ model,
731
+ data: res
732
+ });
733
+ let transformed = res;
734
+ if (!config.disableTransformOutput && resultNeedsOutputTransform && res) transformed = await transformOutput(res, unsafeModel, void 0, void 0);
735
+ debugLog({ method: "consumeOne" }, `${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`, `${formatMethod("consumeOne")} ${formatAction("Parsed Result")}:`, {
736
+ model,
737
+ data: transformed
738
+ });
739
+ return transformed;
740
+ },
680
741
  count: async ({ model: unsafeModel, where: unsafeWhere }) => {
681
742
  transactionId++;
682
743
  const thisTransactionId = transactionId;
@@ -22,6 +22,7 @@ type DBAdapterDebugLogOption = boolean | {
22
22
  findMany?: boolean | undefined;
23
23
  delete?: boolean | undefined;
24
24
  deleteMany?: boolean | undefined;
25
+ consumeOne?: boolean | undefined;
25
26
  count?: boolean | undefined;
26
27
  } | {
27
28
  /**
@@ -197,7 +198,7 @@ interface DBAdapterFactoryConfig<Options extends BetterAuthOptions = BetterAuthO
197
198
  /**
198
199
  * The action which was called from the adapter.
199
200
  */
200
- action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "count";
201
+ action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "consumeOne" | "count";
201
202
  /**
202
203
  * The model name.
203
204
  */
@@ -415,6 +416,26 @@ type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
415
416
  model: string;
416
417
  where: Where[];
417
418
  }) => Promise<number>;
419
+ /**
420
+ * Atomically consume a single row matching the where clause: delete it and
421
+ * return the deleted row, or return `null` if no row matched.
422
+ * Implementations MUST NOT delete any additional rows that also match a
423
+ * non-unique predicate.
424
+ *
425
+ * Under concurrent invocation against the same row, exactly one caller
426
+ * receives the row; subsequent racers receive `null`. This is the
427
+ * race-safe primitive for consuming single-use credentials
428
+ * (verification tokens, authorization codes, one-time tokens).
429
+ *
430
+ * Always defined on the factory-wrapped adapter. When the underlying
431
+ * `CustomAdapter` does not implement `consumeOne`, the factory provides
432
+ * a fallback that wraps `findMany + deleteMany` in `transaction(...)`
433
+ * and returns the row only when the delete reports an affected row.
434
+ */
435
+ consumeOne: <T>(data: {
436
+ model: string;
437
+ where: Where[];
438
+ }) => Promise<T | null>;
418
439
  /**
419
440
  * Execute multiple operations in a transaction.
420
441
  * If the adapter doesn't support transactions, operations will be executed sequentially.
@@ -496,6 +517,19 @@ interface CustomAdapter {
496
517
  model: string;
497
518
  where: CleanedWhere[];
498
519
  }) => Promise<number>;
520
+ /**
521
+ * Optional native atomic single-row consume. When omitted, the adapter
522
+ * factory falls back to `transaction(findMany + deleteMany)`.
523
+ * Implementing this method natively (e.g. `DELETE ... RETURNING *`,
524
+ * `findOneAndDelete`, `OUTPUT deleted.*`) gives one round trip and the
525
+ * strongest race-safety guarantee. Implementations must delete at most
526
+ * one matching row. TODO(consume-one-required): tighten to required in the
527
+ * next minor on `next`.
528
+ */
529
+ consumeOne?: <T>(data: {
530
+ model: string;
531
+ where: CleanedWhere[];
532
+ }) => Promise<T | null>;
499
533
  count: ({
500
534
  model,
501
535
  where
@@ -94,7 +94,7 @@ type AdapterFactoryCustomizeAdapterCreator = (config: {
94
94
  }: {
95
95
  where: W;
96
96
  model: string;
97
- action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "count";
97
+ action: "create" | "update" | "findOne" | "findMany" | "updateMany" | "delete" | "deleteMany" | "consumeOne" | "count";
98
98
  }) => W extends undefined ? undefined : CleanedWhere[];
99
99
  }) => CustomAdapter;
100
100
  type AdapterTestDebugLogs = {
@@ -141,6 +141,18 @@ interface SecondaryStorage {
141
141
  * @returns - Value of the key
142
142
  */
143
143
  get: (key: string) => Awaitable<unknown>;
144
+ /**
145
+ * Atomically get a value and delete it from storage.
146
+ *
147
+ * This is optional for backwards compatibility with existing secondary
148
+ * storage implementations. Single-use credential consumers use it when
149
+ * present to avoid a read-then-delete race.
150
+ *
151
+ * TODO(secondary-storage-atomic-consume): make this required in the next
152
+ * breaking release, or require database-backed verification storage for
153
+ * security-sensitive consume paths.
154
+ */
155
+ getAndDelete?: (key: string) => Awaitable<unknown>;
144
156
  set: (
145
157
  /**
146
158
  * Key to store
@@ -36,6 +36,7 @@ declare const BASE_ERROR_CODES: {
36
36
  USER_ALREADY_EXISTS: RawError<"USER_ALREADY_EXISTS">;
37
37
  USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: RawError<"USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL">;
38
38
  EMAIL_CAN_NOT_BE_UPDATED: RawError<"EMAIL_CAN_NOT_BE_UPDATED">;
39
+ CHANGE_EMAIL_DISABLED: RawError<"CHANGE_EMAIL_DISABLED">;
39
40
  CREDENTIAL_ACCOUNT_NOT_FOUND: RawError<"CREDENTIAL_ACCOUNT_NOT_FOUND">;
40
41
  ACCOUNT_NOT_FOUND: RawError<"ACCOUNT_NOT_FOUND">;
41
42
  SESSION_EXPIRED: RawError<"SESSION_EXPIRED">;
@@ -23,6 +23,7 @@ const BASE_ERROR_CODES = defineErrorCodes({
23
23
  USER_ALREADY_EXISTS: "User already exists.",
24
24
  USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.",
25
25
  EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
26
+ CHANGE_EMAIL_DISABLED: "Change email is disabled",
26
27
  CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
27
28
  SESSION_EXPIRED: "Session expired. Re-authenticate to perform this action.",
28
29
  FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
@@ -2,7 +2,7 @@ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes.mjs";
2
2
  import { getOpenTelemetryAPI } from "./api.mjs";
3
3
  //#region src/instrumentation/tracer.ts
4
4
  const INSTRUMENTATION_SCOPE = "better-auth";
5
- const INSTRUMENTATION_VERSION = "1.7.0-beta.2";
5
+ const INSTRUMENTATION_VERSION = "1.7.0-beta.4";
6
6
  /**
7
7
  * Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
8
8
  * callbacks). These are APIErrors with 3xx status codes and should not be
@@ -0,0 +1,12 @@
1
+ import * as z from "zod";
2
+
3
+ //#region src/oauth2/authorization-params.d.ts
4
+ /**
5
+ * Zod schema for the `additionalParams` field on social sign-in and
6
+ * account-linking request bodies. Rejects any key reserved by the
7
+ * authorization-URL builder (see `RESERVED_AUTHORIZATION_PARAMS`), so
8
+ * a caller cannot overwrite `state`, PKCE, `redirect_uri`, etc.
9
+ */
10
+ declare const additionalAuthorizationParamsSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
11
+ //#endregion
12
+ export { additionalAuthorizationParamsSchema };
@@ -0,0 +1,12 @@
1
+ import { RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET } from "./create-authorization-url.mjs";
2
+ import * as z from "zod";
3
+ //#region src/oauth2/authorization-params.ts
4
+ /**
5
+ * Zod schema for the `additionalParams` field on social sign-in and
6
+ * account-linking request bodies. Rejects any key reserved by the
7
+ * authorization-URL builder (see `RESERVED_AUTHORIZATION_PARAMS`), so
8
+ * a caller cannot overwrite `state`, PKCE, `redirect_uri`, etc.
9
+ */
10
+ const additionalAuthorizationParamsSchema = z.record(z.string(), z.string()).refine((value) => !Object.keys(value).some((key) => RESERVED_AUTHORIZATION_PARAMS_SET.has(key)), { message: `additionalParams cannot include reserved OAuth parameters: ${RESERVED_AUTHORIZATION_PARAMS.join(", ")}` }).meta({ description: "Extra query parameters to append to the provider authorization URL (e.g. Cognito identity_provider, Google hd)." }).optional();
11
+ //#endregion
12
+ export { additionalAuthorizationParamsSchema };
@@ -0,0 +1,30 @@
1
+ //#region src/oauth2/basic-credentials.d.ts
2
+ /**
3
+ * Encodes an OAuth client id and secret as an HTTP Basic credential string.
4
+ *
5
+ * Follows RFC 6749 §2.3.1: both values are `application/x-www-form-urlencoded`
6
+ * prior to base64 encoding. The returned string is the full value of the
7
+ * `Authorization` header, including the `Basic ` prefix.
8
+ */
9
+ declare function encodeBasicCredentials(clientId: string, clientSecret: string): string;
10
+ /**
11
+ * Decodes an `Authorization: Basic …` header value into its OAuth client id
12
+ * and secret.
13
+ *
14
+ * Scheme matching is case-insensitive and tolerates one or more spaces
15
+ * between the scheme and credentials per RFC 7235 §2.1. The base64 payload
16
+ * is split on the first `:` only, so secrets containing colons round-trip
17
+ * correctly. Each half is form-url-decoded per RFC 6749 §2.3.1, accepting
18
+ * both `+` and `%20` as space. Per the URL Living Standard, invalid
19
+ * percent-escapes pass through as-is; downstream client lookup will fail
20
+ * with `invalid_client` for malformed credentials.
21
+ *
22
+ * Throws when the header is not a Basic credential, when the base64 payload
23
+ * contains no `:`, or when either half is empty.
24
+ */
25
+ declare function decodeBasicCredentials(authorization: string): {
26
+ clientId: string;
27
+ clientSecret: string;
28
+ };
29
+ //#endregion
30
+ export { decodeBasicCredentials, encodeBasicCredentials };
@@ -0,0 +1,64 @@
1
+ import { base64 } from "@better-auth/utils/base64";
2
+ //#region src/oauth2/basic-credentials.ts
3
+ const BASIC_AUTHORIZATION_PATTERN = /^Basic +(.*)$/i;
4
+ /**
5
+ * Encodes a value using `application/x-www-form-urlencoded` per the URL
6
+ * Living Standard. Differs from `encodeURIComponent` in two ways: it escapes
7
+ * `!`, `'`, `(`, and `)`, and it represents space as `+` rather than `%20`.
8
+ * `*` is left unescaped, matching the URL Standard's percent-encode set.
9
+ */
10
+ function formUrlEncode(value) {
11
+ return new URLSearchParams({ v: value }).toString().slice(2);
12
+ }
13
+ /**
14
+ * Inverse of `formUrlEncode`: decodes a single `application/x-www-form-urlencoded`
15
+ * value, handling both `+` and `%20` as space.
16
+ */
17
+ function formUrlDecode(value) {
18
+ const decoded = new URLSearchParams(`v=${value}`).get("v");
19
+ if (decoded === null) throw new Error("form-url-encoded value could not be decoded");
20
+ return decoded;
21
+ }
22
+ /**
23
+ * Encodes an OAuth client id and secret as an HTTP Basic credential string.
24
+ *
25
+ * Follows RFC 6749 §2.3.1: both values are `application/x-www-form-urlencoded`
26
+ * prior to base64 encoding. The returned string is the full value of the
27
+ * `Authorization` header, including the `Basic ` prefix.
28
+ */
29
+ function encodeBasicCredentials(clientId, clientSecret) {
30
+ const payload = `${formUrlEncode(clientId)}:${formUrlEncode(clientSecret)}`;
31
+ return `Basic ${base64.encode(payload)}`;
32
+ }
33
+ /**
34
+ * Decodes an `Authorization: Basic …` header value into its OAuth client id
35
+ * and secret.
36
+ *
37
+ * Scheme matching is case-insensitive and tolerates one or more spaces
38
+ * between the scheme and credentials per RFC 7235 §2.1. The base64 payload
39
+ * is split on the first `:` only, so secrets containing colons round-trip
40
+ * correctly. Each half is form-url-decoded per RFC 6749 §2.3.1, accepting
41
+ * both `+` and `%20` as space. Per the URL Living Standard, invalid
42
+ * percent-escapes pass through as-is; downstream client lookup will fail
43
+ * with `invalid_client` for malformed credentials.
44
+ *
45
+ * Throws when the header is not a Basic credential, when the base64 payload
46
+ * contains no `:`, or when either half is empty.
47
+ */
48
+ function decodeBasicCredentials(authorization) {
49
+ const match = authorization.match(BASIC_AUTHORIZATION_PATTERN);
50
+ if (!match) throw new Error("Authorization header is not a Basic credential");
51
+ const encoded = match[1] ?? "";
52
+ const decoded = new TextDecoder().decode(base64.decode(encoded));
53
+ const separatorIndex = decoded.indexOf(":");
54
+ if (separatorIndex === -1) throw new Error("Basic credential is missing the client id/secret separator");
55
+ const rawClientId = decoded.slice(0, separatorIndex);
56
+ const rawClientSecret = decoded.slice(separatorIndex + 1);
57
+ if (!rawClientId || !rawClientSecret) throw new Error("Basic credential client id and secret must both be non-empty");
58
+ return {
59
+ clientId: formUrlDecode(rawClientId),
60
+ clientSecret: formUrlDecode(rawClientSecret)
61
+ };
62
+ }
63
+ //#endregion
64
+ export { decodeBasicCredentials, encodeBasicCredentials };
@@ -1,11 +1,17 @@
1
+ import { Awaitable } from "../types/helper.mjs";
1
2
  //#region src/oauth2/client-assertion.d.ts
2
3
  /** Asymmetric signing algorithms compatible with private_key_jwt (RFC 7523). */
3
- declare const ASSERTION_SIGNING_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA"];
4
- type AssertionSigningAlgorithm = (typeof ASSERTION_SIGNING_ALGORITHMS)[number];
4
+ declare const PRIVATE_KEY_JWT_SIGNING_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA"];
5
+ type PrivateKeyJwtSigningAlgorithm = (typeof PRIVATE_KEY_JWT_SIGNING_ALGORITHMS)[number];
5
6
  declare const CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
6
- interface ClientAssertionConfig {
7
- /** Pre-signed JWT assertion string. If provided, signing is skipped. */
8
- assertion?: string;
7
+ type ClientAssertionGrantType = "authorization_code" | "refresh_token" | "client_credentials";
8
+ interface ClientAssertionContext {
9
+ clientId: string;
10
+ tokenEndpoint: string;
11
+ grantType: ClientAssertionGrantType;
12
+ }
13
+ type ClientAssertionGetter = (context: ClientAssertionContext) => Awaitable<string>;
14
+ interface PrivateKeyJwtClientAssertionGetterOptions {
9
15
  /** Private key in JWK format for signing. */
10
16
  privateKeyJwk?: JsonWebKey;
11
17
  /** Private key in PKCS#8 PEM format for signing. */
@@ -13,19 +19,23 @@ interface ClientAssertionConfig {
13
19
  /** Key ID to include in the JWT header. */
14
20
  kid?: string;
15
21
  /** Asymmetric signing algorithm. Symmetric algorithms (HS256) and "none" are not allowed. @default "RS256" */
16
- algorithm?: AssertionSigningAlgorithm;
17
- /** Token endpoint URL (used as the JWT `aud` claim). */
18
- tokenEndpoint?: string;
22
+ algorithm?: PrivateKeyJwtSigningAlgorithm;
19
23
  /** Assertion lifetime in seconds. @default 120 */
20
24
  expiresIn?: number;
21
25
  }
22
26
  /**
23
27
  * Signs an RFC 7523 client assertion JWT for `private_key_jwt` authentication.
24
28
  *
25
- * The JWT contains: iss=clientId, sub=clientId, aud=tokenEndpoint,
26
- * exp=now+120s, jti=unique, iat=now.
29
+ * The JWT contains these claims:
30
+ *
31
+ * - iss=clientId
32
+ * - sub=clientId
33
+ * - aud=tokenEndpoint
34
+ * - exp=now + 120s
35
+ * - jti=unique
36
+ * - iat=now
27
37
  */
28
- declare function signClientAssertion({
38
+ declare function signPrivateKeyJwtClientAssertion({
29
39
  clientId,
30
40
  tokenEndpoint,
31
41
  privateKeyJwk,
@@ -39,21 +49,27 @@ declare function signClientAssertion({
39
49
  privateKeyJwk?: JsonWebKey;
40
50
  privateKeyPem?: string;
41
51
  kid?: string;
42
- algorithm?: AssertionSigningAlgorithm;
52
+ algorithm?: PrivateKeyJwtSigningAlgorithm;
43
53
  expiresIn?: number;
44
54
  }): Promise<string>;
45
55
  /**
46
- * Resolves a ClientAssertionConfig into `client_assertion` + `client_assertion_type`
47
- * params for injection into a token request body.
56
+ * Creates a client assertion getter for `private_key_jwt` authentication.
57
+ *
58
+ * Validates options eagerly (key material, supported algorithm, JWK alg
59
+ * agreement) so misconfiguration surfaces at construction rather than on the
60
+ * first token request. The returned function signs a fresh RFC 7523 JWT
61
+ * assertion for every token endpoint request.
48
62
  */
49
- declare function resolveAssertionParams({
50
- clientAssertion,
51
- clientId,
52
- tokenEndpoint
63
+ declare function createPrivateKeyJwtClientAssertionGetter(options: PrivateKeyJwtClientAssertionGetterOptions): ClientAssertionGetter;
64
+ /**
65
+ * Resolves a client assertion getter into `client_assertion` + `client_assertion_type` params for injection into a token request body.
66
+ */
67
+ declare function resolveClientAssertionParams({
68
+ getClientAssertion,
69
+ context
53
70
  }: {
54
- clientAssertion: ClientAssertionConfig;
55
- clientId: string;
56
- tokenEndpoint?: string;
71
+ getClientAssertion: ClientAssertionGetter;
72
+ context: ClientAssertionContext;
57
73
  }): Promise<Record<string, string>>;
58
74
  //#endregion
59
- export { ASSERTION_SIGNING_ALGORITHMS, AssertionSigningAlgorithm, CLIENT_ASSERTION_TYPE, ClientAssertionConfig, resolveAssertionParams, signClientAssertion };
75
+ export { CLIENT_ASSERTION_TYPE, ClientAssertionContext, ClientAssertionGetter, ClientAssertionGrantType, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, PrivateKeyJwtClientAssertionGetterOptions, PrivateKeyJwtSigningAlgorithm, createPrivateKeyJwtClientAssertionGetter, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion };
@@ -1,7 +1,7 @@
1
1
  import { SignJWT, importJWK, importPKCS8 } from "jose";
2
2
  //#region src/oauth2/client-assertion.ts
3
3
  /** Asymmetric signing algorithms compatible with private_key_jwt (RFC 7523). */
4
- const ASSERTION_SIGNING_ALGORITHMS = [
4
+ const PRIVATE_KEY_JWT_SIGNING_ALGORITHMS = [
5
5
  "RS256",
6
6
  "RS384",
7
7
  "RS512",
@@ -13,20 +13,46 @@ const ASSERTION_SIGNING_ALGORITHMS = [
13
13
  "ES512",
14
14
  "EdDSA"
15
15
  ];
16
+ function assertSupportedPrivateKeyJwtAlgorithm(candidate) {
17
+ if (!PRIVATE_KEY_JWT_SIGNING_ALGORITHMS.includes(candidate)) throw new Error(`Unsupported private_key_jwt signing algorithm: ${candidate}. Use one of ${PRIVATE_KEY_JWT_SIGNING_ALGORITHMS.join(", ")}.`);
18
+ }
19
+ /**
20
+ * Validates `private_key_jwt` options eagerly and returns the algorithm to
21
+ * use for signing.
22
+ *
23
+ * Asserts that key material is configured, that any explicit `algorithm` is
24
+ * supported, that any JWK-embedded `alg` is supported, and that the two
25
+ * agree when both are set.
26
+ */
27
+ function resolveValidPrivateKeyJwtOptions(options) {
28
+ if (!options.privateKeyJwk && !options.privateKeyPem) throw new Error("private_key_jwt requires either privateKeyJwk or privateKeyPem");
29
+ if (options.algorithm) assertSupportedPrivateKeyJwtAlgorithm(options.algorithm);
30
+ const jwkAlg = options.privateKeyJwk?.alg;
31
+ if (typeof jwkAlg === "string") assertSupportedPrivateKeyJwtAlgorithm(jwkAlg);
32
+ if (options.algorithm && typeof jwkAlg === "string" && options.algorithm !== jwkAlg) throw new Error(`JWK alg "${jwkAlg}" does not match configured algorithm "${options.algorithm}". Remove the JWK alg field, or pass an algorithm that matches the JWK.`);
33
+ return options.algorithm ?? (typeof jwkAlg === "string" ? jwkAlg : "RS256");
34
+ }
16
35
  const CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
17
36
  /**
18
37
  * Signs an RFC 7523 client assertion JWT for `private_key_jwt` authentication.
19
38
  *
20
- * The JWT contains: iss=clientId, sub=clientId, aud=tokenEndpoint,
21
- * exp=now+120s, jti=unique, iat=now.
39
+ * The JWT contains these claims:
40
+ *
41
+ * - iss=clientId
42
+ * - sub=clientId
43
+ * - aud=tokenEndpoint
44
+ * - exp=now + 120s
45
+ * - jti=unique
46
+ * - iat=now
22
47
  */
23
- async function signClientAssertion({ clientId, tokenEndpoint, privateKeyJwk, privateKeyPem, kid, algorithm, expiresIn = 120 }) {
48
+ async function signPrivateKeyJwtClientAssertion({ clientId, tokenEndpoint, privateKeyJwk, privateKeyPem, kid, algorithm, expiresIn = 120 }) {
49
+ const resolvedAlg = resolveValidPrivateKeyJwtOptions({
50
+ privateKeyJwk,
51
+ privateKeyPem,
52
+ algorithm
53
+ });
24
54
  const resolvedKid = kid ?? privateKeyJwk?.kid;
25
- const resolvedAlg = algorithm ?? privateKeyJwk?.alg ?? "RS256";
26
- let key;
27
- if (privateKeyJwk) key = await importJWK(privateKeyJwk, resolvedAlg);
28
- else if (privateKeyPem) key = await importPKCS8(privateKeyPem, resolvedAlg);
29
- else throw new Error("private_key_jwt requires either privateKeyJwk or privateKeyPem");
55
+ const key = privateKeyJwk ? await importJWK(privateKeyJwk, resolvedAlg) : await importPKCS8(privateKeyPem, resolvedAlg);
30
56
  const now = Math.floor(Date.now() / 1e3);
31
57
  const jti = crypto.randomUUID();
32
58
  const header = {
@@ -37,28 +63,37 @@ async function signClientAssertion({ clientId, tokenEndpoint, privateKeyJwk, pri
37
63
  return new SignJWT({}).setProtectedHeader(header).setIssuer(clientId).setSubject(clientId).setAudience(tokenEndpoint).setIssuedAt(now).setExpirationTime(now + expiresIn).setJti(jti).sign(key);
38
64
  }
39
65
  /**
40
- * Resolves a ClientAssertionConfig into `client_assertion` + `client_assertion_type`
41
- * params for injection into a token request body.
66
+ * Creates a client assertion getter for `private_key_jwt` authentication.
67
+ *
68
+ * Validates options eagerly (key material, supported algorithm, JWK alg
69
+ * agreement) so misconfiguration surfaces at construction rather than on the
70
+ * first token request. The returned function signs a fresh RFC 7523 JWT
71
+ * assertion for every token endpoint request.
72
+ */
73
+ function createPrivateKeyJwtClientAssertionGetter(options) {
74
+ resolveValidPrivateKeyJwtOptions({
75
+ privateKeyJwk: options.privateKeyJwk,
76
+ privateKeyPem: options.privateKeyPem,
77
+ algorithm: options.algorithm
78
+ });
79
+ return ({ clientId, tokenEndpoint }) => signPrivateKeyJwtClientAssertion({
80
+ clientId,
81
+ tokenEndpoint,
82
+ privateKeyJwk: options.privateKeyJwk,
83
+ privateKeyPem: options.privateKeyPem,
84
+ kid: options.kid,
85
+ algorithm: options.algorithm,
86
+ expiresIn: options.expiresIn
87
+ });
88
+ }
89
+ /**
90
+ * Resolves a client assertion getter into `client_assertion` + `client_assertion_type` params for injection into a token request body.
42
91
  */
43
- async function resolveAssertionParams({ clientAssertion, clientId, tokenEndpoint }) {
44
- let assertion = clientAssertion.assertion;
45
- if (!assertion) {
46
- const audEndpoint = tokenEndpoint ?? clientAssertion.tokenEndpoint;
47
- if (!audEndpoint) throw new Error("private_key_jwt requires a tokenEndpoint for the JWT audience claim");
48
- assertion = await signClientAssertion({
49
- clientId,
50
- tokenEndpoint: audEndpoint,
51
- privateKeyJwk: clientAssertion.privateKeyJwk,
52
- privateKeyPem: clientAssertion.privateKeyPem,
53
- kid: clientAssertion.kid,
54
- algorithm: clientAssertion.algorithm,
55
- expiresIn: clientAssertion.expiresIn
56
- });
57
- }
92
+ async function resolveClientAssertionParams({ getClientAssertion, context }) {
58
93
  return {
59
- client_assertion: assertion,
94
+ client_assertion: await getClientAssertion(context),
60
95
  client_assertion_type: CLIENT_ASSERTION_TYPE
61
96
  };
62
97
  }
63
98
  //#endregion
64
- export { ASSERTION_SIGNING_ALGORITHMS, CLIENT_ASSERTION_TYPE, resolveAssertionParams, signClientAssertion };
99
+ export { CLIENT_ASSERTION_TYPE, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, createPrivateKeyJwtClientAssertionGetter, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion };
@@ -1,59 +1,38 @@
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/client-credentials-token.d.ts
6
- declare function clientCredentialsTokenRequest({
7
- options,
8
- scope,
9
- authentication,
10
- clientAssertion,
11
- tokenEndpoint,
12
- resource
13
- }: {
6
+ interface ClientCredentialsTokenRequestInput {
14
7
  options: AwaitableFunction<ProviderOptions>;
15
8
  scope?: string | undefined;
16
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
17
- clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
9
+ authentication?: TokenEndpointSecretAuthentication | undefined;
10
+ tokenEndpointAuth?: TokenEndpointAuth | undefined;
18
11
  tokenEndpoint?: string | undefined;
19
12
  resource?: (string | string[]) | undefined;
20
- }): Promise<{
21
- body: URLSearchParams;
22
- headers: Record<string, any>;
23
- }>;
24
- /**
25
- * @deprecated use async'd clientCredentialsTokenRequest instead
26
- */
27
- declare function createClientCredentialsTokenRequest({
13
+ }
14
+ interface ClientCredentialsTokenInput extends ClientCredentialsTokenRequestInput {
15
+ tokenEndpoint: string;
16
+ scope: string;
17
+ }
18
+ declare function clientCredentialsTokenRequest({
28
19
  options,
29
20
  scope,
30
21
  authentication,
31
- resource,
32
- extraParams
33
- }: {
34
- options: ProviderOptions;
35
- scope?: string | undefined;
36
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
37
- resource?: (string | string[]) | undefined;
38
- extraParams?: Record<string, string> | undefined;
39
- }): {
22
+ tokenEndpointAuth,
23
+ tokenEndpoint,
24
+ resource
25
+ }: ClientCredentialsTokenRequestInput): Promise<{
40
26
  body: URLSearchParams;
41
- headers: Record<string, any>;
42
- };
27
+ headers: Record<string, string>;
28
+ }>;
43
29
  declare function clientCredentialsToken({
44
30
  options,
45
31
  tokenEndpoint,
46
32
  scope,
47
33
  authentication,
48
- clientAssertion,
34
+ tokenEndpointAuth,
49
35
  resource
50
- }: {
51
- options: AwaitableFunction<ProviderOptions>;
52
- tokenEndpoint: string;
53
- scope: string;
54
- authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
55
- clientAssertion?: ClientAssertionConfig | undefined;
56
- resource?: (string | string[]) | undefined;
57
- }): Promise<OAuth2Tokens>;
36
+ }: ClientCredentialsTokenInput): Promise<OAuth2Tokens>;
58
37
  //#endregion
59
- export { clientCredentialsToken, clientCredentialsTokenRequest, createClientCredentialsTokenRequest };
38
+ export { clientCredentialsToken, clientCredentialsTokenRequest };