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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import { AuthContext } from "../types/context.mjs";
6
6
  import "../types/index.mjs";
7
7
  import { OAuthProvider } from "../oauth2/oauth-provider.mjs";
8
8
  import "../oauth2/index.mjs";
9
- import { EndpointContext, EndpointOptions, StrictEndpoint } from "better-call";
9
+ import { EndpointHandler, EndpointOptions, MiddlewareHandler, StrictEndpoint } from "better-call";
10
10
  //#region src/api/index.d.ts
11
11
  /**
12
12
  * Response headers that forbid any intermediary (proxy, CDN, browser) from
@@ -26,12 +26,9 @@ declare const NO_STORE_HEADERS: {
26
26
  readonly "Cache-Control": "no-store";
27
27
  readonly Pragma: "no-cache";
28
28
  };
29
- declare const optionsMiddleware: <InputCtx extends import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>>(inputContext: InputCtx) => Promise<AuthContext>;
29
+ declare const optionsMiddleware: import("better-call").Middleware<import("better-call").MiddlewareOptions, <InputCtx extends import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>>(inputContext: InputCtx) => Promise<AuthContext>>;
30
30
  declare const createAuthMiddleware: {
31
- <Options extends import("better-call").MiddlewareOptions, R>(options: Options, handler: (ctx: import("better-call").MiddlewareContext<Options, {
32
- returned?: unknown | undefined;
33
- responseHeaders?: Headers | undefined;
34
- } & import("@better-auth/core").PluginContext<import("@better-auth/core").BetterAuthOptions> & import("@better-auth/core").InfoContext & {
31
+ <Options extends import("better-call").MiddlewareOptions, R>(options: Options, handler: (ctx: import("better-call").MiddlewareContext<Options, import("@better-auth/core").PluginContext<import("@better-auth/core").BetterAuthOptions> & import("@better-auth/core").InfoContext & {
35
32
  options: import("@better-auth/core").BetterAuthOptions;
36
33
  trustedOrigins: string[];
37
34
  trustedProviders: string[];
@@ -123,6 +120,7 @@ declare const createAuthMiddleware: {
123
120
  updateAge: number;
124
121
  expiresIn: number;
125
122
  freshAge: number;
123
+ cookieCacheSigner?: import("@better-auth/core").CookieCacheSigner | undefined;
126
124
  cookieRefreshCache: false | {
127
125
  enabled: true;
128
126
  updateAge: number;
@@ -156,11 +154,11 @@ declare const createAuthMiddleware: {
156
154
  skipCSRFCheck: boolean;
157
155
  runInBackground: (promise: Promise<unknown>) => void;
158
156
  runInBackgroundOrAwait: (promise: Promise<unknown> | void) => import("@better-auth/core").Awaitable<unknown>;
159
- }>) => Promise<R>): (inputContext: import("better-call").MiddlewareInputContext<Options>) => Promise<R>;
160
- <Options extends import("better-call").MiddlewareOptions, R_1>(handler: (ctx: import("better-call").MiddlewareContext<Options, {
157
+ } & {
161
158
  returned?: unknown | undefined;
162
159
  responseHeaders?: Headers | undefined;
163
- } & import("@better-auth/core").PluginContext<import("@better-auth/core").BetterAuthOptions> & import("@better-auth/core").InfoContext & {
160
+ }>) => Promise<R>): import("better-call").Middleware<Options, (inputContext: import("better-call").MiddlewareInputContext<Options>) => Promise<R>>;
161
+ <Options extends import("better-call").MiddlewareOptions, R_1>(handler: (ctx: import("better-call").MiddlewareContext<Options, import("@better-auth/core").PluginContext<import("@better-auth/core").BetterAuthOptions> & import("@better-auth/core").InfoContext & {
164
162
  options: import("@better-auth/core").BetterAuthOptions;
165
163
  trustedOrigins: string[];
166
164
  trustedProviders: string[];
@@ -252,6 +250,7 @@ declare const createAuthMiddleware: {
252
250
  updateAge: number;
253
251
  expiresIn: number;
254
252
  freshAge: number;
253
+ cookieCacheSigner?: import("@better-auth/core").CookieCacheSigner | undefined;
255
254
  cookieRefreshCache: false | {
256
255
  enabled: true;
257
256
  updateAge: number;
@@ -285,37 +284,19 @@ declare const createAuthMiddleware: {
285
284
  skipCSRFCheck: boolean;
286
285
  runInBackground: (promise: Promise<unknown>) => void;
287
286
  runInBackgroundOrAwait: (promise: Promise<unknown> | void) => import("@better-auth/core").Awaitable<unknown>;
288
- }>) => Promise<R_1>): (inputContext: import("better-call").MiddlewareInputContext<Options>) => Promise<R_1>;
287
+ } & {
288
+ returned?: unknown | undefined;
289
+ responseHeaders?: Headers | undefined;
290
+ }>) => Promise<R_1>): import("better-call").Middleware<Options, (inputContext: import("better-call").MiddlewareInputContext<Options>) => Promise<R_1>>;
289
291
  };
290
- type EndpointHandler<Path extends string, Options extends EndpointOptions, R> = (context: EndpointContext<Path, Options, AuthContext>) => Promise<R>;
291
- declare function createAuthEndpoint<Path extends string, Options extends EndpointOptions, R>(path: Path, options: Options, handler: EndpointHandler<Path, Options, R>): StrictEndpoint<Path, Options, R>;
292
- declare function createAuthEndpoint<Path extends string, Options extends EndpointOptions, R>(options: Options, handler: EndpointHandler<Path, Options, R>): StrictEndpoint<Path, Options, R>;
292
+ type AuthEndpointHandler<Path extends string, Options extends EndpointOptions, R> = EndpointHandler<Path, Options, R, AuthContext>;
293
+ type PathlessAuthEndpointHandler<Options extends EndpointOptions, R> = AuthEndpointHandler<string, Options, R>;
294
+ declare function createAuthEndpoint<Path extends string, Options extends EndpointOptions, R>(path: Path, options: Options, handler: AuthEndpointHandler<Path, Options, R>): StrictEndpoint<Path, Options, R>;
295
+ declare function createAuthEndpoint<InferredPath extends string, Options extends EndpointOptions, R>(options: Options, handler: PathlessAuthEndpointHandler<Options, R>): StrictEndpoint<InferredPath, Options, R>;
293
296
  declare namespace createAuthEndpoint {
294
- /**
295
- * Declare a **server-only** endpoint.
296
- *
297
- * The endpoint is callable through `auth.api.*` from trusted server code but is
298
- * never registered on the HTTP router and never emitted into the OpenAPI
299
- * schema. It takes no path because it has no URL to be reached at.
300
- *
301
- * Prefer this over the path-less `createAuthEndpoint({ ... }, handler)` form.
302
- * Setting `metadata.SERVER_ONLY` makes the intent explicit at the call site and
303
- * keeps the endpoint off the HTTP surface even if a path is later added by
304
- * mistake: better-call's router skips an endpoint when its path is missing *or*
305
- * when `SERVER_ONLY` is set, so the two together are defense in depth. Relying
306
- * on path omission alone is invisible and one keystroke away from exposure.
307
- *
308
- * @example
309
- * ```ts
310
- * viewBackupCodes: createAuthEndpoint.serverOnly(
311
- * { method: "POST", body: schema },
312
- * async (ctx) => { ... },
313
- * )
314
- * ```
315
- */
316
- function serverOnly<Path extends string, Options extends EndpointOptions, R>(options: Options, handler: EndpointHandler<Path, Options, R>): StrictEndpoint<Path, Options, R>;
297
+ var serverOnly: <InferredPath extends string, Options extends EndpointOptions, R>(options: Options, handler: PathlessAuthEndpointHandler<Options, R>) => StrictEndpoint<InferredPath, Options, R>;
317
298
  }
318
299
  type AuthEndpoint<Path extends string, Opts extends EndpointOptions, R> = ReturnType<typeof createAuthEndpoint<Path, Opts, R>>;
319
- type AuthMiddleware = ReturnType<typeof createAuthMiddleware>;
300
+ type AuthMiddleware = MiddlewareHandler;
320
301
  //#endregion
321
302
  export { AuthEndpoint, AuthMiddleware, NO_STORE_HEADERS, createAuthEndpoint, createAuthMiddleware, optionsMiddleware };
@@ -47,30 +47,26 @@ const optionsMiddleware = createMiddleware(async () => {
47
47
  const createAuthMiddleware = createMiddleware.create({ use: [optionsMiddleware, createMiddleware(async () => {
48
48
  return {};
49
49
  })] });
50
- const use = [optionsMiddleware];
51
- function createAuthEndpoint(pathOrOptions, handlerOrOptions, handlerOrNever) {
52
- const path = typeof pathOrOptions === "string" ? pathOrOptions : void 0;
53
- const options = typeof handlerOrOptions === "object" ? handlerOrOptions : pathOrOptions;
54
- const handler = typeof handlerOrOptions === "function" ? handlerOrOptions : handlerOrNever;
50
+ const createEndpointWithAuthContext = createEndpoint.create({ use: [optionsMiddleware] });
51
+ function wrapEndpointHandler(handler, options) {
55
52
  const noStore = options.metadata?.noStore === true;
56
- const wrapped = async (ctx) => {
57
- if (noStore) for (const [name, value] of Object.entries(NO_STORE_HEADERS)) ctx.setHeader(name, value);
58
- const runtimeCtx = ctx;
53
+ return async (context) => {
54
+ if (noStore) for (const [name, value] of Object.entries(NO_STORE_HEADERS)) context.setHeader(name, value);
59
55
  try {
60
- return await runWithEndpointContext(ctx, () => handler(ctx));
61
- } catch (e) {
62
- attachResponseHeadersToAPIError(runtimeCtx.responseHeaders, e);
63
- throw e;
56
+ return await runWithEndpointContext(context, () => handler(context));
57
+ } catch (error) {
58
+ attachResponseHeadersToAPIError(context.responseHeaders, error);
59
+ throw error;
64
60
  }
65
61
  };
66
- if (path) return createEndpoint(path, {
67
- ...options,
68
- use: [...options?.use || [], ...use]
69
- }, wrapped);
70
- return createEndpoint({
71
- ...options,
72
- use: [...options?.use || [], ...use]
73
- }, wrapped);
62
+ }
63
+ function createAuthEndpoint(...args) {
64
+ if (args.length === 3) {
65
+ const [path, options, handler] = args;
66
+ return createEndpointWithAuthContext(path, options, wrapEndpointHandler(handler, options));
67
+ }
68
+ const [options, handler] = args;
69
+ return createEndpointWithAuthContext(options, wrapEndpointHandler(handler, options));
74
70
  }
75
71
  /**
76
72
  * Set `metadata.SERVER_ONLY` while preserving any existing metadata
@@ -85,11 +81,28 @@ function withServerOnly(options) {
85
81
  }
86
82
  };
87
83
  }
88
- (function(_createAuthEndpoint) {
89
- function serverOnly(options, handler) {
90
- return createAuthEndpoint(withServerOnly(options), handler);
91
- }
92
- _createAuthEndpoint.serverOnly = serverOnly;
93
- })(createAuthEndpoint || (createAuthEndpoint = {}));
84
+ /**
85
+ * Declare a **server-only** endpoint.
86
+ *
87
+ * The endpoint is callable through `auth.api.*` from trusted server code but is
88
+ * never registered on the HTTP router and never emitted into the OpenAPI
89
+ * schema. It takes no path because it has no URL to be reached at.
90
+ *
91
+ * Prefer this over the path-less `createAuthEndpoint({ ... }, handler)` form.
92
+ * Setting `metadata.SERVER_ONLY` makes the intent explicit at the call site and
93
+ * keeps the endpoint off the HTTP surface even if a path is later added by
94
+ * mistake: better-call's router skips an endpoint when its path is missing *or*
95
+ * when `SERVER_ONLY` is set, so the two together are defense in depth. Relying
96
+ * on path omission alone is invisible and one keystroke away from exposure.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * viewBackupCodes: createAuthEndpoint.serverOnly(
101
+ * { method: "POST", body: schema },
102
+ * async (ctx) => { ... },
103
+ * )
104
+ * ```
105
+ */
106
+ createAuthEndpoint.serverOnly = (options, handler) => createAuthEndpoint(withServerOnly(options), handler);
94
107
  //#endregion
95
108
  export { NO_STORE_HEADERS, createAuthEndpoint, createAuthMiddleware, optionsMiddleware };
@@ -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-rc.3";
5
+ const __betterAuthVersion = "1.7.0-rc.5";
6
6
  /**
7
7
  * We store context instance in the globalThis.
8
8
  *
@@ -198,7 +198,7 @@ const buildAuthTables = (options) => {
198
198
  account: {
199
199
  modelName: options.account?.modelName || "account",
200
200
  indexes: mergeTableIndexes([{
201
- fields: ["issuer", "providerAccountId"],
201
+ fields: ["issuer", "accountId"],
202
202
  unique: true
203
203
  }], account?.indexes),
204
204
  fields: {
@@ -207,10 +207,10 @@ const buildAuthTables = (options) => {
207
207
  required: true,
208
208
  fieldName: options.account?.fields?.issuer || "issuer"
209
209
  },
210
- providerAccountId: {
210
+ accountId: {
211
211
  type: "string",
212
212
  required: true,
213
- fieldName: options.account?.fields?.providerAccountId || "providerAccountId"
213
+ fieldName: options.account?.fields?.accountId || "accountId"
214
214
  },
215
215
  providerId: {
216
216
  type: "string",
@@ -10,7 +10,7 @@ declare const accountSchema: z.ZodObject<{
10
10
  updatedAt: z.ZodDefault<z.ZodDate>;
11
11
  providerId: z.ZodString;
12
12
  issuer: z.ZodString;
13
- providerAccountId: z.ZodString;
13
+ accountId: z.ZodString;
14
14
  userId: z.ZodCoercedString<unknown>;
15
15
  accessToken: z.ZodOptional<z.ZodNullable<z.ZodString>>;
16
16
  refreshToken: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -22,7 +22,7 @@ declare const accountSchema: z.ZodObject<{
22
22
  }, z.core.$strip>;
23
23
  type BaseAccount = z.infer<typeof accountSchema>;
24
24
  /** The stable provider-side key used to recognize an account. */
25
- type AccountKey = Readonly<Pick<BaseAccount, "issuer" | "providerAccountId">>;
25
+ type AccountKey = Readonly<Pick<BaseAccount, "issuer" | "accountId">>;
26
26
  /**
27
27
  * Creates the synthetic issuer used by providers without an issuer of their own.
28
28
  */
@@ -4,7 +4,7 @@ import * as z from "zod";
4
4
  const accountSchema = coreSchema.extend({
5
5
  providerId: z.string(),
6
6
  issuer: z.string(),
7
- providerAccountId: z.string(),
7
+ accountId: z.string(),
8
8
  userId: z.coerce.string(),
9
9
  accessToken: z.string().nullish(),
10
10
  refreshToken: z.string().nullish(),
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Awaitable, AwaitableFunction, LiteralString, LiteralUnion, Prettify, Primitive, UnionToIntersection } from "./types/helper.mjs";
2
2
  import { BetterAuthPlugin, BetterAuthPluginErrorCodePart, HookEndpointContext } from "./types/plugin.mjs";
3
3
  import { BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, DynamicBaseURLConfig, GenerateIdFn, StoreIdentifierOption, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource } from "./types/init-options.mjs";
4
- import { BetterAuthCookie, BetterAuthCookies } from "./types/cookie.mjs";
4
+ import { BetterAuthCookie, BetterAuthCookies, CookieCachePayload } from "./types/cookie.mjs";
5
5
  import { SecretConfig } from "./types/secret.mjs";
6
- import { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext } from "./types/context.mjs";
6
+ import { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, CookieCacheSigner, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext } from "./types/context.mjs";
7
7
  import { BetterAuthClientOptions, BetterAuthClientPlugin, ClientAtomListener, ClientFetchOption, ClientStore } from "./types/plugin-client.mjs";
8
8
  import { StandardSchemaV1 } from "./types/index.mjs";
9
- export type { AuthContext, Awaitable, AwaitableFunction, BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookie, BetterAuthCookies, BetterAuthDBOptions, BetterAuthOptions, BetterAuthPlugin, BetterAuthPluginErrorCodePart, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, ClientAtomListener, ClientFetchOption, ClientStore, DynamicBaseURLConfig, GenerateIdFn, GenericEndpointContext, HookEndpointContext, InfoContext, InternalAdapter, LiteralString, LiteralUnion, PluginContext, Prettify, Primitive, SecretConfig, StandardSchemaV1, StoreIdentifierOption, UnionToIntersection, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource };
9
+ export type { AuthContext, Awaitable, AwaitableFunction, BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookie, BetterAuthCookies, BetterAuthDBOptions, BetterAuthOptions, BetterAuthPlugin, BetterAuthPluginErrorCodePart, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, ClientAtomListener, ClientFetchOption, ClientStore, CookieCachePayload, CookieCacheSigner, DynamicBaseURLConfig, GenerateIdFn, GenericEndpointContext, HookEndpointContext, InfoContext, InternalAdapter, LiteralString, LiteralUnion, PluginContext, Prettify, Primitive, SecretConfig, StandardSchemaV1, StoreIdentifierOption, UnionToIntersection, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource };
@@ -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-rc.3";
5
+ const INSTRUMENTATION_VERSION = "1.7.0-rc.5";
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
@@ -70,7 +70,7 @@ declare const apple: (options: AppleOptions) => {
70
70
  name: string;
71
71
  accountSubject: ({ profile }: OAuthAccountKeyContext<AppleProfile>) => string;
72
72
  accountIssuer: string;
73
- createAuthorizationURL({ state, scopes, redirectURI, additionalParams }: {
73
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams, codeVerifier }: {
74
74
  state: string;
75
75
  codeVerifier: string;
76
76
  scopes?: string[] | undefined;
@@ -14,7 +14,7 @@ const apple = (options) => {
14
14
  name: "Apple",
15
15
  accountSubject: ({ profile }) => profile.sub,
16
16
  accountIssuer: "https://appleid.apple.com",
17
- async createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
17
+ async createAuthorizationURL({ state, scopes, redirectURI, additionalParams, codeVerifier }) {
18
18
  if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
19
19
  logger.error("Client ID and client secret are required for Apple. Make sure to provide them in the options.");
20
20
  throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
@@ -29,6 +29,7 @@ const apple = (options) => {
29
29
  scopes: _scope,
30
30
  state,
31
31
  redirectURI,
32
+ codeVerifier,
32
33
  responseMode: "form_post",
33
34
  responseType: "code id_token",
34
35
  additionalParams
@@ -45,7 +45,7 @@ declare const socialProviders: {
45
45
  name: string;
46
46
  accountSubject: ({ profile }: OAuthAccountKeyContext<AppleProfile>) => string;
47
47
  accountIssuer: string;
48
- createAuthorizationURL({ state, scopes, redirectURI, additionalParams }: {
48
+ createAuthorizationURL({ state, scopes, redirectURI, additionalParams, codeVerifier }: {
49
49
  state: string;
50
50
  codeVerifier: string;
51
51
  scopes?: string[] | undefined;
@@ -9,7 +9,7 @@ import { BetterAuthPlugin } from "./plugin.mjs";
9
9
  import { BetterAuthOptions, BetterAuthRateLimitOptions, UserProvisioningSource } from "./init-options.mjs";
10
10
  import { Account, AccountKey } from "../db/schema/account.mjs";
11
11
  import "../db/index.mjs";
12
- import { BetterAuthCookie, BetterAuthCookies } from "./cookie.mjs";
12
+ import { BetterAuthCookie, BetterAuthCookies, CookieCachePayload } from "./cookie.mjs";
13
13
  import { SecretConfig } from "./secret.mjs";
14
14
  import { OAuthProvider } from "../oauth2/oauth-provider.mjs";
15
15
  import "../oauth2/index.mjs";
@@ -54,6 +54,16 @@ type BetterAuthPluginRegistryIdentifier = keyof BetterAuthPluginRegistry<unknown
54
54
  type GenericEndpointContext<Options extends BetterAuthOptions = BetterAuthOptions> = EndpointContext<string, any> & {
55
55
  context: AuthContext<Options>;
56
56
  };
57
+ /**
58
+ * Signs and verifies session cookie-cache values.
59
+ */
60
+ type CookieCacheSigner = {
61
+ sign: (ctx: GenericEndpointContext, payload: CookieCachePayload, expiresIn: number) => Promise<string>;
62
+ verify: (ctx: GenericEndpointContext, token: string) => Promise<{
63
+ payload: CookieCachePayload;
64
+ expiresAt: number;
65
+ } | null>;
66
+ };
57
67
  interface InternalAdapter<_Options extends BetterAuthOptions = BetterAuthOptions> {
58
68
  createOAuthUser(user: Omit<User, "id" | "createdAt" | "updatedAt">, account: Omit<Account, "userId" | "id" | "createdAt" | "updatedAt"> & Partial<Account>): Promise<{
59
69
  user: User;
@@ -261,6 +271,7 @@ type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> = Plugin
261
271
  updateAge: number;
262
272
  expiresIn: number;
263
273
  freshAge: number;
274
+ cookieCacheSigner?: CookieCacheSigner | undefined;
264
275
  cookieRefreshCache: false | {
265
276
  enabled: true;
266
277
  updateAge: number;
@@ -330,4 +341,4 @@ type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> = Plugin
330
341
  runInBackgroundOrAwait: (promise: Promise<unknown> | void) => Awaitable<unknown>;
331
342
  };
332
343
  //#endregion
333
- export { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext };
344
+ export { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, CookieCacheSigner, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext };
@@ -1,3 +1,6 @@
1
+ import { Session } from "../db/schema/session.mjs";
2
+ import { User } from "../db/schema/user.mjs";
3
+ import "../db/index.mjs";
1
4
  import { CookieOptions } from "better-call";
2
5
  //#region src/types/cookie.d.ts
3
6
  type BetterAuthCookie = {
@@ -10,5 +13,14 @@ type BetterAuthCookies = {
10
13
  accountData: BetterAuthCookie;
11
14
  dontRememberToken: BetterAuthCookie;
12
15
  };
16
+ /**
17
+ * A validated cookie-cache payload, including legacy payloads without a version.
18
+ */
19
+ type CookieCachePayload = {
20
+ session: Session & Record<string, unknown>;
21
+ user: User & Record<string, unknown>;
22
+ updatedAt: number;
23
+ version?: string | undefined;
24
+ };
13
25
  //#endregion
14
- export { BetterAuthCookie, BetterAuthCookies };
26
+ export { BetterAuthCookie, BetterAuthCookies, CookieCachePayload };
@@ -1,9 +1,9 @@
1
1
  import { Awaitable, AwaitableFunction, LiteralString, LiteralUnion, Prettify, Primitive, UnionToIntersection } from "./helper.mjs";
2
2
  import { BetterAuthPlugin, BetterAuthPluginErrorCodePart, HookEndpointContext } from "./plugin.mjs";
3
3
  import { BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, DynamicBaseURLConfig, GenerateIdFn, StoreIdentifierOption, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource } from "./init-options.mjs";
4
- import { BetterAuthCookie, BetterAuthCookies } from "./cookie.mjs";
4
+ import { BetterAuthCookie, BetterAuthCookies, CookieCachePayload } from "./cookie.mjs";
5
5
  import { SecretConfig } from "./secret.mjs";
6
- import { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext } from "./context.mjs";
6
+ import { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, CookieCacheSigner, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext } from "./context.mjs";
7
7
  import { BetterAuthClientOptions, BetterAuthClientPlugin, ClientAtomListener, ClientFetchOption, ClientStore } from "./plugin-client.mjs";
8
8
  import { StandardSchemaV1 as StandardSchemaV1$1 } from "@standard-schema/spec";
9
- export type { AuthContext, Awaitable, AwaitableFunction, BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookie, BetterAuthCookies, BetterAuthDBOptions, BetterAuthOptions, BetterAuthPlugin, BetterAuthPluginErrorCodePart, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, ClientAtomListener, ClientFetchOption, ClientStore, DynamicBaseURLConfig, GenerateIdFn, GenericEndpointContext, HookEndpointContext, InfoContext, InternalAdapter, LiteralString, LiteralUnion, PluginContext, Prettify, Primitive, SecretConfig, StandardSchemaV1$1 as StandardSchemaV1, StoreIdentifierOption, UnionToIntersection, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource };
9
+ export type { AuthContext, Awaitable, AwaitableFunction, BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookie, BetterAuthCookies, BetterAuthDBOptions, BetterAuthOptions, BetterAuthPlugin, BetterAuthPluginErrorCodePart, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, ClientAtomListener, ClientFetchOption, ClientStore, CookieCachePayload, CookieCacheSigner, DynamicBaseURLConfig, GenerateIdFn, GenericEndpointContext, HookEndpointContext, InfoContext, InternalAdapter, LiteralString, LiteralUnion, PluginContext, Prettify, Primitive, SecretConfig, StandardSchemaV1$1 as StandardSchemaV1, StoreIdentifierOption, UnionToIntersection, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource };
@@ -978,20 +978,6 @@ type BetterAuthOptions = {
978
978
  * @default "compact"
979
979
  */
980
980
  strategy?: "compact" | "jwt" | "jwe";
981
- /**
982
- * JWT-specific configuration for `strategy: "jwt"`.
983
- */
984
- jwt?: {
985
- /**
986
- * Which signing key is used for cookie-cache JWTs.
987
- *
988
- * - `"secret"`: uses the Better Auth secret with HS256.
989
- * - `"jwt-plugin"`: uses the installed `jwt()` plugin's asymmetric signing keys.
990
- *
991
- * @default "secret"
992
- */
993
- signingKey?: "secret" | "jwt-plugin";
994
- };
995
981
  /**
996
982
  * Controls stateless cookie cache refresh behavior.
997
983
  *
@@ -0,0 +1,19 @@
1
+ //#region src/utils/email.d.ts
2
+ interface PlaceholderEmailOptions {
3
+ /**
4
+ * A stable identifier that distinguishes the account within the namespace.
5
+ */
6
+ identifier: string;
7
+ /**
8
+ * A namespace that distinguishes identical identifiers from different sources.
9
+ */
10
+ namespace: string;
11
+ }
12
+ /**
13
+ * Creates a stable, non-routable email address for an account without an email.
14
+ *
15
+ * @throws TypeError if the generated email is invalid.
16
+ */
17
+ declare function createPlaceholderEmail({ identifier, namespace }: PlaceholderEmailOptions): string;
18
+ //#endregion
19
+ export { PlaceholderEmailOptions, createPlaceholderEmail };
@@ -0,0 +1,18 @@
1
+ import * as z from "zod";
2
+ //#region src/utils/email.ts
3
+ /**
4
+ * @see https://www.rfc-editor.org/rfc/rfc6761.html#section-6.4
5
+ */
6
+ const PLACEHOLDER_EMAIL_DOMAIN = "placeholder.invalid";
7
+ /**
8
+ * Creates a stable, non-routable email address for an account without an email.
9
+ *
10
+ * @throws TypeError if the generated email is invalid.
11
+ */
12
+ function createPlaceholderEmail({ identifier, namespace }) {
13
+ const result = z.email().safeParse(`${identifier}@${namespace}.${PLACEHOLDER_EMAIL_DOMAIN}`);
14
+ if (!result.success) throw new TypeError("Invalid placeholder email");
15
+ return result.data;
16
+ }
17
+ //#endregion
18
+ export { createPlaceholderEmail };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.7.0-rc.3",
3
+ "version": "1.7.0-rc.5",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -164,9 +164,9 @@
164
164
  "@better-fetch/fetch": "1.3.1",
165
165
  "@cloudflare/workers-types": "^4.20250121.0",
166
166
  "@opentelemetry/api": "^1.9.1",
167
- "@opentelemetry/sdk-trace-base": "^2.8.0",
168
- "@opentelemetry/sdk-trace-node": "^2.8.0",
169
- "better-call": "1.3.7",
167
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
168
+ "@opentelemetry/sdk-trace-node": "^2.10.0",
169
+ "better-call": "1.4.0",
170
170
  "jose": "^6.2.3",
171
171
  "kysely": "^0.28.17 || ^0.29.0",
172
172
  "nanostores": "^1.3.0",
@@ -177,7 +177,7 @@
177
177
  "@better-fetch/fetch": "1.3.1",
178
178
  "@cloudflare/workers-types": ">=4",
179
179
  "@opentelemetry/api": "^1.9.0",
180
- "better-call": "1.3.7",
180
+ "better-call": "1.4.0",
181
181
  "jose": "^6.1.0",
182
182
  "kysely": "^0.28.5 || ^0.29.0",
183
183
  "nanostores": "^1.0.1"
package/src/api/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
- EndpointContext,
2
+ EndpointHandler,
3
3
  EndpointOptions,
4
+ MiddlewareHandler,
4
5
  StrictEndpoint,
5
6
  } from "better-call";
6
7
  import {
@@ -75,94 +76,93 @@ export const createAuthMiddleware = createMiddleware.create({
75
76
  ],
76
77
  });
77
78
 
78
- const use = [optionsMiddleware];
79
+ const createEndpointWithAuthContext = createEndpoint.create({
80
+ use: [optionsMiddleware],
81
+ });
79
82
 
80
- type EndpointHandler<
83
+ type AuthEndpointHandler<
81
84
  Path extends string,
82
85
  Options extends EndpointOptions,
83
86
  R,
84
- > = (context: EndpointContext<Path, Options, AuthContext>) => Promise<R>;
87
+ > = EndpointHandler<Path, Options, R, AuthContext>;
85
88
 
86
- export function createAuthEndpoint<
87
- Path extends string,
89
+ type PathlessAuthEndpointHandler<
88
90
  Options extends EndpointOptions,
89
91
  R,
90
- >(
91
- path: Path,
92
- options: Options,
93
- handler: EndpointHandler<Path, Options, R>,
94
- ): StrictEndpoint<Path, Options, R>;
92
+ > = AuthEndpointHandler<string, Options, R>;
95
93
 
96
- export function createAuthEndpoint<
94
+ function wrapEndpointHandler<
97
95
  Path extends string,
98
96
  Options extends EndpointOptions,
99
97
  R,
100
98
  >(
99
+ handler: AuthEndpointHandler<Path, Options, R>,
101
100
  options: Options,
102
- handler: EndpointHandler<Path, Options, R>,
103
- ): StrictEndpoint<Path, Options, R>;
104
-
105
- export function createAuthEndpoint<
106
- Path extends string,
107
- Opts extends EndpointOptions,
108
- R,
109
- >(
110
- pathOrOptions: Path | Opts,
111
- handlerOrOptions: EndpointHandler<Path, Opts, R> | Opts,
112
- handlerOrNever?: any,
113
- ) {
114
- const path: Path | undefined =
115
- typeof pathOrOptions === "string" ? pathOrOptions : undefined;
116
- const options: Opts =
117
- typeof handlerOrOptions === "object"
118
- ? handlerOrOptions
119
- : (pathOrOptions as Opts);
120
- const handler: EndpointHandler<Path, Opts, R> =
121
- typeof handlerOrOptions === "function" ? handlerOrOptions : handlerOrNever;
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).
101
+ ): AuthEndpointHandler<Path, Options, R> {
129
102
  const noStore =
130
103
  (options as { metadata?: { noStore?: boolean } }).metadata?.noStore ===
131
104
  true;
132
105
 
133
- // todo: prettify the code, we want to call `runWithEndpointContext` to top level
134
- const wrapped: EndpointHandler<Path, Opts, R> = async (ctx) => {
106
+ return async (context) => {
135
107
  if (noStore) {
136
108
  for (const [name, value] of Object.entries(NO_STORE_HEADERS)) {
137
- ctx.setHeader(name, value);
109
+ context.setHeader(name, value);
138
110
  }
139
111
  }
140
- const runtimeCtx = ctx as unknown as { responseHeaders?: Headers };
141
112
  try {
142
- return await runWithEndpointContext(ctx as any, () => handler(ctx));
143
- } catch (e) {
144
- attachResponseHeadersToAPIError(runtimeCtx.responseHeaders, e);
145
- throw e;
113
+ return await runWithEndpointContext(context, () => handler(context));
114
+ } catch (error) {
115
+ attachResponseHeadersToAPIError(context.responseHeaders, error);
116
+ throw error;
146
117
  }
147
118
  };
119
+ }
148
120
 
149
- if (path) {
150
- return createEndpoint(
121
+ export function createAuthEndpoint<
122
+ Path extends string,
123
+ Options extends EndpointOptions,
124
+ R,
125
+ >(
126
+ path: Path,
127
+ options: Options,
128
+ handler: AuthEndpointHandler<Path, Options, R>,
129
+ ): StrictEndpoint<Path, Options, R>;
130
+
131
+ export function createAuthEndpoint<
132
+ InferredPath extends string,
133
+ Options extends EndpointOptions,
134
+ R,
135
+ >(
136
+ options: Options,
137
+ handler: PathlessAuthEndpointHandler<Options, R>,
138
+ ): StrictEndpoint<InferredPath, Options, R>;
139
+
140
+ export function createAuthEndpoint<
141
+ Path extends string,
142
+ Options extends EndpointOptions,
143
+ R,
144
+ >(
145
+ ...args:
146
+ | [
147
+ path: Path,
148
+ options: Options,
149
+ handler: AuthEndpointHandler<Path, Options, R>,
150
+ ]
151
+ | [options: Options, handler: PathlessAuthEndpointHandler<Options, R>]
152
+ ) {
153
+ if (args.length === 3) {
154
+ const [path, options, handler] = args;
155
+ return createEndpointWithAuthContext(
151
156
  path,
152
- {
153
- ...options,
154
- use: [...(options?.use || []), ...use],
155
- },
156
- wrapped,
157
+ options,
158
+ wrapEndpointHandler(handler, options),
157
159
  );
158
160
  }
159
161
 
160
- return createEndpoint(
161
- {
162
- ...options,
163
- use: [...(options?.use || []), ...use],
164
- },
165
- wrapped,
162
+ const [options, handler] = args;
163
+ return createEndpointWithAuthContext(
164
+ options,
165
+ wrapEndpointHandler(handler, options),
166
166
  );
167
167
  }
168
168
 
@@ -179,44 +179,45 @@ function withServerOnly<Options extends EndpointOptions>(
179
179
  } as Options;
180
180
  }
181
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
- }
182
+ /**
183
+ * Declare a **server-only** endpoint.
184
+ *
185
+ * The endpoint is callable through `auth.api.*` from trusted server code but is
186
+ * never registered on the HTTP router and never emitted into the OpenAPI
187
+ * schema. It takes no path because it has no URL to be reached at.
188
+ *
189
+ * Prefer this over the path-less `createAuthEndpoint({ ... }, handler)` form.
190
+ * Setting `metadata.SERVER_ONLY` makes the intent explicit at the call site and
191
+ * keeps the endpoint off the HTTP surface even if a path is later added by
192
+ * mistake: better-call's router skips an endpoint when its path is missing *or*
193
+ * when `SERVER_ONLY` is set, so the two together are defense in depth. Relying
194
+ * on path omission alone is invisible and one keystroke away from exposure.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * viewBackupCodes: createAuthEndpoint.serverOnly(
199
+ * { method: "POST", body: schema },
200
+ * async (ctx) => { ... },
201
+ * )
202
+ * ```
203
+ */
204
+ createAuthEndpoint.serverOnly = <
205
+ InferredPath extends string,
206
+ Options extends EndpointOptions,
207
+ R,
208
+ >(
209
+ options: Options,
210
+ handler: PathlessAuthEndpointHandler<Options, R>,
211
+ ): StrictEndpoint<InferredPath, Options, R> =>
212
+ createAuthEndpoint<InferredPath, Options, R>(
213
+ withServerOnly(options),
214
+ handler,
215
+ );
216
216
 
217
217
  export type AuthEndpoint<
218
218
  Path extends string,
219
219
  Opts extends EndpointOptions,
220
220
  R,
221
221
  > = ReturnType<typeof createAuthEndpoint<Path, Opts, R>>;
222
- export type AuthMiddleware = ReturnType<typeof createAuthMiddleware>;
222
+
223
+ export type AuthMiddleware = MiddlewareHandler;
@@ -253,7 +253,7 @@ const buildAuthTables = (options: BetterAuthOptions): BetterAuthDBSchema => {
253
253
  indexes: mergeTableIndexes(
254
254
  [
255
255
  {
256
- fields: ["issuer", "providerAccountId"],
256
+ fields: ["issuer", "accountId"],
257
257
  unique: true,
258
258
  },
259
259
  ],
@@ -265,11 +265,10 @@ const buildAuthTables = (options: BetterAuthOptions): BetterAuthDBSchema => {
265
265
  required: true,
266
266
  fieldName: options.account?.fields?.issuer || "issuer",
267
267
  },
268
- providerAccountId: {
268
+ accountId: {
269
269
  type: "string",
270
270
  required: true,
271
- fieldName:
272
- options.account?.fields?.providerAccountId || "providerAccountId",
271
+ fieldName: options.account?.fields?.accountId || "accountId",
273
272
  },
274
273
  providerId: {
275
274
  type: "string",
@@ -10,7 +10,7 @@ import { coreSchema } from "./shared";
10
10
  export const accountSchema = coreSchema.extend({
11
11
  providerId: z.string(),
12
12
  issuer: z.string(),
13
- providerAccountId: z.string(),
13
+ accountId: z.string(),
14
14
  userId: z.coerce.string(),
15
15
  accessToken: z.string().nullish(),
16
16
  refreshToken: z.string().nullish(),
@@ -39,9 +39,7 @@ export const accountSchema = coreSchema.extend({
39
39
  export type BaseAccount = z.infer<typeof accountSchema>;
40
40
 
41
41
  /** The stable provider-side key used to recognize an account. */
42
- export type AccountKey = Readonly<
43
- Pick<BaseAccount, "issuer" | "providerAccountId">
44
- >;
42
+ export type AccountKey = Readonly<Pick<BaseAccount, "issuer" | "accountId">>;
45
43
 
46
44
  function encodeAccountIssuerProviderId(providerId: string): string {
47
45
  return encodeURIComponent(providerId);
@@ -9,7 +9,7 @@ import { coreSchema } from "./shared";
9
9
  export const userSchema = coreSchema.extend({
10
10
  // TODO(#9124): widen to nullish in v2. OAuth providers (Discord phone-only,
11
11
  // Apple subsequent sign-ins, etc.) can legitimately omit email; identity
12
- // must key on (issuer, providerAccountId) per OpenID Connect Core §5.7.
12
+ // must key on (issuer, accountId) per OpenID Connect Core §5.7.
13
13
  email: z.string().transform((val) => val.toLowerCase()),
14
14
  emailVerified: z.boolean().default(false),
15
15
  name: z.string(),
@@ -89,6 +89,7 @@ export const apple = (options: AppleOptions) => {
89
89
  scopes,
90
90
  redirectURI,
91
91
  additionalParams,
92
+ codeVerifier,
92
93
  }) {
93
94
  if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
94
95
  logger.error(
@@ -106,6 +107,7 @@ export const apple = (options: AppleOptions) => {
106
107
  scopes: _scope,
107
108
  state,
108
109
  redirectURI,
110
+ codeVerifier,
109
111
  responseMode: "form_post",
110
112
  responseType: "code id_token",
111
113
  additionalParams,
@@ -12,7 +12,11 @@ import type { DBAdapter, Where } from "../db/adapter";
12
12
  import type { AccountKey } from "../db/schema/account";
13
13
  import type { createLogger } from "../env";
14
14
  import type { OAuthProvider } from "../oauth2";
15
- import type { BetterAuthCookie, BetterAuthCookies } from "./cookie";
15
+ import type {
16
+ BetterAuthCookie,
17
+ BetterAuthCookies,
18
+ CookieCachePayload,
19
+ } from "./cookie";
16
20
  import type { Awaitable, LiteralString } from "./helper";
17
21
  import type {
18
22
  BetterAuthOptions,
@@ -86,6 +90,24 @@ export type GenericEndpointContext<
86
90
  context: AuthContext<Options>;
87
91
  };
88
92
 
93
+ /**
94
+ * Signs and verifies session cookie-cache values.
95
+ */
96
+ export type CookieCacheSigner = {
97
+ sign: (
98
+ ctx: GenericEndpointContext,
99
+ payload: CookieCachePayload,
100
+ expiresIn: number,
101
+ ) => Promise<string>;
102
+ verify: (
103
+ ctx: GenericEndpointContext,
104
+ token: string,
105
+ ) => Promise<{
106
+ payload: CookieCachePayload;
107
+ expiresAt: number;
108
+ } | null>;
109
+ };
110
+
89
111
  export interface InternalAdapter<
90
112
  _Options extends BetterAuthOptions = BetterAuthOptions,
91
113
  > {
@@ -403,6 +425,7 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
403
425
  updateAge: number;
404
426
  expiresIn: number;
405
427
  freshAge: number;
428
+ cookieCacheSigner?: CookieCacheSigner | undefined;
406
429
  cookieRefreshCache:
407
430
  | false
408
431
  | {
@@ -1,4 +1,5 @@
1
1
  import type { CookieOptions } from "better-call";
2
+ import type { Session, User } from "../db";
2
3
 
3
4
  export type BetterAuthCookie = { name: string; attributes: CookieOptions };
4
5
 
@@ -8,3 +9,13 @@ export type BetterAuthCookies = {
8
9
  accountData: BetterAuthCookie;
9
10
  dontRememberToken: BetterAuthCookie;
10
11
  };
12
+
13
+ /**
14
+ * A validated cookie-cache payload, including legacy payloads without a version.
15
+ */
16
+ export type CookieCachePayload = {
17
+ session: Session & Record<string, unknown>;
18
+ user: User & Record<string, unknown>;
19
+ updatedAt: number;
20
+ version?: string | undefined;
21
+ };
@@ -3,6 +3,7 @@ export type {
3
3
  AuthContext,
4
4
  BetterAuthPluginRegistry,
5
5
  BetterAuthPluginRegistryIdentifier,
6
+ CookieCacheSigner,
6
7
  GenericEndpointContext,
7
8
  InfoContext,
8
9
  InternalAdapter,
@@ -11,6 +12,7 @@ export type {
11
12
  export type {
12
13
  BetterAuthCookie,
13
14
  BetterAuthCookies,
15
+ CookieCachePayload,
14
16
  } from "./cookie";
15
17
  export type * from "./helper";
16
18
  export type {
@@ -1089,20 +1089,6 @@ export type BetterAuthOptions = {
1089
1089
  * @default "compact"
1090
1090
  */
1091
1091
  strategy?: "compact" | "jwt" | "jwe";
1092
- /**
1093
- * JWT-specific configuration for `strategy: "jwt"`.
1094
- */
1095
- jwt?: {
1096
- /**
1097
- * Which signing key is used for cookie-cache JWTs.
1098
- *
1099
- * - `"secret"`: uses the Better Auth secret with HS256.
1100
- * - `"jwt-plugin"`: uses the installed `jwt()` plugin's asymmetric signing keys.
1101
- *
1102
- * @default "secret"
1103
- */
1104
- signingKey?: "secret" | "jwt-plugin";
1105
- };
1106
1092
  /**
1107
1093
  * Controls stateless cookie cache refresh behavior.
1108
1094
  *
@@ -0,0 +1,36 @@
1
+ import * as z from "zod";
2
+
3
+ /**
4
+ * @see https://www.rfc-editor.org/rfc/rfc6761.html#section-6.4
5
+ */
6
+ const PLACEHOLDER_EMAIL_DOMAIN = "placeholder.invalid";
7
+
8
+ export interface PlaceholderEmailOptions {
9
+ /**
10
+ * A stable identifier that distinguishes the account within the namespace.
11
+ */
12
+ identifier: string;
13
+ /**
14
+ * A namespace that distinguishes identical identifiers from different sources.
15
+ */
16
+ namespace: string;
17
+ }
18
+
19
+ /**
20
+ * Creates a stable, non-routable email address for an account without an email.
21
+ *
22
+ * @throws TypeError if the generated email is invalid.
23
+ */
24
+ export function createPlaceholderEmail({
25
+ identifier,
26
+ namespace,
27
+ }: PlaceholderEmailOptions): string {
28
+ const result = z
29
+ .email()
30
+ .safeParse(`${identifier}@${namespace}.${PLACEHOLDER_EMAIL_DOMAIN}`);
31
+ if (!result.success) {
32
+ throw new TypeError("Invalid placeholder email");
33
+ }
34
+
35
+ return result.data;
36
+ }