@better-auth/core 1.7.0-beta.0 → 1.7.0-beta.10

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 (218) hide show
  1. package/dist/api/index.d.mts +44 -1
  2. package/dist/api/index.mjs +69 -4
  3. package/dist/context/global.mjs +1 -1
  4. package/dist/context/transaction.d.mts +7 -4
  5. package/dist/context/transaction.mjs +6 -3
  6. package/dist/db/adapter/factory.mjs +91 -3
  7. package/dist/db/adapter/get-id-field.mjs +1 -1
  8. package/dist/db/adapter/index.d.mts +87 -3
  9. package/dist/db/adapter/types.d.mts +1 -1
  10. package/dist/db/get-tables.mjs +2 -1
  11. package/dist/db/type.d.mts +17 -0
  12. package/dist/env/env-impl.mjs +1 -1
  13. package/dist/error/codes.d.mts +1 -0
  14. package/dist/error/codes.mjs +1 -0
  15. package/dist/error/index.d.mts +7 -0
  16. package/dist/index.d.mts +2 -2
  17. package/dist/instrumentation/api.mjs +12 -0
  18. package/dist/instrumentation/noop.mjs +42 -0
  19. package/dist/instrumentation/pure.index.d.mts +7 -0
  20. package/dist/instrumentation/pure.index.mjs +7 -0
  21. package/dist/instrumentation/tracer.mjs +6 -3
  22. package/dist/oauth2/authorization-params.d.mts +12 -0
  23. package/dist/oauth2/authorization-params.mjs +12 -0
  24. package/dist/oauth2/basic-credentials.d.mts +30 -0
  25. package/dist/oauth2/basic-credentials.mjs +64 -0
  26. package/dist/oauth2/client-assertion.d.mts +38 -22
  27. package/dist/oauth2/client-assertion.mjs +63 -28
  28. package/dist/oauth2/client-credentials-token.d.mts +19 -40
  29. package/dist/oauth2/client-credentials-token.mjs +20 -31
  30. package/dist/oauth2/create-authorization-url.d.mts +11 -1
  31. package/dist/oauth2/create-authorization-url.mjs +27 -7
  32. package/dist/oauth2/dpop.d.mts +142 -0
  33. package/dist/oauth2/dpop.mjs +246 -0
  34. package/dist/oauth2/index.d.mts +14 -9
  35. package/dist/oauth2/index.mjs +12 -8
  36. package/dist/oauth2/oauth-provider.d.mts +150 -10
  37. package/dist/oauth2/refresh-access-token.d.mts +20 -40
  38. package/dist/oauth2/refresh-access-token.mjs +38 -36
  39. package/dist/oauth2/reject-redirects.mjs +65 -0
  40. package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
  41. package/dist/oauth2/token-endpoint-auth.mjs +89 -0
  42. package/dist/oauth2/utils.d.mts +23 -1
  43. package/dist/oauth2/utils.mjs +48 -2
  44. package/dist/oauth2/validate-authorization-code.d.mts +17 -52
  45. package/dist/oauth2/validate-authorization-code.mjs +28 -34
  46. package/dist/oauth2/verify-id-token.d.mts +27 -0
  47. package/dist/oauth2/verify-id-token.mjs +62 -0
  48. package/dist/oauth2/verify.d.mts +88 -15
  49. package/dist/oauth2/verify.mjs +203 -25
  50. package/dist/social-providers/apple.d.mts +16 -4
  51. package/dist/social-providers/apple.mjs +17 -24
  52. package/dist/social-providers/atlassian.d.mts +5 -1
  53. package/dist/social-providers/atlassian.mjs +6 -3
  54. package/dist/social-providers/cognito.d.mts +27 -3
  55. package/dist/social-providers/cognito.mjs +15 -25
  56. package/dist/social-providers/discord.d.mts +7 -3
  57. package/dist/social-providers/discord.mjs +16 -3
  58. package/dist/social-providers/dropbox.d.mts +5 -1
  59. package/dist/social-providers/dropbox.mjs +5 -4
  60. package/dist/social-providers/facebook.d.mts +24 -5
  61. package/dist/social-providers/facebook.mjs +54 -20
  62. package/dist/social-providers/figma.d.mts +5 -1
  63. package/dist/social-providers/figma.mjs +4 -3
  64. package/dist/social-providers/github.d.mts +6 -2
  65. package/dist/social-providers/github.mjs +5 -4
  66. package/dist/social-providers/gitlab.d.mts +5 -1
  67. package/dist/social-providers/gitlab.mjs +3 -2
  68. package/dist/social-providers/google.d.mts +53 -5
  69. package/dist/social-providers/google.mjs +69 -25
  70. package/dist/social-providers/huggingface.d.mts +5 -1
  71. package/dist/social-providers/huggingface.mjs +3 -2
  72. package/dist/social-providers/index.d.mts +221 -45
  73. package/dist/social-providers/index.mjs +2 -2
  74. package/dist/social-providers/kakao.d.mts +5 -1
  75. package/dist/social-providers/kakao.mjs +3 -2
  76. package/dist/social-providers/kick.d.mts +5 -1
  77. package/dist/social-providers/kick.mjs +3 -2
  78. package/dist/social-providers/line.d.mts +8 -2
  79. package/dist/social-providers/line.mjs +5 -6
  80. package/dist/social-providers/linear.d.mts +5 -1
  81. package/dist/social-providers/linear.mjs +3 -2
  82. package/dist/social-providers/linkedin.d.mts +7 -3
  83. package/dist/social-providers/linkedin.mjs +4 -3
  84. package/dist/social-providers/microsoft-entra-id.d.mts +41 -7
  85. package/dist/social-providers/microsoft-entra-id.mjs +37 -28
  86. package/dist/social-providers/naver.d.mts +5 -1
  87. package/dist/social-providers/naver.mjs +3 -2
  88. package/dist/social-providers/notion.d.mts +5 -1
  89. package/dist/social-providers/notion.mjs +5 -2
  90. package/dist/social-providers/paybin.d.mts +5 -1
  91. package/dist/social-providers/paybin.mjs +4 -3
  92. package/dist/social-providers/paypal.d.mts +6 -2
  93. package/dist/social-providers/paypal.mjs +19 -14
  94. package/dist/social-providers/polar.d.mts +5 -1
  95. package/dist/social-providers/polar.mjs +3 -2
  96. package/dist/social-providers/railway.d.mts +5 -1
  97. package/dist/social-providers/railway.mjs +3 -2
  98. package/dist/social-providers/reddit.d.mts +5 -1
  99. package/dist/social-providers/reddit.mjs +7 -5
  100. package/dist/social-providers/roblox.d.mts +6 -2
  101. package/dist/social-providers/roblox.mjs +12 -2
  102. package/dist/social-providers/salesforce.d.mts +5 -1
  103. package/dist/social-providers/salesforce.mjs +4 -3
  104. package/dist/social-providers/slack.d.mts +6 -2
  105. package/dist/social-providers/slack.mjs +11 -8
  106. package/dist/social-providers/spotify.d.mts +5 -1
  107. package/dist/social-providers/spotify.mjs +3 -2
  108. package/dist/social-providers/tiktok.d.mts +5 -1
  109. package/dist/social-providers/tiktok.mjs +14 -2
  110. package/dist/social-providers/twitch.d.mts +5 -1
  111. package/dist/social-providers/twitch.mjs +3 -2
  112. package/dist/social-providers/twitter.d.mts +4 -1
  113. package/dist/social-providers/twitter.mjs +2 -1
  114. package/dist/social-providers/vercel.d.mts +5 -1
  115. package/dist/social-providers/vercel.mjs +3 -2
  116. package/dist/social-providers/vk.d.mts +5 -1
  117. package/dist/social-providers/vk.mjs +3 -2
  118. package/dist/social-providers/wechat.d.mts +5 -1
  119. package/dist/social-providers/wechat.mjs +8 -2
  120. package/dist/social-providers/zoom.d.mts +5 -1
  121. package/dist/social-providers/zoom.mjs +10 -17
  122. package/dist/types/context.d.mts +55 -6
  123. package/dist/types/index.d.mts +1 -1
  124. package/dist/types/init-options.d.mts +182 -12
  125. package/dist/types/plugin-client.d.mts +12 -2
  126. package/dist/utils/async.d.mts +22 -0
  127. package/dist/utils/async.mjs +32 -0
  128. package/dist/utils/host.d.mts +147 -0
  129. package/dist/utils/host.mjs +298 -0
  130. package/dist/utils/ip.d.mts +28 -5
  131. package/dist/utils/ip.mjs +118 -4
  132. package/dist/utils/is-api-error.d.mts +6 -0
  133. package/dist/utils/is-api-error.mjs +8 -0
  134. package/dist/utils/redirect-uri.d.mts +20 -0
  135. package/dist/utils/redirect-uri.mjs +48 -0
  136. package/dist/utils/string.d.mts +5 -1
  137. package/dist/utils/string.mjs +20 -1
  138. package/dist/utils/url.d.mts +18 -1
  139. package/dist/utils/url.mjs +34 -4
  140. package/package.json +26 -16
  141. package/src/api/index.ts +121 -5
  142. package/src/context/transaction.ts +45 -12
  143. package/src/db/adapter/factory.ts +193 -7
  144. package/src/db/adapter/get-id-field.ts +2 -2
  145. package/src/db/adapter/index.ts +85 -2
  146. package/src/db/adapter/types.ts +2 -0
  147. package/src/db/get-tables.ts +9 -1
  148. package/src/db/schema/account.ts +4 -1
  149. package/src/db/schema/user.ts +3 -0
  150. package/src/db/type.ts +17 -0
  151. package/src/env/env-impl.ts +1 -2
  152. package/src/error/codes.ts +1 -0
  153. package/src/error/index.ts +9 -0
  154. package/src/instrumentation/api.ts +17 -0
  155. package/src/instrumentation/noop.ts +74 -0
  156. package/src/instrumentation/pure.index.ts +31 -0
  157. package/src/instrumentation/tracer.ts +8 -3
  158. package/src/oauth2/authorization-params.ts +28 -0
  159. package/src/oauth2/basic-credentials.ts +87 -0
  160. package/src/oauth2/client-assertion.ts +131 -58
  161. package/src/oauth2/client-credentials-token.ts +50 -74
  162. package/src/oauth2/create-authorization-url.ts +33 -7
  163. package/src/oauth2/dpop.ts +568 -0
  164. package/src/oauth2/index.ts +81 -11
  165. package/src/oauth2/oauth-provider.ts +159 -11
  166. package/src/oauth2/refresh-access-token.ts +82 -83
  167. package/src/oauth2/reject-redirects.ts +75 -0
  168. package/src/oauth2/token-endpoint-auth.ts +221 -0
  169. package/src/oauth2/utils.ts +72 -5
  170. package/src/oauth2/validate-authorization-code.ts +69 -89
  171. package/src/oauth2/verify-id-token.ts +115 -0
  172. package/src/oauth2/verify.ts +409 -78
  173. package/src/social-providers/apple.ts +30 -40
  174. package/src/social-providers/atlassian.ts +8 -1
  175. package/src/social-providers/cognito.ts +36 -39
  176. package/src/social-providers/discord.ts +22 -18
  177. package/src/social-providers/dropbox.ts +7 -5
  178. package/src/social-providers/facebook.ts +108 -52
  179. package/src/social-providers/figma.ts +8 -1
  180. package/src/social-providers/github.ts +5 -3
  181. package/src/social-providers/gitlab.ts +2 -0
  182. package/src/social-providers/google.ts +141 -39
  183. package/src/social-providers/huggingface.ts +8 -1
  184. package/src/social-providers/kakao.ts +2 -1
  185. package/src/social-providers/kick.ts +8 -1
  186. package/src/social-providers/line.ts +25 -27
  187. package/src/social-providers/linear.ts +8 -1
  188. package/src/social-providers/linkedin.ts +5 -3
  189. package/src/social-providers/microsoft-entra-id.ts +100 -55
  190. package/src/social-providers/naver.ts +2 -1
  191. package/src/social-providers/notion.ts +8 -1
  192. package/src/social-providers/paybin.ts +2 -0
  193. package/src/social-providers/paypal.ts +28 -17
  194. package/src/social-providers/polar.ts +8 -1
  195. package/src/social-providers/railway.ts +8 -1
  196. package/src/social-providers/reddit.ts +9 -4
  197. package/src/social-providers/roblox.ts +16 -11
  198. package/src/social-providers/salesforce.ts +8 -1
  199. package/src/social-providers/slack.ts +15 -9
  200. package/src/social-providers/spotify.ts +8 -1
  201. package/src/social-providers/tiktok.ts +22 -9
  202. package/src/social-providers/twitch.ts +2 -1
  203. package/src/social-providers/twitter.ts +1 -0
  204. package/src/social-providers/vercel.ts +8 -1
  205. package/src/social-providers/vk.ts +8 -1
  206. package/src/social-providers/wechat.ts +17 -2
  207. package/src/social-providers/zoom.ts +15 -19
  208. package/src/types/context.ts +57 -5
  209. package/src/types/index.ts +7 -0
  210. package/src/types/init-options.ts +204 -14
  211. package/src/types/plugin-client.ts +16 -2
  212. package/src/utils/async.ts +53 -0
  213. package/src/utils/host.ts +425 -0
  214. package/src/utils/ip.ts +197 -13
  215. package/src/utils/is-api-error.ts +10 -0
  216. package/src/utils/redirect-uri.ts +54 -0
  217. package/src/utils/string.ts +37 -0
  218. package/src/utils/url.ts +38 -4
@@ -0,0 +1,20 @@
1
+ import * as z from "zod";
2
+
3
+ //#region src/utils/redirect-uri.d.ts
4
+ /**
5
+ * Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
6
+ * server stores and later hands back to a browser.
7
+ *
8
+ * - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
9
+ * - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
10
+ * - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
11
+ * `*.localhost` per RFC 6761), where HTTP is allowed for local development.
12
+ * - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
13
+ *
14
+ * This is the single source of truth for redirect-URI validation across the
15
+ * OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
16
+ * rather than re-implementing the scheme policy per plugin.
17
+ */
18
+ declare const SafeUrlSchema: z.ZodURL;
19
+ //#endregion
20
+ export { SafeUrlSchema };
@@ -0,0 +1,48 @@
1
+ import { isLoopbackHost } from "./host.mjs";
2
+ import { DANGEROUS_URL_SCHEMES } from "./url.mjs";
3
+ import * as z from "zod";
4
+ //#region src/utils/redirect-uri.ts
5
+ /**
6
+ * Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
7
+ * server stores and later hands back to a browser.
8
+ *
9
+ * - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
10
+ * - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
11
+ * - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
12
+ * `*.localhost` per RFC 6761), where HTTP is allowed for local development.
13
+ * - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
14
+ *
15
+ * This is the single source of truth for redirect-URI validation across the
16
+ * OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
17
+ * rather than re-implementing the scheme policy per plugin.
18
+ */
19
+ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
20
+ let u;
21
+ try {
22
+ u = new URL(val);
23
+ } catch {
24
+ ctx.addIssue({
25
+ code: "custom",
26
+ message: "URL must be parseable",
27
+ fatal: true
28
+ });
29
+ return z.NEVER;
30
+ }
31
+ if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
32
+ ctx.addIssue({
33
+ code: "custom",
34
+ message: "URL cannot use javascript:, data:, or vbscript: scheme"
35
+ });
36
+ return;
37
+ }
38
+ if (val.includes("#")) ctx.addIssue({
39
+ code: "custom",
40
+ message: "Redirect URI must not contain a fragment component"
41
+ });
42
+ if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
43
+ code: "custom",
44
+ message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
45
+ });
46
+ });
47
+ //#endregion
48
+ export { SafeUrlSchema };
@@ -1,4 +1,8 @@
1
1
  //#region src/utils/string.d.ts
2
2
  declare function capitalizeFirstLetter(str: string): string;
3
+ declare function toSnakeCase(input: string): string;
4
+ declare function toKebabCase(input: string): string;
5
+ declare function toCamelCase(input: string): string;
6
+ declare function toPascalCase(input: string): string;
3
7
  //#endregion
4
- export { capitalizeFirstLetter };
8
+ export { capitalizeFirstLetter, toCamelCase, toKebabCase, toPascalCase, toSnakeCase };
@@ -2,5 +2,24 @@
2
2
  function capitalizeFirstLetter(str) {
3
3
  return str.charAt(0).toUpperCase() + str.slice(1);
4
4
  }
5
+ const WORD_PATTERN = /[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
6
+ const APOSTROPHE_PATTERN = /['\u2019]/g;
7
+ function splitWords(input) {
8
+ return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
9
+ }
10
+ function toSnakeCase(input) {
11
+ return splitWords(input).map((word) => word.toLowerCase()).join("_");
12
+ }
13
+ function toKebabCase(input) {
14
+ return splitWords(input).map((word) => word.toLowerCase()).join("-");
15
+ }
16
+ function toCamelCase(input) {
17
+ return splitWords(input).reduce((acc, word, i) => {
18
+ return acc + (i === 0 ? word.toLowerCase() : `${word[0].toUpperCase()}${word.slice(1)}`);
19
+ }, "");
20
+ }
21
+ function toPascalCase(input) {
22
+ return splitWords(input).map((word) => `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`).join("");
23
+ }
5
24
  //#endregion
6
- export { capitalizeFirstLetter };
25
+ export { capitalizeFirstLetter, toCamelCase, toKebabCase, toPascalCase, toSnakeCase };
@@ -16,5 +16,22 @@
16
16
  * // Returns: "/sso/saml2/callback/provider1"
17
17
  */
18
18
  declare function normalizePathname(requestUrl: string, basePath: string): string;
19
+ /**
20
+ * Schemes that execute or embed code when navigated to or accepted as a
21
+ * redirect target. These are never safe as an OAuth `redirect_uri` or as a
22
+ * client-side navigation target (`window.location.href`, `location.assign`, ...).
23
+ */
24
+ declare const DANGEROUS_URL_SCHEMES: string[];
25
+ /**
26
+ * Returns `false` only when `value` is an absolute URL using a dangerous scheme
27
+ * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
28
+ * safe absolute schemes (`http`, `https`, custom app schemes such as
29
+ * `myapp://`) return `true`.
30
+ *
31
+ * Use this to guard browser navigation sinks and any redirect target that may
32
+ * originate from untrusted input. It is intentionally narrow: it blocks code
33
+ * execution schemes without rejecting relative paths or mobile deep links.
34
+ */
35
+ declare function isSafeUrlScheme(value: string): boolean;
19
36
  //#endregion
20
- export { normalizePathname };
37
+ export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
@@ -22,10 +22,40 @@ function normalizePathname(requestUrl, basePath) {
22
22
  } catch {
23
23
  return "/";
24
24
  }
25
- if (basePath === "/" || basePath === "") return pathname;
26
- if (pathname === basePath) return "/";
27
- if (pathname.startsWith(basePath + "/")) return pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
25
+ const normalizedBasePath = basePath.replace(/\/+$/, "");
26
+ if (normalizedBasePath === "") return pathname;
27
+ if (pathname === normalizedBasePath) return "/";
28
+ if (pathname.startsWith(normalizedBasePath + "/")) return pathname.slice(normalizedBasePath.length).replace(/\/+$/, "") || "/";
28
29
  return pathname;
29
30
  }
31
+ /**
32
+ * Schemes that execute or embed code when navigated to or accepted as a
33
+ * redirect target. These are never safe as an OAuth `redirect_uri` or as a
34
+ * client-side navigation target (`window.location.href`, `location.assign`, ...).
35
+ */
36
+ const DANGEROUS_URL_SCHEMES = [
37
+ "javascript:",
38
+ "data:",
39
+ "vbscript:"
40
+ ];
41
+ /**
42
+ * Returns `false` only when `value` is an absolute URL using a dangerous scheme
43
+ * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
44
+ * safe absolute schemes (`http`, `https`, custom app schemes such as
45
+ * `myapp://`) return `true`.
46
+ *
47
+ * Use this to guard browser navigation sinks and any redirect target that may
48
+ * originate from untrusted input. It is intentionally narrow: it blocks code
49
+ * execution schemes without rejecting relative paths or mobile deep links.
50
+ */
51
+ function isSafeUrlScheme(value) {
52
+ let parsed;
53
+ try {
54
+ parsed = new URL(value);
55
+ } catch {
56
+ return true;
57
+ }
58
+ return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
59
+ }
30
60
  //#endregion
31
- export { normalizePathname };
61
+ export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.7.0-beta.0",
3
+ "version": "1.7.0-beta.10",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -93,6 +93,13 @@
93
93
  "./instrumentation": {
94
94
  "dev-source": "./src/instrumentation/index.ts",
95
95
  "types": "./dist/instrumentation/index.d.mts",
96
+ "workerd": "./dist/instrumentation/pure.index.mjs",
97
+ "edge": "./dist/instrumentation/pure.index.mjs",
98
+ "browser": "./dist/instrumentation/pure.index.mjs",
99
+ "node": "./dist/instrumentation/index.mjs",
100
+ "deno": "./dist/instrumentation/index.mjs",
101
+ "bun": "./dist/instrumentation/index.mjs",
102
+ "import": "./dist/instrumentation/index.mjs",
96
103
  "default": "./dist/instrumentation/index.mjs"
97
104
  }
98
105
  },
@@ -140,36 +147,39 @@
140
147
  }
141
148
  },
142
149
  "dependencies": {
143
- "@opentelemetry/semantic-conventions": "^1.39.0",
150
+ "@opentelemetry/semantic-conventions": "^1.41.1",
144
151
  "@standard-schema/spec": "^1.1.0",
145
152
  "zod": "^4.3.6"
146
153
  },
147
154
  "devDependencies": {
148
- "@better-auth/utils": "0.4.0",
149
- "@better-fetch/fetch": "1.1.21",
150
- "@opentelemetry/api": "^1.9.0",
151
- "@opentelemetry/sdk-trace-base": "^1.30.0",
152
- "@opentelemetry/sdk-trace-node": "^1.30.0",
153
- "better-call": "1.3.5",
155
+ "@better-auth/utils": "0.4.2",
156
+ "@better-fetch/fetch": "1.3.1",
154
157
  "@cloudflare/workers-types": "^4.20250121.0",
155
- "jose": "^6.1.3",
156
- "kysely": "^0.28.14",
157
- "nanostores": "^1.1.1",
158
+ "@opentelemetry/api": "^1.9.1",
159
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
160
+ "@opentelemetry/sdk-trace-node": "^2.8.0",
161
+ "better-call": "1.3.7",
162
+ "jose": "^6.2.3",
163
+ "kysely": "^0.28.17 || ^0.29.0",
164
+ "nanostores": "^1.3.0",
158
165
  "tsdown": "0.21.1"
159
166
  },
160
167
  "peerDependencies": {
161
- "@better-auth/utils": "0.4.0",
162
- "@better-fetch/fetch": "1.1.21",
163
- "@opentelemetry/api": "^1.9.0",
164
- "better-call": "1.3.5",
168
+ "@better-auth/utils": "0.4.2",
169
+ "@better-fetch/fetch": "1.3.1",
165
170
  "@cloudflare/workers-types": ">=4",
171
+ "@opentelemetry/api": "^1.9.0",
172
+ "better-call": "1.3.7",
166
173
  "jose": "^6.1.0",
167
- "kysely": "^0.28.5",
174
+ "kysely": "^0.28.5 || ^0.29.0",
168
175
  "nanostores": "^1.0.1"
169
176
  },
170
177
  "peerDependenciesMeta": {
171
178
  "@cloudflare/workers-types": {
172
179
  "optional": true
180
+ },
181
+ "@opentelemetry/api": {
182
+ "optional": true
173
183
  }
174
184
  },
175
185
  "scripts": {
package/src/api/index.ts CHANGED
@@ -3,9 +3,53 @@ import type {
3
3
  EndpointOptions,
4
4
  StrictEndpoint,
5
5
  } from "better-call";
6
- import { createEndpoint, createMiddleware } from "better-call";
6
+ import {
7
+ createEndpoint,
8
+ createMiddleware,
9
+ kAPIErrorHeaderSymbol,
10
+ } from "better-call";
7
11
  import { runWithEndpointContext } from "../context";
8
12
  import type { AuthContext } from "../types";
13
+ import { isAPIError } from "../utils/is-api-error";
14
+
15
+ /**
16
+ * Response headers that forbid any intermediary (proxy, CDN, browser) from
17
+ * caching a response body. Credential-bearing responses (access/refresh tokens,
18
+ * ID tokens, client secrets, device codes) must carry them.
19
+ *
20
+ * Set `metadata: { noStore: true }` on an endpoint and {@link createAuthEndpoint}
21
+ * applies these to the responses its handler produces: the success body and any
22
+ * error the handler throws. A request rejected by schema or media-type
23
+ * validation before the handler runs is not covered, and carries no credentials
24
+ * to protect. Spread them into a hand-built `Response` or `APIError`'s headers
25
+ * for the rare endpoint that constructs its own response.
26
+ *
27
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
28
+ */
29
+ export const NO_STORE_HEADERS = {
30
+ "Cache-Control": "no-store",
31
+ Pragma: "no-cache",
32
+ } as const;
33
+
34
+ /**
35
+ * Better-call's createEndpoint re-throws APIError without exposing the headers
36
+ * accumulated on ctx.responseHeaders (e.g. Set-Cookie from deleteSessionCookie
37
+ * before throw). Attach them to the error via kAPIErrorHeaderSymbol — matching
38
+ * better-call's createMiddleware contract so the outer pipeline can merge them
39
+ * into the response.
40
+ */
41
+ function attachResponseHeadersToAPIError(
42
+ responseHeaders: Headers | undefined,
43
+ e: unknown,
44
+ ): void {
45
+ if (!isAPIError(e) || !responseHeaders) return;
46
+ Object.defineProperty(e, kAPIErrorHeaderSymbol, {
47
+ enumerable: false,
48
+ configurable: true,
49
+ value: responseHeaders,
50
+ writable: false,
51
+ });
52
+ }
9
53
 
10
54
  export const optionsMiddleware = createMiddleware(async () => {
11
55
  /**
@@ -76,6 +120,32 @@ export function createAuthEndpoint<
76
120
  const handler: EndpointHandler<Path, Opts, R> =
77
121
  typeof handlerOrOptions === "function" ? handlerOrOptions : handlerOrNever;
78
122
 
123
+ // Endpoints that return credentials declare `metadata: { noStore: true }`.
124
+ // Emit the no-store headers at the boundary, before the handler runs, so they
125
+ // land on every response the handler produces: a success harvests
126
+ // `responseHeaders`, and a thrown error carries the same headers through
127
+ // `attachResponseHeadersToAPIError`. Validation that rejects the request
128
+ // before the handler runs is not covered (and returns no credentials).
129
+ const noStore =
130
+ (options as { metadata?: { noStore?: boolean } }).metadata?.noStore ===
131
+ true;
132
+
133
+ // todo: prettify the code, we want to call `runWithEndpointContext` to top level
134
+ const wrapped: EndpointHandler<Path, Opts, R> = async (ctx) => {
135
+ if (noStore) {
136
+ for (const [name, value] of Object.entries(NO_STORE_HEADERS)) {
137
+ ctx.setHeader(name, value);
138
+ }
139
+ }
140
+ const runtimeCtx = ctx as unknown as { responseHeaders?: Headers };
141
+ try {
142
+ return await runWithEndpointContext(ctx as any, () => handler(ctx));
143
+ } catch (e) {
144
+ attachResponseHeadersToAPIError(runtimeCtx.responseHeaders, e);
145
+ throw e;
146
+ }
147
+ };
148
+
79
149
  if (path) {
80
150
  return createEndpoint(
81
151
  path,
@@ -83,8 +153,7 @@ export function createAuthEndpoint<
83
153
  ...options,
84
154
  use: [...(options?.use || []), ...use],
85
155
  },
86
- // todo: prettify the code, we want to call `runWithEndpointContext` to top level
87
- async (ctx) => runWithEndpointContext(ctx as any, () => handler(ctx)),
156
+ wrapped,
88
157
  );
89
158
  }
90
159
 
@@ -93,11 +162,58 @@ export function createAuthEndpoint<
93
162
  ...options,
94
163
  use: [...(options?.use || []), ...use],
95
164
  },
96
- // todo: prettify the code, we want to call `runWithEndpointContext` to top level
97
- async (ctx) => runWithEndpointContext(ctx as any, () => handler(ctx)),
165
+ wrapped,
98
166
  );
99
167
  }
100
168
 
169
+ /**
170
+ * Set `metadata.SERVER_ONLY` while preserving any existing metadata
171
+ * (`$Infer`, `openapi`, ...).
172
+ */
173
+ function withServerOnly<Options extends EndpointOptions>(
174
+ options: Options,
175
+ ): Options {
176
+ return {
177
+ ...options,
178
+ metadata: { ...options.metadata, SERVER_ONLY: true },
179
+ } as Options;
180
+ }
181
+
182
+ export namespace createAuthEndpoint {
183
+ /**
184
+ * Declare a **server-only** endpoint.
185
+ *
186
+ * The endpoint is callable through `auth.api.*` from trusted server code but is
187
+ * never registered on the HTTP router and never emitted into the OpenAPI
188
+ * schema. It takes no path because it has no URL to be reached at.
189
+ *
190
+ * Prefer this over the path-less `createAuthEndpoint({ ... }, handler)` form.
191
+ * Setting `metadata.SERVER_ONLY` makes the intent explicit at the call site and
192
+ * keeps the endpoint off the HTTP surface even if a path is later added by
193
+ * mistake: better-call's router skips an endpoint when its path is missing *or*
194
+ * when `SERVER_ONLY` is set, so the two together are defense in depth. Relying
195
+ * on path omission alone is invisible and one keystroke away from exposure.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * viewBackupCodes: createAuthEndpoint.serverOnly(
200
+ * { method: "POST", body: schema },
201
+ * async (ctx) => { ... },
202
+ * )
203
+ * ```
204
+ */
205
+ export function serverOnly<
206
+ Path extends string,
207
+ Options extends EndpointOptions,
208
+ R,
209
+ >(
210
+ options: Options,
211
+ handler: EndpointHandler<Path, Options, R>,
212
+ ): StrictEndpoint<Path, Options, R> {
213
+ return createAuthEndpoint(withServerOnly(options), handler);
214
+ }
215
+ }
216
+
101
217
  export type AuthEndpoint<
102
218
  Path extends string,
103
219
  Opts extends EndpointOptions,
@@ -1,11 +1,15 @@
1
1
  import type { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
3
3
  import type { DBAdapter, DBTransactionAdapter } from "../db/adapter";
4
+ import type { BetterAuthOptions } from "../types";
4
5
  import { __getBetterAuthGlobal } from "./global";
5
6
 
7
+ type StoredAdapter = DBTransactionAdapter<BetterAuthOptions>;
8
+
6
9
  type HookContext = {
7
- adapter: DBTransactionAdapter;
10
+ adapter: StoredAdapter;
8
11
  pendingHooks: Array<() => Promise<void>>;
12
+ isTransactionActive: boolean;
9
13
  };
10
14
 
11
15
  const ensureAsyncStorage = async () => {
@@ -27,21 +31,29 @@ export const getCurrentDBAdapterAsyncLocalStorage = async () => {
27
31
  return ensureAsyncStorage();
28
32
  };
29
33
 
30
- export const getCurrentAdapter = async (
31
- fallback: DBTransactionAdapter,
32
- ): Promise<DBTransactionAdapter> => {
34
+ export const getCurrentAdapter = async <
35
+ Options extends BetterAuthOptions = BetterAuthOptions,
36
+ >(
37
+ fallback: DBTransactionAdapter<Options>,
38
+ ): Promise<DBTransactionAdapter<Options>> => {
33
39
  return ensureAsyncStorage()
34
40
  .then((als) => {
35
41
  const store = als.getStore();
36
- return store?.adapter || fallback;
42
+ return (
43
+ (store?.adapter as DBTransactionAdapter<Options> | undefined) ||
44
+ fallback
45
+ );
37
46
  })
38
47
  .catch(() => {
39
48
  return fallback;
40
49
  });
41
50
  };
42
51
 
43
- export const runWithAdapter = async <R>(
44
- adapter: DBAdapter,
52
+ export const runWithAdapter = async <
53
+ R,
54
+ Options extends BetterAuthOptions = BetterAuthOptions,
55
+ >(
56
+ adapter: DBAdapter<Options>,
45
57
  fn: () => R,
46
58
  ): Promise<R> => {
47
59
  let called = false;
@@ -53,7 +65,14 @@ export const runWithAdapter = async <R>(
53
65
  let error: unknown;
54
66
  let hasError = false;
55
67
  try {
56
- result = await als.run({ adapter, pendingHooks }, fn);
68
+ result = await als.run(
69
+ {
70
+ adapter: adapter as unknown as StoredAdapter,
71
+ pendingHooks,
72
+ isTransactionActive: false,
73
+ },
74
+ fn,
75
+ );
57
76
  } catch (err) {
58
77
  error = err;
59
78
  hasError = true;
@@ -75,21 +94,35 @@ export const runWithAdapter = async <R>(
75
94
  });
76
95
  };
77
96
 
78
- export const runWithTransaction = async <R>(
79
- adapter: DBAdapter,
97
+ export const runWithTransaction = async <
98
+ R,
99
+ Options extends BetterAuthOptions = BetterAuthOptions,
100
+ >(
101
+ adapter: DBAdapter<Options>,
80
102
  fn: () => R,
81
103
  ): Promise<R> => {
82
- let called = true;
104
+ let called = false;
83
105
  return ensureAsyncStorage()
84
106
  .then(async (als) => {
85
107
  called = true;
108
+ const store = als.getStore();
109
+ if (store?.isTransactionActive) {
110
+ return fn();
111
+ }
86
112
  const pendingHooks: Array<() => Promise<void>> = [];
87
113
  let result: Awaited<R>;
88
114
  let error: unknown;
89
115
  let hasError = false;
90
116
  try {
91
117
  result = await adapter.transaction(async (trx) => {
92
- return als.run({ adapter: trx, pendingHooks }, fn);
118
+ return als.run(
119
+ {
120
+ adapter: trx as unknown as StoredAdapter,
121
+ pendingHooks,
122
+ isTransactionActive: true,
123
+ },
124
+ fn,
125
+ );
93
126
  });
94
127
  } catch (e) {
95
128
  hasError = true;