@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.
- package/dist/api/index.d.mts +18 -37
- package/dist/api/index.mjs +39 -26
- package/dist/context/global.mjs +1 -1
- package/dist/db/get-tables.mjs +3 -3
- package/dist/db/schema/account.d.mts +2 -2
- package/dist/db/schema/account.mjs +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/social-providers/apple.d.mts +1 -1
- package/dist/social-providers/apple.mjs +2 -1
- package/dist/social-providers/index.d.mts +1 -1
- package/dist/types/context.d.mts +13 -2
- package/dist/types/cookie.d.mts +13 -1
- package/dist/types/index.d.mts +3 -3
- package/dist/types/init-options.d.mts +0 -14
- package/dist/utils/email.d.mts +19 -0
- package/dist/utils/email.mjs +18 -0
- package/package.json +5 -5
- package/src/api/index.ts +96 -95
- package/src/db/get-tables.ts +3 -4
- package/src/db/schema/account.ts +2 -4
- package/src/db/schema/user.ts +1 -1
- package/src/social-providers/apple.ts +2 -0
- package/src/types/context.ts +24 -1
- package/src/types/cookie.ts +11 -0
- package/src/types/index.ts +2 -0
- package/src/types/init-options.ts +0 -14
- package/src/utils/email.ts +36 -0
package/dist/api/index.d.mts
CHANGED
|
@@ -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 {
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
|
291
|
-
|
|
292
|
-
declare function createAuthEndpoint<Path extends string, Options extends EndpointOptions, R>(options: Options, handler:
|
|
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 =
|
|
300
|
+
type AuthMiddleware = MiddlewareHandler;
|
|
320
301
|
//#endregion
|
|
321
302
|
export { AuthEndpoint, AuthMiddleware, NO_STORE_HEADERS, createAuthEndpoint, createAuthMiddleware, optionsMiddleware };
|
package/dist/api/index.mjs
CHANGED
|
@@ -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
|
|
51
|
-
function
|
|
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
|
-
|
|
57
|
-
if (noStore) for (const [name, value] of Object.entries(NO_STORE_HEADERS))
|
|
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(
|
|
61
|
-
} catch (
|
|
62
|
-
attachResponseHeadersToAPIError(
|
|
63
|
-
throw
|
|
56
|
+
return await runWithEndpointContext(context, () => handler(context));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
attachResponseHeadersToAPIError(context.responseHeaders, error);
|
|
59
|
+
throw error;
|
|
64
60
|
}
|
|
65
61
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 };
|
package/dist/context/global.mjs
CHANGED
package/dist/db/get-tables.mjs
CHANGED
|
@@ -198,7 +198,7 @@ const buildAuthTables = (options) => {
|
|
|
198
198
|
account: {
|
|
199
199
|
modelName: options.account?.modelName || "account",
|
|
200
200
|
indexes: mergeTableIndexes([{
|
|
201
|
-
fields: ["issuer", "
|
|
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
|
-
|
|
210
|
+
accountId: {
|
|
211
211
|
type: "string",
|
|
212
212
|
required: true,
|
|
213
|
-
fieldName: options.account?.fields?.
|
|
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
|
-
|
|
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" | "
|
|
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
|
-
|
|
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.
|
|
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;
|
package/dist/types/context.d.mts
CHANGED
|
@@ -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 };
|
package/dist/types/cookie.d.mts
CHANGED
|
@@ -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 };
|
package/dist/types/index.d.mts
CHANGED
|
@@ -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
|
+
"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.
|
|
168
|
-
"@opentelemetry/sdk-trace-node": "^2.
|
|
169
|
-
"better-call": "1.
|
|
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.
|
|
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
|
-
|
|
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
|
|
79
|
+
const createEndpointWithAuthContext = createEndpoint.create({
|
|
80
|
+
use: [optionsMiddleware],
|
|
81
|
+
});
|
|
79
82
|
|
|
80
|
-
type
|
|
83
|
+
type AuthEndpointHandler<
|
|
81
84
|
Path extends string,
|
|
82
85
|
Options extends EndpointOptions,
|
|
83
86
|
R,
|
|
84
|
-
> =
|
|
87
|
+
> = EndpointHandler<Path, Options, R, AuthContext>;
|
|
85
88
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
143
|
-
} catch (
|
|
144
|
-
attachResponseHeadersToAPIError(
|
|
145
|
-
throw
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
use: [...(options?.use || []), ...use],
|
|
155
|
-
},
|
|
156
|
-
wrapped,
|
|
157
|
+
options,
|
|
158
|
+
wrapEndpointHandler(handler, options),
|
|
157
159
|
);
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
222
|
+
|
|
223
|
+
export type AuthMiddleware = MiddlewareHandler;
|
package/src/db/get-tables.ts
CHANGED
|
@@ -253,7 +253,7 @@ const buildAuthTables = (options: BetterAuthOptions): BetterAuthDBSchema => {
|
|
|
253
253
|
indexes: mergeTableIndexes(
|
|
254
254
|
[
|
|
255
255
|
{
|
|
256
|
-
fields: ["issuer", "
|
|
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
|
-
|
|
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",
|
package/src/db/schema/account.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/src/db/schema/user.ts
CHANGED
|
@@ -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,
|
|
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,
|
package/src/types/context.ts
CHANGED
|
@@ -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 {
|
|
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
|
| {
|
package/src/types/cookie.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
}
|