better-auth 1.7.1 → 1.7.3
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 +6 -22
- package/dist/api/index.mjs +2 -0
- package/dist/api/middlewares/origin-check.mjs +9 -5
- package/dist/api/routes/account.d.mts +1 -9
- package/dist/api/routes/account.mjs +0 -6
- package/dist/api/routes/callback.mjs +7 -7
- package/dist/api/routes/email-verification.mjs +4 -2
- package/dist/api/routes/error.mjs +12 -8
- package/dist/api/routes/password.mjs +0 -2
- package/dist/api/routes/session.mjs +12 -5
- package/dist/api/routes/sign-in.d.mts +2 -2
- package/dist/api/routes/sign-in.mjs +1 -4
- package/dist/api/routes/sign-up.mjs +0 -2
- package/dist/api/routes/update-user.mjs +0 -2
- package/dist/api/to-auth-endpoints.mjs +2 -0
- package/dist/auth/base.mjs +13 -1
- package/dist/auth/trusted-origins.mjs +24 -5
- package/dist/client/config.d.mts +2 -0
- package/dist/client/config.mjs +1 -0
- package/dist/client/lynx/index.d.mts +1 -1
- package/dist/client/react/index.d.mts +1 -1
- package/dist/client/solid/index.d.mts +1 -1
- package/dist/client/svelte/index.d.mts +1 -1
- package/dist/client/vanilla.d.mts +1 -1
- package/dist/client/vue/index.d.mts +32 -21
- package/dist/client/vue/index.mjs +23 -5
- package/dist/context/create-context.mjs +2 -0
- package/dist/cookies/cache.mjs +10 -2
- package/dist/cookies/session-store.d.mts +0 -1
- package/dist/cookies/session-store.mjs +16 -5
- package/dist/db/adapter-kysely.mjs +1 -2
- package/dist/db/get-migration.d.mts +1 -0
- package/dist/db/get-migration.mjs +49 -56
- package/dist/db/internal-adapter.mjs +17 -20
- package/dist/db/schema.d.mts +0 -1
- package/dist/db/with-hooks.mjs +7 -7
- package/dist/oauth2/account-key.mjs +1 -5
- package/dist/oauth2/errors.mjs +4 -3
- package/dist/oauth2/link-account.d.mts +0 -1
- package/dist/oauth2/link-account.mjs +5 -11
- package/dist/oauth2/state.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +1 -4
- package/dist/plugins/anonymous/index.mjs +5 -1
- package/dist/plugins/anonymous/types.d.mts +2 -3
- package/dist/plugins/device-authorization/index.d.mts +11 -11
- package/dist/plugins/email-otp/routes.mjs +0 -2
- package/dist/plugins/generic-oauth/index.mjs +14 -10
- package/dist/plugins/generic-oauth/providers/auth0.mjs +2 -4
- package/dist/plugins/generic-oauth/providers/keycloak.mjs +6 -9
- package/dist/plugins/generic-oauth/providers/line.mjs +0 -1
- package/dist/plugins/generic-oauth/providers/microsoft-entra-id.mjs +15 -5
- package/dist/plugins/generic-oauth/providers/okta.mjs +6 -9
- package/dist/plugins/generic-oauth/providers/slack.mjs +0 -1
- package/dist/plugins/generic-oauth/types.d.mts +0 -9
- package/dist/plugins/haveibeenpwned/index.d.mts +9 -1
- package/dist/plugins/haveibeenpwned/index.mjs +33 -13
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/jwt/verify.mjs +2 -2
- package/dist/plugins/last-login-method/index.mjs +1 -0
- package/dist/plugins/oauth-proxy/index.d.mts +17 -0
- package/dist/plugins/oauth-proxy/index.mjs +145 -99
- package/dist/plugins/one-tap/index.mjs +0 -1
- package/dist/plugins/open-api/generator.mjs +15 -1
- package/dist/plugins/organization/has-permission.mjs +4 -2
- package/dist/plugins/phone-number/routes.mjs +0 -2
- package/dist/plugins/siwe/index.mjs +5 -6
- package/dist/plugins/two-factor/client.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.mjs +1 -0
- package/dist/plugins/two-factor/index.d.mts +1 -0
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/state.d.mts +0 -13
- package/dist/test-utils/http-test-instance.d.mts +1 -1
- package/dist/test-utils/test-instance.mjs +27 -10
- package/package.json +10 -10
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { wildcardMatch } from "../utils/wildcard.mjs";
|
|
2
2
|
import { getHost, getOrigin, getProtocol } from "../utils/url.mjs";
|
|
3
3
|
//#region src/auth/trusted-origins.ts
|
|
4
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
|
|
5
|
+
const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
|
|
6
|
+
const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
|
|
4
7
|
/**
|
|
5
8
|
* Resolves `.` and `..` segments in a path after percent-decoding so a
|
|
6
9
|
* path-pinned pattern cannot be bypassed with traversal: e.g.
|
|
@@ -31,6 +34,7 @@ const normalizePath = (path) => {
|
|
|
31
34
|
* a path-pinned pattern.
|
|
32
35
|
*/
|
|
33
36
|
const parseCustomSchemeOrigin = (value) => {
|
|
37
|
+
if (CONTROL_CHARACTER_PATTERN.test(value)) return null;
|
|
34
38
|
const schemeEnd = value.indexOf(":");
|
|
35
39
|
if (schemeEnd <= 0) return null;
|
|
36
40
|
const scheme = value.slice(0, schemeEnd).toLowerCase();
|
|
@@ -47,7 +51,8 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
47
51
|
rest = rest.slice(authorityEnd);
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
|
-
const
|
|
54
|
+
const pathEnd = rest.search(/[?#]/);
|
|
55
|
+
const path = normalizePath(pathEnd === -1 ? rest : rest.slice(0, pathEnd));
|
|
51
56
|
return {
|
|
52
57
|
scheme,
|
|
53
58
|
authority: authority.toLowerCase(),
|
|
@@ -55,6 +60,23 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
55
60
|
};
|
|
56
61
|
};
|
|
57
62
|
/**
|
|
63
|
+
* Validates root-relative redirects against ambiguous browser and router parsing.
|
|
64
|
+
*
|
|
65
|
+
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-4.2
|
|
66
|
+
* @see https://url.spec.whatwg.org/#concept-basic-url-parser
|
|
67
|
+
*/
|
|
68
|
+
const isSafeRelativeURL = (value) => {
|
|
69
|
+
if (!value.startsWith("/") || value.startsWith("//") || value.includes("\\") || CONTROL_CHARACTER_PATTERN.test(value)) return false;
|
|
70
|
+
const pathEnd = value.search(/[?#]/);
|
|
71
|
+
const path = pathEnd === -1 ? value : value.slice(0, pathEnd);
|
|
72
|
+
if (ENCODED_PATH_SEPARATOR_PATTERN.test(path)) return false;
|
|
73
|
+
try {
|
|
74
|
+
return new URL(value, RELATIVE_URL_PARSER_ORIGIN).origin === RELATIVE_URL_PARSER_ORIGIN;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
58
80
|
* Matches the given url against an origin or origin pattern
|
|
59
81
|
* See "options.trustedOrigins" for details of supported patterns
|
|
60
82
|
*
|
|
@@ -64,10 +86,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
64
86
|
* @returns {boolean} true if the URL matches the origin pattern, false otherwise.
|
|
65
87
|
*/
|
|
66
88
|
const matchesOriginPattern = (url, pattern, settings) => {
|
|
67
|
-
if (url.startsWith("/"))
|
|
68
|
-
if (settings?.allowRelativePaths) return url.startsWith("/") && /^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(url);
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
89
|
+
if (url.startsWith("/")) return settings?.allowRelativePaths === true && isSafeRelativeURL(url);
|
|
71
90
|
if (pattern.includes("*") || pattern.includes("?")) {
|
|
72
91
|
if (pattern.includes("://")) return wildcardMatch(pattern)(getOrigin(url) || url);
|
|
73
92
|
const host = getHost(url);
|
package/dist/client/config.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SessionData } from "./session-atom.mjs";
|
|
2
2
|
import { BetterAuthClientOptions, ClientAtomListener } from "@better-auth/core";
|
|
3
|
+
import * as _$nanostores from "nanostores";
|
|
3
4
|
import { WritableAtom } from "nanostores";
|
|
4
5
|
import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ declare const getClientConfig: (options?: BetterAuthClientOptions | undefined, l
|
|
|
11
12
|
pluginPathMethods: Record<string, "GET" | "POST">;
|
|
12
13
|
atomListeners: ClientAtomListener[];
|
|
13
14
|
hydrateSession: (sessionData: SessionData | null) => void;
|
|
15
|
+
$sessionSignal: _$nanostores.PreinitializedWritableAtom<boolean> & object;
|
|
14
16
|
$fetch: _$_better_fetch_fetch0.BetterFetch<{
|
|
15
17
|
plugins: (_$_better_fetch_fetch0.BetterFetchPlugin<Record<string, any>> | {
|
|
16
18
|
id: string;
|
package/dist/client/config.mjs
CHANGED
|
@@ -22,7 +22,7 @@ type ClientSession<Option extends BetterAuthClientOptions> = InferClientAPI<Opti
|
|
|
22
22
|
* Lynx client returned by `createAuthClient`.
|
|
23
23
|
*/
|
|
24
24
|
type LynxAuthClient<Option extends BetterAuthClientOptions> = UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
|
|
25
|
-
hydrateSession
|
|
25
|
+
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
|
26
26
|
useSession: () => {
|
|
27
27
|
data: ClientSession<Option>;
|
|
28
28
|
isPending: boolean;
|
|
@@ -22,7 +22,7 @@ type ClientSession<Option extends BetterAuthClientOptions> = InferClientAPI<Opti
|
|
|
22
22
|
* React client returned by `createAuthClient`.
|
|
23
23
|
*/
|
|
24
24
|
type ReactAuthClient<Option extends BetterAuthClientOptions> = UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
|
|
25
|
-
hydrateSession
|
|
25
|
+
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
|
26
26
|
useSession: () => {
|
|
27
27
|
data: ClientSession<Option>;
|
|
28
28
|
isPending: boolean;
|
|
@@ -22,7 +22,7 @@ type ClientSession<Option extends BetterAuthClientOptions> = InferClientAPI<Opti
|
|
|
22
22
|
* Solid client returned by `createAuthClient`.
|
|
23
23
|
*/
|
|
24
24
|
type SolidAuthClient<Option extends BetterAuthClientOptions> = UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
|
|
25
|
-
hydrateSession
|
|
25
|
+
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
|
26
26
|
useSession: () => Accessor<{
|
|
27
27
|
data: ClientSession<Option>;
|
|
28
28
|
isPending: boolean;
|
|
@@ -22,7 +22,7 @@ type ClientSession<Option extends BetterAuthClientOptions> = InferClientAPI<Opti
|
|
|
22
22
|
* Svelte client returned by `createAuthClient`.
|
|
23
23
|
*/
|
|
24
24
|
type SvelteAuthClient<Option extends BetterAuthClientOptions> = UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
|
|
25
|
-
hydrateSession
|
|
25
|
+
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
|
26
26
|
useSession: () => Atom<{
|
|
27
27
|
data: ClientSession<Option>;
|
|
28
28
|
error: BetterFetchError | null;
|
|
@@ -20,7 +20,7 @@ type ClientSession<Option extends BetterAuthClientOptions> = InferClientAPI<Opti
|
|
|
20
20
|
* Client returned by `createAuthClient`.
|
|
21
21
|
*/
|
|
22
22
|
type AuthClient<Option extends BetterAuthClientOptions> = UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
|
|
23
|
-
hydrateSession
|
|
23
|
+
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
|
24
24
|
useSession: Atom<{
|
|
25
25
|
data: ClientSession<Option>;
|
|
26
26
|
error: BetterFetchError | null;
|
|
@@ -4,7 +4,7 @@ import { getClientConfig } from "../config.mjs";
|
|
|
4
4
|
import { BetterAuthClientOptions } from "@better-auth/core";
|
|
5
5
|
import { BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
6
6
|
import { BetterFetchError, BetterFetchResponse } from "@better-fetch/fetch";
|
|
7
|
-
import { DeepReadonly, Ref } from "vue";
|
|
7
|
+
import { DeepReadonly, Ref, WatchSource } from "vue";
|
|
8
8
|
export * from "nanostores";
|
|
9
9
|
export * from "@better-fetch/fetch";
|
|
10
10
|
|
|
@@ -18,32 +18,43 @@ type ClientConfig = ReturnType<typeof getClientConfig>;
|
|
|
18
18
|
type ClientSession<Option extends BetterAuthClientOptions> = InferClientAPI<Option> extends {
|
|
19
19
|
getSession: () => Promise<infer Res>;
|
|
20
20
|
} ? Res extends BetterFetchResponse<infer S> ? S : Res : never;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Minimal Nuxt-compatible fetch contract for `useSession(useFetch)`.
|
|
23
|
+
* Compatibility with Nuxt's `useFetch` and `UseFetchOptions` is checked by the Nuxt fixture type test.
|
|
24
|
+
*/
|
|
25
|
+
type SessionFetch = (url: string, options: {
|
|
26
|
+
headers?: HeadersInit;
|
|
27
|
+
key: string;
|
|
28
|
+
watch: WatchSource<unknown>[];
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
data: Ref<unknown>;
|
|
31
|
+
error: Ref<unknown>;
|
|
32
|
+
}>;
|
|
33
|
+
type VueSessionState<Option extends BetterAuthClientOptions> = DeepReadonly<Ref<{
|
|
34
|
+
data: ClientSession<Option>;
|
|
35
|
+
isPending: boolean;
|
|
36
|
+
isRefetching: boolean;
|
|
37
|
+
error: BetterFetchError | null;
|
|
38
|
+
refetch: (queryParams?: {
|
|
39
|
+
query?: SessionQueryParams;
|
|
40
|
+
} | undefined) => Promise<void>;
|
|
41
|
+
}>>;
|
|
42
|
+
type SessionFetchResult<Option extends BetterAuthClientOptions> = {
|
|
43
|
+
data: Ref<ClientSession<Option>>;
|
|
44
|
+
isPending: false;
|
|
45
|
+
error: Ref<{
|
|
46
|
+
message?: string | undefined;
|
|
47
|
+
status: number;
|
|
48
|
+
statusText: string;
|
|
39
49
|
}>;
|
|
40
50
|
};
|
|
41
51
|
/**
|
|
42
52
|
* Vue client returned by `createAuthClient`.
|
|
43
53
|
*/
|
|
44
54
|
type VueAuthClient<Option extends BetterAuthClientOptions> = UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
|
|
45
|
-
hydrateSession
|
|
46
|
-
useSession:
|
|
55
|
+
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
|
56
|
+
useSession(): VueSessionState<Option>;
|
|
57
|
+
useSession(useFetch: SessionFetch): Promise<SessionFetchResult<Option>>;
|
|
47
58
|
$Infer: {
|
|
48
59
|
Session: NonNullable<ClientSession<Option>>;
|
|
49
60
|
};
|
|
@@ -6,18 +6,36 @@ import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
|
|
|
6
6
|
function getAtomKey(str) {
|
|
7
7
|
return `use${capitalizeFirstLetter(str)}`;
|
|
8
8
|
}
|
|
9
|
+
/** Preserves standard `HeadersInit` values and removes undefined record entries. */
|
|
10
|
+
function toHeadersInit(headers) {
|
|
11
|
+
if (!headers) return void 0;
|
|
12
|
+
if (headers instanceof Headers || Array.isArray(headers)) return headers;
|
|
13
|
+
const normalizedHeaders = {};
|
|
14
|
+
for (const [name, value] of Object.entries(headers)) if (value !== void 0) normalizedHeaders[name] = value;
|
|
15
|
+
return normalizedHeaders;
|
|
16
|
+
}
|
|
9
17
|
function createAuthClient(options) {
|
|
10
|
-
const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $fetch, $store, atomListeners } = getClientConfig(options, false);
|
|
18
|
+
const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $sessionSignal, $fetch, $store, atomListeners } = getClientConfig(options, false);
|
|
19
|
+
const sessionCacheKey = [
|
|
20
|
+
"better-auth",
|
|
21
|
+
"session",
|
|
22
|
+
options?.baseURL || "inferred",
|
|
23
|
+
options?.basePath ?? "/api/auth"
|
|
24
|
+
].join(":");
|
|
11
25
|
const resolvedHooks = {};
|
|
12
26
|
for (const [key, value] of Object.entries(pluginsAtoms)) resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
|
13
27
|
function useSession(useFetch) {
|
|
14
28
|
if (useFetch) {
|
|
15
|
-
const
|
|
16
|
-
return useFetch(`${baseURL}/get-session`, {
|
|
29
|
+
const sessionSignal = useStore($sessionSignal);
|
|
30
|
+
return useFetch(`${baseURL}/get-session`, {
|
|
31
|
+
headers: toHeadersInit(options?.fetchOptions?.headers),
|
|
32
|
+
key: sessionCacheKey,
|
|
33
|
+
watch: [sessionSignal]
|
|
34
|
+
}).then((result) => {
|
|
17
35
|
return {
|
|
18
|
-
data:
|
|
36
|
+
data: result.data,
|
|
19
37
|
isPending: false,
|
|
20
|
-
error:
|
|
38
|
+
error: result.error
|
|
21
39
|
};
|
|
22
40
|
});
|
|
23
41
|
}
|
|
@@ -15,6 +15,7 @@ import { getAuthTables } from "@better-auth/core/db";
|
|
|
15
15
|
import { createLogger, env, isProduction, isTest } from "@better-auth/core/env";
|
|
16
16
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
17
17
|
import { generateId } from "@better-auth/core/utils/id";
|
|
18
|
+
import { schemaCheckFor } from "@better-auth/core/db/internal";
|
|
18
19
|
import { socialProviders } from "@better-auth/core/social-providers";
|
|
19
20
|
import { findInvalidTrustedProxies } from "@better-auth/core/utils/ip";
|
|
20
21
|
import { createTelemetry } from "@better-auth/telemetry";
|
|
@@ -227,6 +228,7 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
227
228
|
};
|
|
228
229
|
const initOrPromise = runPluginInit(ctx);
|
|
229
230
|
if (isPromise(initOrPromise)) await initOrPromise;
|
|
231
|
+
ctx.checkSchema = schemaCheckFor(ctx.adapter);
|
|
230
232
|
return ctx;
|
|
231
233
|
}
|
|
232
234
|
//#endregion
|
package/dist/cookies/cache.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sessionSchema, userSchema } from "@better-auth/core/db";
|
|
2
|
+
import { logger } from "@better-auth/core/env";
|
|
2
3
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
3
4
|
import * as z from "zod";
|
|
4
5
|
//#region src/cookies/cache.ts
|
|
@@ -14,8 +15,15 @@ const compactCookieCacheSchema = z.object({
|
|
|
14
15
|
signature: z.string()
|
|
15
16
|
});
|
|
16
17
|
function parseCookieCachePayload(value) {
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const parsed = safeJSONParse(value);
|
|
19
|
+
if (parsed === null) return null;
|
|
20
|
+
const result = cookieCachePayloadSchema.safeParse(parsed);
|
|
21
|
+
if (result.success) return result.data;
|
|
22
|
+
logger.warn("Cookie cache payload failed schema validation", { issues: result.error.issues.map(({ code, path }) => ({
|
|
23
|
+
code,
|
|
24
|
+
path
|
|
25
|
+
})) });
|
|
26
|
+
return null;
|
|
19
27
|
}
|
|
20
28
|
function parseCompactCookieCache(value) {
|
|
21
29
|
const result = compactCookieCacheSchema.safeParse(value);
|
|
@@ -26,13 +26,24 @@ const MAX_COOKIE_CHUNKS = 100;
|
|
|
26
26
|
function getMaxCookieValueSize(name, options) {
|
|
27
27
|
return MAX_COOKIE_SIZE - serializeCookie(name, "", { ...options }).length;
|
|
28
28
|
}
|
|
29
|
+
function parseCookieChunkIndex(cookieName, name) {
|
|
30
|
+
const prefix = `${cookieName}.`;
|
|
31
|
+
if (!name.startsWith(prefix)) return null;
|
|
32
|
+
const suffix = name.slice(prefix.length);
|
|
33
|
+
const index = Number(suffix);
|
|
34
|
+
if (!Number.isSafeInteger(index) || index < 0 || String(index) !== suffix) return null;
|
|
35
|
+
return index;
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Read all existing chunks from cookies
|
|
31
39
|
*/
|
|
32
40
|
function readExistingChunks(cookieName, ctx) {
|
|
33
41
|
const chunks = {};
|
|
34
42
|
const cookies = parseCookies(ctx.headers?.get("cookie") || "");
|
|
35
|
-
for (const [name, value] of cookies)
|
|
43
|
+
for (const [name, value] of cookies) {
|
|
44
|
+
if (name !== cookieName && parseCookieChunkIndex(cookieName, name) === null) continue;
|
|
45
|
+
chunks[name] = value;
|
|
46
|
+
}
|
|
36
47
|
return chunks;
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
@@ -128,10 +139,10 @@ function getChunkedCookie(ctx, cookieName) {
|
|
|
128
139
|
const chunks = [];
|
|
129
140
|
const cookieHeader = ctx.headers?.get("cookie");
|
|
130
141
|
if (!cookieHeader) return null;
|
|
131
|
-
for (const [name, val] of parseCookies(cookieHeader))
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
for (const [name, val] of parseCookies(cookieHeader)) {
|
|
143
|
+
const index = parseCookieChunkIndex(cookieName, name);
|
|
144
|
+
if (index === null) continue;
|
|
145
|
+
chunks.push({
|
|
135
146
|
index,
|
|
136
147
|
value: val
|
|
137
148
|
});
|
|
@@ -3,10 +3,9 @@ import { BetterAuthError } from "@better-auth/core/error";
|
|
|
3
3
|
//#region src/db/adapter-kysely.ts
|
|
4
4
|
async function getAdapter(options) {
|
|
5
5
|
return getBaseAdapter(options, async (opts) => {
|
|
6
|
-
const { createKyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
|
|
6
|
+
const { createKyselyAdapter, kyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
|
|
7
7
|
const { kysely, databaseType, transaction } = await createKyselyAdapter(opts);
|
|
8
8
|
if (!kysely) throw new BetterAuthError("Failed to initialize database adapter");
|
|
9
|
-
const { kyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
|
|
10
9
|
return kyselyAdapter(kysely, {
|
|
11
10
|
type: databaseType || "sqlite",
|
|
12
11
|
debugLogs: opts.database && "debugLogs" in opts.database ? opts.database.debugLogs : false,
|
|
@@ -2,9 +2,9 @@ import { getSchema } from "./get-schema.mjs";
|
|
|
2
2
|
import { getAuthTables } from "@better-auth/core/db";
|
|
3
3
|
import { createLogger } from "@better-auth/core/env";
|
|
4
4
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
|
-
import { createKyselyAdapter } from "@better-auth/kysely-adapter";
|
|
5
|
+
import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
|
|
6
6
|
import { initGetFieldName, initGetModelName } from "@better-auth/core/db/adapter";
|
|
7
|
-
import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey } from "@better-auth/core/db/internal";
|
|
7
|
+
import { diffSchema, formatSchemaFinding, getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, invalidateSchemaChecks } from "@better-auth/core/db/internal";
|
|
8
8
|
import { sql } from "kysely";
|
|
9
9
|
//#region src/db/get-migration.ts
|
|
10
10
|
const map = {
|
|
@@ -103,7 +103,20 @@ function databaseValueIsTrue(value) {
|
|
|
103
103
|
if (typeof value === "number") return value !== 0;
|
|
104
104
|
return value === "1" || value?.toLowerCase() === "true" || value === "t";
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
function toDatabaseIndexMap(indexes) {
|
|
107
|
+
return new Map(indexes.map((index) => {
|
|
108
|
+
const columns = [...index.columns].sort((left, right) => left.position - right.position);
|
|
109
|
+
return [createDatabaseIndexKey(index.table, index.name), {
|
|
110
|
+
columns: columns.flatMap((column) => column.name === null ? [] : [column.name]),
|
|
111
|
+
name: index.name,
|
|
112
|
+
table: index.table,
|
|
113
|
+
unique: index.unique,
|
|
114
|
+
validFullColumns: index.valid && !index.partial && columns.length > 0 && columns.every((column) => column.name !== null && column.fullLength)
|
|
115
|
+
}];
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
async function getDatabaseIndexMap(db, dbType, schemaName, tableNames, introspectIndexes) {
|
|
119
|
+
if (introspectIndexes) return toDatabaseIndexMap(await introspectIndexes(tableNames));
|
|
107
120
|
let rows;
|
|
108
121
|
if (dbType === "sqlite") rows = (await sql`
|
|
109
122
|
SELECT
|
|
@@ -151,7 +164,8 @@ async function getDatabaseIndexes(db, dbType, schemaName) {
|
|
|
151
164
|
column_name AS columnName,
|
|
152
165
|
non_unique AS nonUnique,
|
|
153
166
|
seq_in_index AS columnPosition,
|
|
154
|
-
sub_part AS prefixLength
|
|
167
|
+
sub_part AS prefixLength,
|
|
168
|
+
COALESCE(LOWER(comment) = 'disabled', FALSE) AS isDisabled
|
|
155
169
|
FROM information_schema.statistics
|
|
156
170
|
WHERE table_schema = DATABASE()
|
|
157
171
|
`.execute(db)).rows;
|
|
@@ -180,7 +194,7 @@ async function getDatabaseIndexes(db, dbType, schemaName) {
|
|
|
180
194
|
AND indexes.name IS NOT NULL
|
|
181
195
|
AND index_columns.key_ordinal > 0
|
|
182
196
|
`.execute(db)).rows;
|
|
183
|
-
const
|
|
197
|
+
const indexMetadata = /* @__PURE__ */ new Map();
|
|
184
198
|
for (const row of rows) {
|
|
185
199
|
const table = row.tableName ?? row.table_name ?? row.TABLE_NAME ?? row.tablename ?? row.tbl_name;
|
|
186
200
|
const name = row.indexName ?? row.index_name ?? row.INDEX_NAME ?? row.name;
|
|
@@ -190,28 +204,29 @@ async function getDatabaseIndexes(db, dbType, schemaName) {
|
|
|
190
204
|
const nonUnique = row.nonUnique ?? row.non_unique ?? row.NON_UNIQUE;
|
|
191
205
|
const unique = nonUnique === void 0 ? databaseValueIsTrue(row.isUnique ?? row.is_unique) : !databaseValueIsTrue(nonUnique);
|
|
192
206
|
const position = Number(row.columnPosition ?? row.column_position ?? row.keyOrdinal ?? row.key_ordinal ?? row.ordinality ?? row.seqInIndex ?? row.seq_in_index ?? row.SEQ_IN_INDEX ?? row.seqno ?? 0);
|
|
193
|
-
const
|
|
194
|
-
|
|
207
|
+
const indexColumn = {
|
|
208
|
+
fullLength: column !== void 0 && column !== null && (row.prefixLength === void 0 || row.prefixLength === null),
|
|
209
|
+
name: column ?? null,
|
|
210
|
+
position
|
|
211
|
+
};
|
|
212
|
+
const partial = databaseValueIsTrue(row.isPartial);
|
|
213
|
+
const valid = !databaseValueIsTrue(row.isDisabled) && !databaseValueIsTrue(row.isHypothetical) && (row.isValid === void 0 || databaseValueIsTrue(row.isValid));
|
|
214
|
+
const existing = indexMetadata.get(key);
|
|
215
|
+
indexMetadata.set(key, existing ? {
|
|
216
|
+
...existing,
|
|
217
|
+
columns: [...existing.columns, indexColumn],
|
|
218
|
+
partial: existing.partial || partial,
|
|
219
|
+
valid: existing.valid && valid
|
|
220
|
+
} : {
|
|
221
|
+
columns: [indexColumn],
|
|
195
222
|
name,
|
|
223
|
+
partial,
|
|
196
224
|
table,
|
|
197
225
|
unique,
|
|
198
|
-
|
|
199
|
-
};
|
|
200
|
-
if (column) index.columns.push({
|
|
201
|
-
name: column,
|
|
202
|
-
position
|
|
226
|
+
valid
|
|
203
227
|
});
|
|
204
|
-
else index.validFullColumns = false;
|
|
205
|
-
if (databaseValueIsTrue(row.isPartial) || databaseValueIsTrue(row.isDisabled) || databaseValueIsTrue(row.isHypothetical) || row.isValid !== void 0 && !databaseValueIsTrue(row.isValid) || row.prefixLength !== void 0 && row.prefixLength !== null) index.validFullColumns = false;
|
|
206
|
-
indexRows.set(key, index);
|
|
207
228
|
}
|
|
208
|
-
return
|
|
209
|
-
columns: index.columns.sort((left, right) => left.position - right.position).map((column) => column.name),
|
|
210
|
-
name: index.name,
|
|
211
|
-
table: index.table,
|
|
212
|
-
unique: index.unique,
|
|
213
|
-
validFullColumns: index.validFullColumns
|
|
214
|
-
}]));
|
|
229
|
+
return toDatabaseIndexMap([...indexMetadata.values()]);
|
|
215
230
|
}
|
|
216
231
|
async function getDatabaseColumnBounds(db, dbType, schemaName) {
|
|
217
232
|
if (dbType !== "mysql" && dbType !== "mssql") return /* @__PURE__ */ new Map();
|
|
@@ -278,7 +293,6 @@ function assertExistingTableIndexFits({ columnBounds, dbType, existingColumns, f
|
|
|
278
293
|
}
|
|
279
294
|
if (requiredBytes > byteBudget) throw new BetterAuthError(`Cannot create database index "${index.name}" on existing table "${table}" because its columns can exceed ${dbType === "mysql" ? "MySQL" : "SQL Server"}'s ${byteBudget}-byte index-key limit. Bound the indexed string columns to the generated schema lengths, resolve oversized values, then run the migration again.`);
|
|
280
295
|
}
|
|
281
|
-
const columnBackfillGuideUrl = "https://better-auth.com/docs/guides/1-7-upgrade-guide#account-identity-is-scoped-by-issuer";
|
|
282
296
|
/**
|
|
283
297
|
* Thrown when {@link getMigrations} refuses to add a required column with no
|
|
284
298
|
* default value to a populated table. Distinct from the plain
|
|
@@ -305,27 +319,6 @@ function matchType(columnDataType, fieldType, dbType) {
|
|
|
305
319
|
return (Array.isArray(fieldType) ? types["string"].map((t) => t.toLowerCase()) : types[fieldType].map((t) => t.toLowerCase())).includes(normalize(columnDataType));
|
|
306
320
|
}
|
|
307
321
|
/**
|
|
308
|
-
* Get the current PostgreSQL schema (search_path) for the database connection
|
|
309
|
-
* Returns the first schema in the search_path, defaulting to 'public' if not found
|
|
310
|
-
*/
|
|
311
|
-
async function getPostgresSchema(db) {
|
|
312
|
-
try {
|
|
313
|
-
const result = await sql`SHOW search_path`.execute(db);
|
|
314
|
-
const searchPath = result.rows[0]?.search_path ?? result.rows[0]?.searchPath;
|
|
315
|
-
if (searchPath) return searchPath.split(",").map((s) => s.trim()).map((s) => s.replace(/^["']|["']$/g, "")).filter((s) => !s.startsWith("$") && !s.startsWith("\\$"))[0] || "public";
|
|
316
|
-
} catch {}
|
|
317
|
-
return "public";
|
|
318
|
-
}
|
|
319
|
-
async function getMssqlSchema(db) {
|
|
320
|
-
try {
|
|
321
|
-
return (await sql`
|
|
322
|
-
SELECT SCHEMA_NAME() AS "schemaName"
|
|
323
|
-
`.execute(db)).rows[0]?.schemaName || "dbo";
|
|
324
|
-
} catch {
|
|
325
|
-
return "dbo";
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
322
|
* Build the migration plan that `auth migrate` executes and `auth generate`
|
|
330
323
|
* prints for the Kysely adapter.
|
|
331
324
|
*
|
|
@@ -342,18 +335,13 @@ async function getMssqlSchema(db) {
|
|
|
342
335
|
async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
343
336
|
const betterAuthSchema = getSchema(config);
|
|
344
337
|
const authTables = getAuthTables(config);
|
|
345
|
-
const accountIssuer = authTables.account && {
|
|
346
|
-
table: authTables.account.modelName,
|
|
347
|
-
column: authTables.account.fields.issuer?.fieldName || "issuer"
|
|
348
|
-
};
|
|
349
|
-
const isAccountIssuerColumn = (table, column) => table === accountIssuer?.table && column === accountIssuer.column;
|
|
350
338
|
const logger = createLogger(config.logger);
|
|
351
339
|
const unsafeChanges = [];
|
|
352
340
|
const reportUnsafeChange = (message) => {
|
|
353
341
|
if (throwOnUnsafe) throw new UnsafeMigrationError(message);
|
|
354
342
|
unsafeChanges.push(message);
|
|
355
343
|
};
|
|
356
|
-
let { kysely: db, databaseType: dbType } = await createKyselyAdapter(config);
|
|
344
|
+
let { kysely: db, databaseType: dbType, introspectIndexes } = await createKyselyAdapter(config);
|
|
357
345
|
if (!dbType) {
|
|
358
346
|
logger.warn("Could not determine database type, defaulting to sqlite. Please provide a type in the database options to avoid this.");
|
|
359
347
|
dbType = "sqlite";
|
|
@@ -378,7 +366,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
378
366
|
}
|
|
379
367
|
} else if (dbType === "mssql") logger.debug(`SQL Server migration: Using schema '${currentSchema}' (from the current user's default schema)`);
|
|
380
368
|
const allTableMetadata = await db.introspection.getTables();
|
|
381
|
-
const
|
|
369
|
+
const databaseIndexMap = await getDatabaseIndexMap(db, dbType, currentSchema, allTableMetadata.map((table) => table.name), introspectIndexes);
|
|
382
370
|
const databaseColumnBounds = await getDatabaseColumnBounds(db, dbType, currentSchema);
|
|
383
371
|
let tableMetadata = allTableMetadata;
|
|
384
372
|
if (dbType === "postgres") try {
|
|
@@ -395,6 +383,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
395
383
|
logger.warn(`Could not filter tables by schema. Using all discovered tables. Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
396
384
|
}
|
|
397
385
|
else if (dbType === "mssql") tableMetadata = allTableMetadata.filter((table) => table.schema === currentSchema);
|
|
386
|
+
const schemaProblems = diffSchema(toPhysicalSchema(db, betterAuthSchema), toIntrospectedTables(tableMetadata)).filter((finding) => finding.kind === "unexpected-required-column").map((finding) => formatSchemaFinding(finding, "database"));
|
|
398
387
|
const toBeCreated = [];
|
|
399
388
|
const toBeAdded = [];
|
|
400
389
|
const toBeAddedIndexes = [];
|
|
@@ -405,13 +394,13 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
405
394
|
for (const index of value.indexes ?? []) {
|
|
406
395
|
const name = index.name;
|
|
407
396
|
const indexKey = createDatabaseIndexKey(key, name);
|
|
408
|
-
const existingIndex =
|
|
397
|
+
const existingIndex = databaseIndexMap.get(indexKey);
|
|
409
398
|
if (existingIndex) {
|
|
410
399
|
if (!databaseIndexMatches(existingIndex, index)) throw new BetterAuthError(`Database index "${name}" on table "${key}" does not match the configured fields and uniqueness. Rename or replace the existing index, then run the migration again.`);
|
|
411
400
|
continue;
|
|
412
401
|
}
|
|
413
402
|
if (dbType === "sqlite" || dbType === "postgres") {
|
|
414
|
-
const indexOnAnotherTable = [...
|
|
403
|
+
const indexOnAnotherTable = [...databaseIndexMap.values()].find((databaseIndex) => getPortableDatabaseIdentifierKey(databaseIndex.name) === getPortableDatabaseIdentifierKey(name) && getPortableDatabaseIdentifierKey(databaseIndex.table) !== getPortableDatabaseIdentifierKey(key));
|
|
415
404
|
if (indexOnAnotherTable) throw new BetterAuthError(`Database index name "${name}" is already used by table "${indexOnAnotherTable.table}". Index names must be unique across the schema.`);
|
|
416
405
|
}
|
|
417
406
|
const plannedIndex = plannedIndexes.get(indexKey);
|
|
@@ -587,8 +576,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
587
576
|
}
|
|
588
577
|
if (populated) {
|
|
589
578
|
const textDetail = field.type === "string" ? " For a text column, every existing row ends up with the same empty string." : "";
|
|
590
|
-
|
|
591
|
-
reportUnsafeChange(`Cannot add required column "${fieldName}" to populated table "${table.table}": the schema declares no default value, so existing rows have no value to backfill. MySQL accepts this statement instead of rejecting it and fills every existing row with an implicit default for the column type, reporting a successful migration over corrupted data.${textDetail} Add the column as nullable, backfill a correct value for every row, then make it NOT NULL.${guideLink}`);
|
|
579
|
+
reportUnsafeChange(`Cannot add required column "${fieldName}" to populated table "${table.table}": the schema declares no default value, so existing rows have no value to backfill. MySQL accepts this statement instead of rejecting it and fills every existing row with an implicit default for the column type, reporting a successful migration over corrupted data.${textDetail} Add the column as nullable, backfill a correct value for every row, then make it NOT NULL.`);
|
|
592
580
|
}
|
|
593
581
|
}
|
|
594
582
|
const type = getType(field, fieldName, getTableIndexStringLength(table.table, fieldName));
|
|
@@ -653,7 +641,11 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
653
641
|
}
|
|
654
642
|
for (const index of deferredIndexes) migrations.push(index);
|
|
655
643
|
async function runMigrations() {
|
|
656
|
-
|
|
644
|
+
try {
|
|
645
|
+
for (const migration of migrations) await migration.execute();
|
|
646
|
+
} finally {
|
|
647
|
+
if (migrations.length && config.database) invalidateSchemaChecks(config.database);
|
|
648
|
+
}
|
|
657
649
|
}
|
|
658
650
|
async function compileMigrations() {
|
|
659
651
|
return migrations.map((m) => m.compile().sql).join(";\n\n") + ";";
|
|
@@ -663,6 +655,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
663
655
|
toBeAdded,
|
|
664
656
|
toBeAddedIndexes,
|
|
665
657
|
unsafeChanges,
|
|
658
|
+
schemaProblems,
|
|
666
659
|
runMigrations,
|
|
667
660
|
compileMigrations
|
|
668
661
|
};
|