better-auth 1.7.0-beta.1 → 1.7.0-beta.2

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.
@@ -98,16 +98,43 @@ function toAuthEndpoints(endpoints, ctx) {
98
98
  [ATTR_HTTP_ROUTE]: route,
99
99
  [ATTR_OPERATION_ID]: operationId
100
100
  }, () => endpoint(internalContext))).catch((e) => {
101
- if (isAPIError(e))
102
- /**
103
- * API Errors from response are caught
104
- * and returned to hooks
105
- */
106
- return {
107
- response: e,
108
- status: e.statusCode,
109
- headers: e.headers ? new Headers(e.headers) : null
110
- };
101
+ if (isAPIError(e)) {
102
+ /**
103
+ * API Errors from response are caught
104
+ * and returned to hooks.
105
+ *
106
+ * Headers come from two sources that must both
107
+ * survive:
108
+ * - `kAPIErrorHeaderSymbol`: ctx.responseHeaders
109
+ * accumulated via c.setCookie / c.setHeader
110
+ * before the throw.
111
+ * - `e.headers`: explicit headers on the APIError
112
+ * (e.g. `location` from c.redirect).
113
+ *
114
+ * Start from the accumulated ctx headers, then
115
+ * apply e.headers on top — appending `set-cookie`
116
+ * and setting others — so explicit APIError
117
+ * headers override while cookies accumulate.
118
+ */
119
+ const ctxHeaders = e[kAPIErrorHeaderSymbol];
120
+ const errHeaders = e.headers ? new Headers(e.headers) : null;
121
+ let headers = null;
122
+ if (ctxHeaders || errHeaders) {
123
+ headers = new Headers();
124
+ ctxHeaders?.forEach((value, key) => {
125
+ headers.append(key, value);
126
+ });
127
+ errHeaders?.forEach((value, key) => {
128
+ if (key.toLowerCase() === "set-cookie") headers.append(key, value);
129
+ else headers.set(key, value);
130
+ });
131
+ }
132
+ return {
133
+ response: e,
134
+ status: e.statusCode,
135
+ headers
136
+ };
137
+ }
111
138
  throw e;
112
139
  });
113
140
  if (result && result instanceof Response) return result;
@@ -116,7 +143,26 @@ function toAuthEndpoints(endpoints, ctx) {
116
143
  const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
117
144
  if (after.response) result.response = after.response;
118
145
  if (isAPIError(result.response) && shouldPublishLog(authContext.logger.level, "debug")) result.response.stack = result.response.errorStack;
119
- if (isAPIError(result.response) && !shouldReturnResponse) throw result.response;
146
+ if (isAPIError(result.response) && !shouldReturnResponse) {
147
+ /**
148
+ * Non-response path: we re-throw the raw APIError
149
+ * to callers of `auth.api.*`. `result.headers`
150
+ * holds the merged ctx + explicit headers (see
151
+ * catch block above) — rewrite
152
+ * `kAPIErrorHeaderSymbol` with the merged set so
153
+ * downstream pipelines (e.g. better-call's
154
+ * response builder, or an outer hook catch) see
155
+ * the same headers we'd have written on the
156
+ * response.
157
+ */
158
+ if (result.headers) Object.defineProperty(result.response, kAPIErrorHeaderSymbol, {
159
+ enumerable: false,
160
+ configurable: true,
161
+ writable: false,
162
+ value: result.headers
163
+ });
164
+ throw result.response;
165
+ }
120
166
  return shouldReturnResponse ? toResponse(result.response, {
121
167
  headers: result.headers,
122
168
  status: result.status
@@ -67,7 +67,7 @@ const getClientConfig = (options, loadEnv) => {
67
67
  const atomListeners = [{
68
68
  signal: "$sessionSignal",
69
69
  matcher(path) {
70
- return path === "/sign-out" || path === "/update-user" || path === "/update-session" || path === "/sign-up/email" || path === "/sign-in/email" || path === "/delete-user" || path === "/verify-email" || path === "/revoke-sessions" || path === "/revoke-session" || path === "/change-email";
70
+ return path === "/sign-out" || path === "/update-user" || path === "/update-session" || path === "/sign-up/email" || path === "/sign-in/email" || path === "/delete-user" || path === "/verify-email" || path === "/revoke-sessions" || path === "/revoke-session" || path === "/revoke-other-sessions" || path === "/change-email" || path === "/change-password";
71
71
  },
72
72
  callback(path) {
73
73
  if (path === "/sign-out") broadcastSessionUpdate("signout");
@@ -5,6 +5,7 @@ import { createInternalAdapter } from "../db/internal-adapter.mjs";
5
5
  import { env } from "@better-auth/core/env";
6
6
  import { BetterAuthError } from "@better-auth/core/error";
7
7
  import { defu } from "defu";
8
+ import { isLoopbackHost } from "@better-auth/core/utils/host";
8
9
  //#region src/context/helpers.ts
9
10
  async function runPluginInit(context) {
10
11
  let options = context.options;
@@ -62,7 +63,7 @@ async function getTrustedOrigins(options, request) {
62
63
  const allowedHosts = options.baseURL.allowedHosts;
63
64
  for (const host of allowedHosts) if (!host.includes("://")) {
64
65
  trustedOrigins.push(`https://${host}`);
65
- if (host.includes("localhost") || host.includes("127.0.0.1")) trustedOrigins.push(`http://${host}`);
66
+ if (isLoopbackHost(host)) trustedOrigins.push(`http://${host}`);
66
67
  } else trustedOrigins.push(host);
67
68
  if (options.baseURL.fallback) try {
68
69
  trustedOrigins.push(new URL(options.baseURL.fallback).origin);
@@ -7,9 +7,20 @@ interface CookieAttributes {
7
7
  path?: string | undefined;
8
8
  secure?: boolean | undefined;
9
9
  httponly?: boolean | undefined;
10
+ partitioned?: boolean | undefined;
10
11
  samesite?: ("strict" | "lax" | "none") | undefined;
11
12
  [key: string]: any;
12
13
  }
14
+ interface ParsedCookieOptions {
15
+ maxAge?: number | undefined;
16
+ expires?: Date | undefined;
17
+ domain?: string | undefined;
18
+ path?: string | undefined;
19
+ secure?: boolean | undefined;
20
+ httpOnly?: boolean | undefined;
21
+ partitioned?: boolean | undefined;
22
+ sameSite?: CookieAttributes["samesite"];
23
+ }
13
24
  declare const SECURE_COOKIE_PREFIX = "__Secure-";
14
25
  declare const HOST_COOKIE_PREFIX = "__Host-";
15
26
  /**
@@ -21,8 +32,9 @@ declare function stripSecureCookiePrefix(cookieName: string): string;
21
32
  */
22
33
  declare function splitSetCookieHeader(setCookie: string): string[];
23
34
  declare function parseSetCookieHeader(setCookie: string): Map<string, CookieAttributes>;
35
+ declare function toCookieOptions(attributes: CookieAttributes): ParsedCookieOptions;
24
36
  declare function setCookieToHeader(headers: Headers): (context: {
25
37
  response: Response;
26
38
  }) => void;
27
39
  //#endregion
28
- export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix };
40
+ export { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -78,6 +78,9 @@ function parseSetCookieHeader(setCookie) {
78
78
  case "samesite":
79
79
  attrObj.samesite = attrValue ? attrValue.trim().toLowerCase() : void 0;
80
80
  break;
81
+ case "partitioned":
82
+ attrObj.partitioned = true;
83
+ break;
81
84
  default:
82
85
  attrObj[normalizedAttrName] = attrValue ? attrValue.trim() : true;
83
86
  break;
@@ -87,6 +90,18 @@ function parseSetCookieHeader(setCookie) {
87
90
  });
88
91
  return cookies;
89
92
  }
93
+ function toCookieOptions(attributes) {
94
+ return {
95
+ maxAge: attributes["max-age"],
96
+ expires: attributes.expires,
97
+ domain: attributes.domain,
98
+ path: attributes.path,
99
+ secure: attributes.secure,
100
+ httpOnly: attributes.httponly,
101
+ sameSite: attributes.samesite,
102
+ partitioned: attributes.partitioned
103
+ };
104
+ }
90
105
  function setCookieToHeader(headers) {
91
106
  return (context) => {
92
107
  const setCookieHeader = context.response.headers.get("set-cookie");
@@ -104,4 +119,4 @@ function setCookieToHeader(headers) {
104
119
  };
105
120
  }
106
121
  //#endregion
107
- export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix };
122
+ export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -1,5 +1,5 @@
1
1
  import { Session, User } from "../types/models.mjs";
2
- import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix } from "./cookie-utils.mjs";
2
+ import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
3
3
  import { createSessionStore, getAccountCookie, getChunkedCookie } from "./session-store.mjs";
4
4
  import { BetterAuthCookie, BetterAuthCookies, BetterAuthOptions, GenericEndpointContext } from "@better-auth/core";
5
5
  import * as better_call0 from "better-call";
@@ -116,4 +116,4 @@ declare const getCookieCache: <S extends {
116
116
  version?: string | ((session: Session & Record<string, any>, user: User & Record<string, any>) => string) | ((session: Session & Record<string, any>, user: User & Record<string, any>) => Promise<string>);
117
117
  } | undefined) => Promise<S | null>;
118
118
  //#endregion
119
- export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix };
119
+ export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -4,7 +4,7 @@ import { parseUserOutput } from "../db/schema.mjs";
4
4
  import { getDate } from "../utils/date.mjs";
5
5
  import { isPromise } from "../utils/is-promise.mjs";
6
6
  import { sec } from "../utils/time.mjs";
7
- import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix } from "./cookie-utils.mjs";
7
+ import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
8
8
  import { createAccountStore, createSessionStore, getAccountCookie, getChunkedCookie, setAccountCookie } from "./session-store.mjs";
9
9
  import { env, isProduction } from "@better-auth/core/env";
10
10
  import { BetterAuthError } from "@better-auth/core/error";
@@ -258,4 +258,4 @@ const getCookieCache = async (request, config) => {
258
258
  return null;
259
259
  };
260
260
  //#endregion
261
- export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix };
261
+ export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
@@ -1,4 +1,4 @@
1
- import { parseSetCookieHeader } from "../cookies/cookie-utils.mjs";
1
+ import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { setShouldSkipSessionRefresh } from "../api/state/should-session-refresh.mjs";
3
3
  import { PACKAGE_VERSION } from "../version.mjs";
4
4
  import { createAuthMiddleware } from "@better-auth/core/api";
@@ -70,16 +70,8 @@ const nextCookies = () => {
70
70
  }
71
71
  parsed.forEach((value, key) => {
72
72
  if (!key) return;
73
- const opts = {
74
- sameSite: value.samesite,
75
- secure: value.secure,
76
- maxAge: value["max-age"],
77
- httpOnly: value.httponly,
78
- domain: value.domain,
79
- path: value.path
80
- };
81
73
  try {
82
- cookieHelper.set(key, value.value, opts);
74
+ cookieHelper.set(key, value.value, toCookieOptions(value));
83
75
  } catch {}
84
76
  });
85
77
  return;
@@ -1,4 +1,4 @@
1
- import { parseSetCookieHeader } from "../cookies/cookie-utils.mjs";
1
+ import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { PACKAGE_VERSION } from "../version.mjs";
3
3
  import { createAuthMiddleware } from "@better-auth/core/api";
4
4
  //#region src/integrations/svelte-kit.ts
@@ -36,15 +36,10 @@ const sveltekitCookies = (getRequestEvent) => {
36
36
  const event = getRequestEvent();
37
37
  if (!event) return;
38
38
  const parsed = parseSetCookieHeader(setCookies);
39
- for (const [name, { value, ...ops }] of parsed) try {
40
- event.cookies.set(name, value, {
41
- sameSite: ops.samesite,
42
- path: ops.path || "/",
43
- expires: ops.expires,
44
- secure: ops.secure,
45
- httpOnly: ops.httponly,
46
- domain: ops.domain,
47
- maxAge: ops["max-age"]
39
+ for (const [name, attributes] of parsed) try {
40
+ event.cookies.set(name, attributes.value, {
41
+ ...toCookieOptions(attributes),
42
+ path: attributes.path || "/"
48
43
  });
49
44
  } catch {}
50
45
  }
@@ -1,4 +1,4 @@
1
- import { parseSetCookieHeader } from "../cookies/cookie-utils.mjs";
1
+ import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { PACKAGE_VERSION } from "../version.mjs";
3
3
  import { createAuthMiddleware } from "@better-auth/core/api";
4
4
  //#region src/integrations/tanstack-start-solid.ts
@@ -37,16 +37,8 @@ const tanstackStartCookies = () => {
37
37
  const { setCookie } = await import("@tanstack/solid-start/server");
38
38
  parsed.forEach((value, key) => {
39
39
  if (!key) return;
40
- const opts = {
41
- sameSite: value.samesite,
42
- secure: value.secure,
43
- maxAge: value["max-age"],
44
- httpOnly: value.httponly,
45
- domain: value.domain,
46
- path: value.path
47
- };
48
40
  try {
49
- setCookie(key, value.value, opts);
41
+ setCookie(key, value.value, toCookieOptions(value));
50
42
  } catch {}
51
43
  });
52
44
  return;
@@ -1,4 +1,4 @@
1
- import { parseSetCookieHeader } from "../cookies/cookie-utils.mjs";
1
+ import { parseSetCookieHeader, toCookieOptions } from "../cookies/cookie-utils.mjs";
2
2
  import { PACKAGE_VERSION } from "../version.mjs";
3
3
  import { createAuthMiddleware } from "@better-auth/core/api";
4
4
  //#region src/integrations/tanstack-start.ts
@@ -37,16 +37,8 @@ const tanstackStartCookies = () => {
37
37
  const { setCookie } = await import("@tanstack/react-start/server");
38
38
  parsed.forEach((value, key) => {
39
39
  if (!key) return;
40
- const opts = {
41
- sameSite: value.samesite,
42
- secure: value.secure,
43
- maxAge: value["max-age"],
44
- httpOnly: value.httponly,
45
- domain: value.domain,
46
- path: value.path
47
- };
48
40
  try {
49
- setCookie(key, value.value, opts);
41
+ setCookie(key, value.value, toCookieOptions(value));
50
42
  } catch {}
51
43
  });
52
44
  return;
@@ -29,7 +29,7 @@ async function generateState(c, link, additionalData) {
29
29
  }
30
30
  }
31
31
  async function parseState(c) {
32
- const state = c.query.state || c.body.state;
32
+ const state = c.query.state || c.body?.state;
33
33
  const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
34
34
  let parsedData;
35
35
  try {
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.7.0-beta.1";
2
+ var version = "1.7.0-beta.2";
3
3
  //#endregion
4
4
  export { version };
@@ -2,7 +2,8 @@ import * as _better_auth_core0 from "@better-auth/core";
2
2
  import { BetterAuthOptions, GenericEndpointContext } from "@better-auth/core";
3
3
  import { Session, User } from "@better-auth/core/db";
4
4
  import * as better_call0 from "better-call";
5
- import * as z from "zod";
5
+ import * as zod from "zod";
6
+ import * as zod_v4_core0 from "zod/v4/core";
6
7
 
7
8
  //#region src/plugins/custom-session/index.d.ts
8
9
  declare module "@better-auth/core" {
@@ -34,10 +35,10 @@ declare const customSession: <Returns extends Record<string, any>, O extends Bet
34
35
  endpoints: {
35
36
  getSession: better_call0.StrictEndpoint<"/get-session", {
36
37
  method: "GET";
37
- query: z.ZodOptional<z.ZodObject<{
38
- disableCookieCache: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodPipe<z.ZodString, z.ZodTransform<boolean, string>>]>>;
39
- disableRefresh: z.ZodOptional<z.ZodBoolean>;
40
- }, z.core.$strip>>;
38
+ query: zod.ZodOptional<zod.ZodObject<{
39
+ disableCookieCache: zod.ZodOptional<zod.ZodCoercedBoolean<unknown>>;
40
+ disableRefresh: zod.ZodOptional<zod.ZodCoercedBoolean<unknown>>;
41
+ }, zod_v4_core0.$strip>>;
41
42
  metadata: {
42
43
  CUSTOM_SESSION: boolean;
43
44
  openapi: {
@@ -1,14 +1,10 @@
1
- import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
1
+ import { parseSetCookieHeader, toCookieOptions } from "../../cookies/cookie-utils.mjs";
2
+ import { getSessionQuerySchema } from "../../cookies/session-store.mjs";
2
3
  import { getSession } from "../../api/routes/session.mjs";
3
4
  import { PACKAGE_VERSION } from "../../version.mjs";
4
5
  import { getEndpointResponse } from "../../utils/plugin-helper.mjs";
5
6
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
6
- import * as z from "zod";
7
7
  //#region src/plugins/custom-session/index.ts
8
- const getSessionQuerySchema = z.optional(z.object({
9
- disableCookieCache: z.boolean().meta({ description: "Disable cookie cache and fetch session from database" }).or(z.string().transform((v) => v === "true")).optional(),
10
- disableRefresh: z.boolean().meta({ description: "Disable session refresh. Useful for checking session status, without updating the session" }).optional()
11
- }));
12
8
  const customSession = (fn, options, pluginOptions) => {
13
9
  return {
14
10
  id: "custom-session",
@@ -53,15 +49,7 @@ const customSession = (fn, options, pluginOptions) => {
53
49
  if (!session?.response) return ctx.json(null);
54
50
  const fnResult = await fn(session.response, ctx);
55
51
  for (const cookieStr of session.headers.getSetCookie()) parseSetCookieHeader(cookieStr).forEach((attrs, name) => {
56
- ctx.setCookie(name, attrs.value, {
57
- maxAge: attrs["max-age"],
58
- expires: attrs.expires,
59
- domain: attrs.domain,
60
- path: attrs.path,
61
- secure: attrs.secure,
62
- httpOnly: attrs.httponly,
63
- sameSite: attrs.samesite
64
- });
52
+ ctx.setCookie(name, attrs.value, toCookieOptions(attrs));
65
53
  });
66
54
  session.headers.delete("set-cookie");
67
55
  session.headers.forEach((value, key) => {
@@ -1,6 +1,6 @@
1
1
  import { AccessControl, ArrayElement, Statements } from "../access/types.mjs";
2
2
  import { OrganizationOptions } from "./types.mjs";
3
- import { InferInvitation, InferMember, InferOrganization, InferTeam, OrganizationSchema, Team, TeamMember } from "./schema.mjs";
3
+ import { InferInvitation, InferMember, InferOrganization, InferTeam, OrganizationSchema, TeamMember } from "./schema.mjs";
4
4
  import { ORGANIZATION_ERROR_CODES } from "./error-codes.mjs";
5
5
  import { createOrgRole, deleteOrgRole, getOrgRole, listOrgRoles, updateOrgRole } from "./routes/crud-access-control.mjs";
6
6
  import { acceptInvitation, cancelInvitation, createInvitation, getInvitation, listInvitations, listUserInvitations, rejectInvitation } from "./routes/crud-invites.mjs";
@@ -30,7 +30,7 @@ type DefaultOrganizationPlugin<Options extends OrganizationOptions> = {
30
30
  Member: InferMember<Options>;
31
31
  Team: Options["teams"] extends {
32
32
  enabled: true;
33
- } ? Team : never;
33
+ } ? InferTeam<Options> : never;
34
34
  TeamMember: Options["teams"] extends {
35
35
  enabled: true;
36
36
  } ? TeamMember : never;
@@ -249,7 +249,7 @@ type OrganizationPlugin<O extends OrganizationOptions> = {
249
249
  Member: InferMember<O>;
250
250
  Team: O["teams"] extends {
251
251
  enabled: true;
252
- } ? Team : never;
252
+ } ? InferTeam<O> : never;
253
253
  TeamMember: O["teams"] extends {
254
254
  enabled: true;
255
255
  } ? TeamMember : never;
@@ -300,7 +300,7 @@ declare function organization<O extends OrganizationOptions & {
300
300
  Member: InferMember<O>;
301
301
  Team: O["teams"] extends {
302
302
  enabled: true;
303
- } ? Team : never;
303
+ } ? InferTeam<O> : never;
304
304
  TeamMember: O["teams"] extends {
305
305
  enabled: true;
306
306
  } ? TeamMember : never;
@@ -336,7 +336,7 @@ declare function organization<O extends OrganizationOptions & {
336
336
  Member: InferMember<O>;
337
337
  Team: O["teams"] extends {
338
338
  enabled: true;
339
- } ? Team : never;
339
+ } ? InferTeam<O> : never;
340
340
  TeamMember: O["teams"] extends {
341
341
  enabled: true;
342
342
  } ? TeamMember : never;
@@ -372,7 +372,7 @@ declare function organization<O extends OrganizationOptions & {
372
372
  Member: InferMember<O>;
373
373
  Team: O["teams"] extends {
374
374
  enabled: true;
375
- } ? Team : never;
375
+ } ? InferTeam<O> : never;
376
376
  TeamMember: O["teams"] extends {
377
377
  enabled: true;
378
378
  } ? TeamMember : never;
@@ -576,6 +576,10 @@ declare const setActiveTeam: <O extends OrganizationOptions>(options: O) => bett
576
576
  } | null>;
577
577
  declare const listUserTeams: <O extends OrganizationOptions>(options: O) => better_call0.StrictEndpoint<"/organization/list-user-teams", {
578
578
  method: "GET";
579
+ query: z.ZodOptional<z.ZodObject<{
580
+ userId: z.ZodOptional<z.ZodString>;
581
+ organizationId: z.ZodOptional<z.ZodString>;
582
+ }, z.core.$strip>>;
579
583
  metadata: {
580
584
  openapi: {
581
585
  description: string;
@@ -417,8 +417,12 @@ const setActiveTeam = (options) => createAuthEndpoint("/organization/set-active-
417
417
  });
418
418
  const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-teams", {
419
419
  method: "GET",
420
+ query: z.object({
421
+ userId: z.string().optional().meta({ description: "The user ID to list teams for. Defaults to the current session user." }),
422
+ organizationId: z.string().optional().meta({ description: "The organization ID to scope the team list to. When omitted on a self-query, teams are returned across every organization the user belongs to. When querying another user, falls back to the session's active organization and is required if there is no active organization." })
423
+ }).optional(),
420
424
  metadata: { openapi: {
421
- description: "List all teams that the current user is a part of.",
425
+ description: "List teams for a user. Without parameters, returns teams for the current user across every organization they belong to. Pass `organizationId` to scope the result to a specific organization. Pass `userId` to list teams for another member; this requires `member:update` permission in the target organization (the explicit `organizationId` if provided, otherwise the session's active organization).",
422
426
  responses: { "200": {
423
427
  description: "Teams retrieved successfully",
424
428
  content: { "application/json": { schema: {
@@ -436,7 +440,40 @@ const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-t
436
440
  use: [orgMiddleware, orgSessionMiddleware]
437
441
  }, async (ctx) => {
438
442
  const session = ctx.context.session;
439
- const teams = await getOrgAdapter(ctx.context, ctx.context.orgOptions).listTeamsByUser({ userId: session.user.id });
443
+ const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
444
+ const targetUserId = ctx.query?.userId || session.user.id;
445
+ const isSelf = targetUserId === session.user.id;
446
+ const organizationId = ctx.query?.organizationId || session.session.activeOrganizationId;
447
+ const isExplicitOrg = Boolean(ctx.query?.organizationId);
448
+ if (!isSelf) {
449
+ if (!organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
450
+ const requesterMember = await adapter.findMemberByOrgId({
451
+ userId: session.user.id,
452
+ organizationId
453
+ });
454
+ if (!requesterMember) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
455
+ if (!await hasPermission({
456
+ role: requesterMember.role,
457
+ options: ctx.context.orgOptions,
458
+ permissions: { member: ["update"] },
459
+ organizationId
460
+ }, ctx)) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER);
461
+ if (!await adapter.findMemberByOrgId({
462
+ userId: targetUserId,
463
+ organizationId
464
+ })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION);
465
+ const teams = await adapter.listTeamsByUser({ userId: targetUserId });
466
+ return ctx.json(teams.filter((t) => t.organizationId === organizationId));
467
+ }
468
+ if (isExplicitOrg && organizationId) {
469
+ if (!await adapter.findMemberByOrgId({
470
+ userId: session.user.id,
471
+ organizationId
472
+ })) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
473
+ const teams = await adapter.listTeamsByUser({ userId: session.user.id });
474
+ return ctx.json(teams.filter((t) => t.organizationId === organizationId));
475
+ }
476
+ const teams = await adapter.listTeamsByUser({ userId: session.user.id });
440
477
  return ctx.json(teams);
441
478
  });
442
479
  const listTeamMembersQuerySchema = z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) }));
@@ -16,6 +16,36 @@ declare module "@better-auth/core" {
16
16
  declare const phoneNumber: (options?: PhoneNumberOptions | undefined) => {
17
17
  id: "phone-number";
18
18
  version: string;
19
+ init(): {
20
+ options: {
21
+ databaseHooks: {
22
+ user: {
23
+ update: {
24
+ before(data: Partial<{
25
+ id: string;
26
+ createdAt: Date;
27
+ updatedAt: Date;
28
+ email: string;
29
+ emailVerified: boolean;
30
+ name: string;
31
+ image?: string | null | undefined;
32
+ }> & Record<string, unknown>): Promise<{
33
+ data: {
34
+ [x: string]: unknown;
35
+ id?: string | undefined;
36
+ createdAt?: Date | undefined;
37
+ updatedAt?: Date | undefined;
38
+ email?: string | undefined;
39
+ emailVerified?: boolean | undefined;
40
+ name?: string | undefined;
41
+ image?: string | null | undefined;
42
+ };
43
+ } | undefined>;
44
+ };
45
+ };
46
+ };
47
+ };
48
+ };
19
49
  hooks: {
20
50
  before: {
21
51
  matcher: (ctx: _better_auth_core0.HookEndpointContext) => boolean;
@@ -19,8 +19,16 @@ const phoneNumber = (options) => {
19
19
  return {
20
20
  id: "phone-number",
21
21
  version: PACKAGE_VERSION,
22
+ init() {
23
+ return { options: { databaseHooks: { user: { update: { async before(data) {
24
+ if (opts.phoneNumber in data && data[opts.phoneNumber] === null) return { data: {
25
+ ...data,
26
+ [opts.phoneNumberVerified]: false
27
+ } };
28
+ } } } } } };
29
+ },
22
30
  hooks: { before: [{
23
- matcher: (ctx) => ctx.path === "/update-user" && "phoneNumber" in ctx.body,
31
+ matcher: (ctx) => ctx.path === "/update-user" && "phoneNumber" in ctx.body && ctx.body.phoneNumber !== null,
24
32
  handler: createAuthMiddleware(async (_ctx) => {
25
33
  throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_CANNOT_BE_UPDATED);
26
34
  })
@@ -312,6 +312,11 @@ const verifyPhoneNumber = (opts) => createAuthEndpoint("/phone-number/verify", {
312
312
  [opts.phoneNumber]: ctx.body.phoneNumber,
313
313
  [opts.phoneNumberVerified]: true
314
314
  });
315
+ if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_UPDATE_USER);
316
+ await opts?.callbackOnVerification?.({
317
+ phoneNumber: ctx.body.phoneNumber,
318
+ user
319
+ }, ctx);
315
320
  return ctx.json({
316
321
  status: true,
317
322
  token: session.session.token,
@@ -18,19 +18,28 @@ declare module "@better-auth/core" {
18
18
  * - Auth helpers (login, getAuthHeaders, getCookies)
19
19
  * - OTP capture (when captureOTP: true)
20
20
  *
21
+ * This plugin does not register public HTTP routes or API endpoints, but it does
22
+ * expose privileged helpers on `ctx.test` for creating sessions and mutating data.
23
+ * Prefer including it in a test-only auth instance such as `auth.test.ts` instead
24
+ * of a production auth config.
25
+ *
26
+ * If you conditionally spread it into `plugins`, TypeScript may stop inferring
27
+ * `ctx.test` correctly. A separate test-only auth instance keeps the helpers
28
+ * typed without adding the plugin to your production auth config.
29
+ *
21
30
  * @example
22
31
  * ```ts
23
32
  * import { betterAuth } from "better-auth";
24
33
  * import { testUtils } from "better-auth/plugins";
25
34
  *
26
- * export const auth = betterAuth({
35
+ * export const testAuth = betterAuth({
27
36
  * plugins: [
28
37
  * testUtils({ captureOTP: true }),
29
38
  * ],
30
39
  * });
31
40
  *
32
41
  * // In tests, access helpers via context:
33
- * const ctx = await auth.$context;
42
+ * const ctx = await testAuth.$context;
34
43
  * const test = ctx.test;
35
44
  *
36
45
  * const user = test.createUser({ email: "test@example.com" });
@@ -13,19 +13,28 @@ import { createOTPStore } from "./otp-sink.mjs";
13
13
  * - Auth helpers (login, getAuthHeaders, getCookies)
14
14
  * - OTP capture (when captureOTP: true)
15
15
  *
16
+ * This plugin does not register public HTTP routes or API endpoints, but it does
17
+ * expose privileged helpers on `ctx.test` for creating sessions and mutating data.
18
+ * Prefer including it in a test-only auth instance such as `auth.test.ts` instead
19
+ * of a production auth config.
20
+ *
21
+ * If you conditionally spread it into `plugins`, TypeScript may stop inferring
22
+ * `ctx.test` correctly. A separate test-only auth instance keeps the helpers
23
+ * typed without adding the plugin to your production auth config.
24
+ *
16
25
  * @example
17
26
  * ```ts
18
27
  * import { betterAuth } from "better-auth";
19
28
  * import { testUtils } from "better-auth/plugins";
20
29
  *
21
- * export const auth = betterAuth({
30
+ * export const testAuth = betterAuth({
22
31
  * plugins: [
23
32
  * testUtils({ captureOTP: true }),
24
33
  * ],
25
34
  * });
26
35
  *
27
36
  * // In tests, access helpers via context:
28
- * const ctx = await auth.$context;
37
+ * const ctx = await testAuth.$context;
29
38
  * const test = ctx.test;
30
39
  *
31
40
  * const user = test.createUser({ email: "test@example.com" });
@@ -212,13 +212,12 @@ const twoFactor = (options) => {
212
212
  options,
213
213
  hooks: { after: [{
214
214
  matcher(context) {
215
- return context.context.newSession != null && !context.path?.startsWith("/two-factor/");
215
+ return context.path === "/sign-in/email" || context.path === "/sign-in/username" || context.path === "/sign-in/phone-number";
216
216
  },
217
217
  handler: createAuthMiddleware(async (ctx) => {
218
218
  const data = ctx.context.newSession;
219
219
  if (!data) return;
220
220
  if (!data?.user.twoFactorEnabled) return;
221
- if (ctx.context.session) return;
222
221
  const trustDeviceCookieAttrs = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: trustDeviceMaxAge });
223
222
  const trustDeviceCookie = await ctx.getSignedCookie(trustDeviceCookieAttrs.name, ctx.context.secret);
224
223
  if (trustDeviceCookie) {
@@ -1,6 +1,2 @@
1
- import { APIError } from "@better-auth/core/error";
2
-
3
- //#region src/utils/is-api-error.d.ts
4
- declare function isAPIError(error: unknown): error is APIError;
5
- //#endregion
1
+ import { isAPIError } from "@better-auth/core/utils/is-api-error";
6
2
  export { isAPIError };
@@ -1,8 +1,2 @@
1
- import { APIError } from "@better-auth/core/error";
2
- import { APIError as APIError$1 } from "better-call";
3
- //#region src/utils/is-api-error.ts
4
- function isAPIError(error) {
5
- return error instanceof APIError$1 || error instanceof APIError || error?.name === "APIError";
6
- }
7
- //#endregion
1
+ import { isAPIError } from "@better-auth/core/utils/is-api-error";
8
2
  export { isAPIError };
@@ -2,6 +2,21 @@ import { wildcardMatch } from "./wildcard.mjs";
2
2
  import { env } from "@better-auth/core/env";
3
3
  import { BetterAuthError } from "@better-auth/core/error";
4
4
  //#region src/utils/url.ts
5
+ /**
6
+ * Minimal loopback check for dev scheme inference only. Reachable from
7
+ * `client/config.ts` via `getBaseURL`, so we MUST NOT import the full
8
+ * `@better-auth/core/utils/host` classifier here: its `utils/ip` dependency
9
+ * on zod would leak into the client bundle (see `e2e/smoke/test/vite.spec.ts`).
10
+ *
11
+ * Server-side SSRF/loopback checks (oauth redirect matching, trusted-origin
12
+ * resolution, electron fetch gate) continue to use the authoritative
13
+ * `isLoopbackHost` from `@better-auth/core/utils/host`. This helper's only
14
+ * job is picking `http` vs `https` for dev ergonomics.
15
+ */
16
+ function isLoopbackForDevScheme(host) {
17
+ const hostname = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "").toLowerCase();
18
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname.startsWith("127.");
19
+ }
5
20
  function checkHasPath(url) {
6
21
  try {
7
22
  return (new URL(url).pathname.replace(/\/+$/, "") || "/") !== "/";
@@ -146,13 +161,9 @@ function getProtocolFromSource(source, configProtocol, trustedProxyHeaders) {
146
161
  if (url.protocol === "http:" || url.protocol === "https:") return url.protocol.slice(0, -1);
147
162
  } catch {}
148
163
  const host = getHostFromSource(source, trustedProxyHeaders);
149
- if (host && isLoopbackHost(host)) return "http";
164
+ if (host && isLoopbackForDevScheme(host)) return "http";
150
165
  return "https";
151
166
  }
152
- function isLoopbackHost(host) {
153
- const h = host.toLowerCase();
154
- return h === "localhost" || h.startsWith("localhost:") || h === "127.0.0.1" || h.startsWith("127.0.0.1:") || h === "[::1]" || h.startsWith("[::1]:") || h === "0.0.0.0" || h.startsWith("0.0.0.0:");
155
- }
156
167
  /**
157
168
  * Matches a hostname against a host pattern.
158
169
  * Supports wildcard patterns like `*.vercel.app` or `preview-*.myapp.com`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.7.0-beta.1",
3
+ "version": "1.7.0-beta.2",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -489,13 +489,13 @@
489
489
  "kysely": "^0.28.14",
490
490
  "nanostores": "^1.1.1",
491
491
  "zod": "^4.3.6",
492
- "@better-auth/core": "1.7.0-beta.1",
493
- "@better-auth/drizzle-adapter": "1.7.0-beta.1",
494
- "@better-auth/kysely-adapter": "1.7.0-beta.1",
495
- "@better-auth/memory-adapter": "1.7.0-beta.1",
496
- "@better-auth/mongo-adapter": "1.7.0-beta.1",
497
- "@better-auth/prisma-adapter": "1.7.0-beta.1",
498
- "@better-auth/telemetry": "1.7.0-beta.1"
492
+ "@better-auth/core": "1.7.0-beta.2",
493
+ "@better-auth/drizzle-adapter": "1.7.0-beta.2",
494
+ "@better-auth/kysely-adapter": "1.7.0-beta.2",
495
+ "@better-auth/memory-adapter": "1.7.0-beta.2",
496
+ "@better-auth/mongo-adapter": "1.7.0-beta.2",
497
+ "@better-auth/prisma-adapter": "1.7.0-beta.2",
498
+ "@better-auth/telemetry": "1.7.0-beta.2"
499
499
  },
500
500
  "devDependencies": {
501
501
  "@lynx-js/react": "^0.116.3",
@@ -512,7 +512,7 @@
512
512
  "happy-dom": "^20.8.9",
513
513
  "listhen": "^1.9.0",
514
514
  "msw": "^2.12.10",
515
- "next": "^16.2.0",
515
+ "next": "^16.2.3",
516
516
  "oauth2-mock-server": "^8.2.2",
517
517
  "react": "^19.2.4",
518
518
  "react-dom": "^19.2.4",
@@ -531,7 +531,7 @@
531
531
  "@tanstack/solid-start": "^1.0.0",
532
532
  "better-sqlite3": "^12.0.0",
533
533
  "drizzle-kit": ">=0.31.4",
534
- "drizzle-orm": ">=0.41.0",
534
+ "drizzle-orm": "^0.45.2",
535
535
  "mongodb": "^6.0.0 || ^7.0.0",
536
536
  "mysql2": "^3.0.0",
537
537
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",