better-auth 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/middlewares/origin-check.mjs +9 -5
- package/dist/api/routes/callback.mjs +7 -5
- package/dist/api/routes/email-verification.mjs +4 -2
- package/dist/api/routes/error.mjs +12 -8
- package/dist/api/routes/sign-in.mjs +1 -0
- package/dist/auth/trusted-origins.mjs +21 -4
- 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 +1 -1
- package/dist/cookies/cache.mjs +10 -2
- package/dist/db/get-migration.mjs +39 -24
- package/dist/db/internal-adapter.mjs +3 -3
- package/dist/db/with-hooks.mjs +7 -7
- package/dist/index.mjs +1 -0
- package/dist/oauth2/errors.mjs +4 -3
- package/dist/oauth2/state.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +1 -1
- package/dist/plugins/anonymous/index.mjs +5 -1
- package/dist/plugins/anonymous/types.d.mts +2 -3
- package/dist/plugins/generic-oauth/providers/microsoft-entra-id.mjs +15 -5
- package/dist/plugins/haveibeenpwned/index.mjs +2 -2
- package/dist/plugins/jwt/verify.mjs +2 -2
- package/dist/plugins/open-api/index.mjs +1 -0
- package/dist/plugins/phone-number/routes.mjs +1 -0
- package/dist/plugins/siwe/index.mjs +5 -3
- package/dist/test-utils/http-test-instance.d.mts +1 -1
- package/dist/test-utils/test-instance.mjs +17 -1
- package/dist/utils/index.mjs +5 -0
- package/package.json +9 -9
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getBaseURL, getOrigin } from "../../utils/url.mjs";
|
|
1
2
|
import { matchesOriginPattern } from "../../auth/trusted-origins.mjs";
|
|
2
3
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
3
4
|
import { normalizePathname } from "@better-auth/core/utils/url";
|
|
@@ -95,7 +96,8 @@ const originCheck = (getValue) => createAuthMiddleware(async (ctx) => {
|
|
|
95
96
|
async function validateOrigin(ctx, forceValidate = false) {
|
|
96
97
|
const headers = ctx.request?.headers;
|
|
97
98
|
if (!headers || !ctx.request) return;
|
|
98
|
-
const
|
|
99
|
+
const origin = headers.get("origin");
|
|
100
|
+
const originHeader = origin || headers.get("referer") || "";
|
|
99
101
|
const useCookies = headers.has("cookie");
|
|
100
102
|
if (ctx.context.skipCSRFCheck) return;
|
|
101
103
|
if (shouldSkipCSRFForBackwardCompat(ctx)) {
|
|
@@ -104,11 +106,13 @@ async function validateOrigin(ctx, forceValidate = false) {
|
|
|
104
106
|
}
|
|
105
107
|
if (shouldSkipOriginCheck(ctx)) return;
|
|
106
108
|
if (!(forceValidate || useCookies)) return;
|
|
107
|
-
|
|
109
|
+
const inferredBaseURL = origin === "null" && headers.get("sec-fetch-site") === "same-origin" ? getBaseURL(void 0, ctx.context.options.basePath, ctx.request, false, ctx.context.options.advanced?.trustedProxyHeaders) : void 0;
|
|
110
|
+
const originToValidate = (inferredBaseURL ? getOrigin(inferredBaseURL) : void 0) ?? originHeader;
|
|
111
|
+
if (!originToValidate || originToValidate === "null") throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.MISSING_OR_NULL_ORIGIN);
|
|
108
112
|
const trustedOrigins = Array.isArray(ctx.context.options.trustedOrigins) ? ctx.context.trustedOrigins : [...ctx.context.trustedOrigins, ...(await ctx.context.options.trustedOrigins?.(ctx.request))?.filter((v) => Boolean(v)) || []];
|
|
109
|
-
if (!trustedOrigins.some((origin) => matchesOriginPattern(
|
|
110
|
-
ctx.context.logger.error(`Invalid origin: ${
|
|
111
|
-
ctx.context.logger.info(`If it's a valid URL, please add ${
|
|
113
|
+
if (!trustedOrigins.some((origin) => matchesOriginPattern(originToValidate, origin))) {
|
|
114
|
+
ctx.context.logger.error(`Invalid origin: ${originToValidate}`);
|
|
115
|
+
ctx.context.logger.info(`If it's a valid URL, please add ${originToValidate} to trustedOrigins in your auth config\n`, `Current list of trustedOrigins: ${trustedOrigins}`);
|
|
112
116
|
throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.INVALID_ORIGIN);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
@@ -10,6 +10,7 @@ import { generateIdTokenNonce, generateState, parseState } from "../../oauth2/st
|
|
|
10
10
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
11
11
|
import { mergeScopes } from "@better-auth/core/oauth2";
|
|
12
12
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
13
|
+
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
13
14
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
14
15
|
import * as z from "zod";
|
|
15
16
|
//#region src/api/routes/callback.ts
|
|
@@ -52,7 +53,8 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
52
53
|
else throw new Error("Unsupported method");
|
|
53
54
|
} catch (e) {
|
|
54
55
|
c.context.logger.error("INVALID_CALLBACK_REQUEST", e);
|
|
55
|
-
|
|
56
|
+
const redirectURL = appendQueryParams(defaultErrorURL, new URLSearchParams({ error: "invalid_callback_request" }));
|
|
57
|
+
throw c.redirect(redirectURL);
|
|
56
58
|
}
|
|
57
59
|
const { code, error, state, error_description, device_id, user: userData, iss } = queryOrBody;
|
|
58
60
|
if (state === void 0 && code) {
|
|
@@ -71,16 +73,16 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
71
73
|
}
|
|
72
74
|
if (!state) {
|
|
73
75
|
c.context.logger.error("State not found", error);
|
|
74
|
-
const
|
|
75
|
-
throw c.redirect(
|
|
76
|
+
const redirectURL = appendQueryParams(defaultErrorURL, new URLSearchParams({ error: "state_not_found" }));
|
|
77
|
+
throw c.redirect(redirectURL);
|
|
76
78
|
}
|
|
77
79
|
const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp, idTokenNonce } = await parseState(c);
|
|
78
80
|
function redirectOnError(error, description) {
|
|
79
81
|
const baseURL = errorURL ?? defaultErrorURL;
|
|
80
82
|
const params = new URLSearchParams({ error });
|
|
81
83
|
if (description) params.set("error_description", description);
|
|
82
|
-
const
|
|
83
|
-
throw c.redirect(
|
|
84
|
+
const redirectURL = appendQueryParams(baseURL, params);
|
|
85
|
+
throw c.redirect(redirectURL);
|
|
84
86
|
}
|
|
85
87
|
if (error) redirectOnError(error, error_description);
|
|
86
88
|
if (!code) {
|
|
@@ -5,6 +5,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
|
|
|
5
5
|
import { getSessionFromCtx } from "./session.mjs";
|
|
6
6
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
7
7
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
8
|
+
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
8
9
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
9
10
|
import * as z from "zod";
|
|
10
11
|
import { jwtVerify } from "jose";
|
|
@@ -165,8 +166,9 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
165
166
|
}, async (ctx) => {
|
|
166
167
|
function redirectOnError(error) {
|
|
167
168
|
if (ctx.query.callbackURL) {
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
const params = new URLSearchParams({ error: error.code });
|
|
170
|
+
const redirectURL = appendQueryParams(ctx.query.callbackURL, params);
|
|
171
|
+
throw ctx.redirect(redirectURL);
|
|
170
172
|
}
|
|
171
173
|
throw APIError.from("UNAUTHORIZED", error);
|
|
172
174
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
2
2
|
import { isProduction } from "@better-auth/core/env";
|
|
3
|
+
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
3
4
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
4
5
|
//#region src/api/routes/error.ts
|
|
5
6
|
function sanitize(input) {
|
|
@@ -364,18 +365,21 @@ const error = createAuthEndpoint("/error", {
|
|
|
364
365
|
const unsanitizedDescription = url.searchParams.get("error_description") || null;
|
|
365
366
|
const safeCode = /^[\'A-Za-z0-9_-]+$/.test(unsanitizedCode || "") ? unsanitizedCode : "UNKNOWN";
|
|
366
367
|
const safeDescription = unsanitizedDescription ? sanitize(unsanitizedDescription) : null;
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
if (unsanitizedDescription)
|
|
368
|
+
const params = new URLSearchParams();
|
|
369
|
+
params.set("error", safeCode);
|
|
370
|
+
if (unsanitizedDescription) params.set("error_description", unsanitizedDescription);
|
|
370
371
|
const options = c.context.options;
|
|
371
372
|
const errorURL = options.onAPIError?.errorURL;
|
|
372
|
-
if (errorURL)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
if (errorURL) {
|
|
374
|
+
const redirectURL = appendQueryParams(errorURL, params);
|
|
375
|
+
return new Response(null, {
|
|
376
|
+
status: 302,
|
|
377
|
+
headers: { Location: redirectURL }
|
|
378
|
+
});
|
|
379
|
+
}
|
|
376
380
|
if (isProduction && !options.onAPIError?.customizeDefaultErrorPage) return new Response(null, {
|
|
377
381
|
status: 302,
|
|
378
|
-
headers: { Location: `/?${
|
|
382
|
+
headers: { Location: `/?${params.toString()}` }
|
|
379
383
|
});
|
|
380
384
|
return new Response(html(c.context.options, safeCode, safeDescription), { headers: { "Content-Type": "text/html" } });
|
|
381
385
|
});
|
|
@@ -9,6 +9,7 @@ import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
|
9
9
|
import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
|
|
10
10
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
11
11
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
12
|
+
import "../../utils/index.mjs";
|
|
12
13
|
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
13
14
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
14
15
|
import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
@@ -54,6 +54,26 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
54
54
|
path
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
|
+
const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
|
|
58
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
|
|
59
|
+
const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
|
|
60
|
+
/**
|
|
61
|
+
* Validates root-relative redirects against ambiguous browser and router parsing.
|
|
62
|
+
*
|
|
63
|
+
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-4.2
|
|
64
|
+
* @see https://url.spec.whatwg.org/#concept-basic-url-parser
|
|
65
|
+
*/
|
|
66
|
+
const isSafeRelativeURL = (value) => {
|
|
67
|
+
if (!value.startsWith("/") || value.startsWith("//") || value.includes("\\") || CONTROL_CHARACTER_PATTERN.test(value)) return false;
|
|
68
|
+
const pathEnd = value.search(/[?#]/);
|
|
69
|
+
const path = pathEnd === -1 ? value : value.slice(0, pathEnd);
|
|
70
|
+
if (ENCODED_PATH_SEPARATOR_PATTERN.test(path)) return false;
|
|
71
|
+
try {
|
|
72
|
+
return new URL(value, RELATIVE_URL_PARSER_ORIGIN).origin === RELATIVE_URL_PARSER_ORIGIN;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
57
77
|
/**
|
|
58
78
|
* Matches the given url against an origin or origin pattern
|
|
59
79
|
* See "options.trustedOrigins" for details of supported patterns
|
|
@@ -64,10 +84,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
64
84
|
* @returns {boolean} true if the URL matches the origin pattern, false otherwise.
|
|
65
85
|
*/
|
|
66
86
|
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
|
-
}
|
|
87
|
+
if (url.startsWith("/")) return settings?.allowRelativePaths === true && isSafeRelativeURL(url);
|
|
71
88
|
if (pattern.includes("*") || pattern.includes("?")) {
|
|
72
89
|
if (pattern.includes("://")) return wildcardMatch(pattern)(getOrigin(url) || url);
|
|
73
90
|
const host = getHost(url);
|
|
@@ -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;
|
|
@@ -42,7 +42,7 @@ type VueUseSession<Option extends BetterAuthClientOptions> = {
|
|
|
42
42
|
* Vue client returned by `createAuthClient`.
|
|
43
43
|
*/
|
|
44
44
|
type VueAuthClient<Option extends BetterAuthClientOptions> = UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & {
|
|
45
|
-
hydrateSession
|
|
45
|
+
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
|
46
46
|
useSession: VueUseSession<Option>;
|
|
47
47
|
$Infer: {
|
|
48
48
|
Session: NonNullable<ClientSession<Option>>;
|
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);
|
|
@@ -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();
|
|
@@ -353,7 +368,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
353
368
|
if (throwOnUnsafe) throw new UnsafeMigrationError(message);
|
|
354
369
|
unsafeChanges.push(message);
|
|
355
370
|
};
|
|
356
|
-
let { kysely: db, databaseType: dbType } = await createKyselyAdapter(config);
|
|
371
|
+
let { kysely: db, databaseType: dbType, introspectIndexes } = await createKyselyAdapter(config);
|
|
357
372
|
if (!dbType) {
|
|
358
373
|
logger.warn("Could not determine database type, defaulting to sqlite. Please provide a type in the database options to avoid this.");
|
|
359
374
|
dbType = "sqlite";
|
|
@@ -378,7 +393,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
378
393
|
}
|
|
379
394
|
} else if (dbType === "mssql") logger.debug(`SQL Server migration: Using schema '${currentSchema}' (from the current user's default schema)`);
|
|
380
395
|
const allTableMetadata = await db.introspection.getTables();
|
|
381
|
-
const
|
|
396
|
+
const databaseIndexMap = await getDatabaseIndexMap(db, dbType, currentSchema, allTableMetadata.map((table) => table.name), introspectIndexes);
|
|
382
397
|
const databaseColumnBounds = await getDatabaseColumnBounds(db, dbType, currentSchema);
|
|
383
398
|
let tableMetadata = allTableMetadata;
|
|
384
399
|
if (dbType === "postgres") try {
|
|
@@ -405,13 +420,13 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
405
420
|
for (const index of value.indexes ?? []) {
|
|
406
421
|
const name = index.name;
|
|
407
422
|
const indexKey = createDatabaseIndexKey(key, name);
|
|
408
|
-
const existingIndex =
|
|
423
|
+
const existingIndex = databaseIndexMap.get(indexKey);
|
|
409
424
|
if (existingIndex) {
|
|
410
425
|
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
426
|
continue;
|
|
412
427
|
}
|
|
413
428
|
if (dbType === "sqlite" || dbType === "postgres") {
|
|
414
|
-
const indexOnAnotherTable = [...
|
|
429
|
+
const indexOnAnotherTable = [...databaseIndexMap.values()].find((databaseIndex) => getPortableDatabaseIdentifierKey(databaseIndex.name) === getPortableDatabaseIdentifierKey(name) && getPortableDatabaseIdentifierKey(databaseIndex.table) !== getPortableDatabaseIdentifierKey(key));
|
|
415
430
|
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
431
|
}
|
|
417
432
|
const plannedIndex = plannedIndexes.get(indexKey);
|
|
@@ -3,7 +3,7 @@ import { getDate } from "../utils/date.mjs";
|
|
|
3
3
|
import { assertValidUserInfo, assertValidUserInfoSource } from "../utils/validate-user-info.mjs";
|
|
4
4
|
import { getStorageOption, processIdentifier } from "./verification-token-storage.mjs";
|
|
5
5
|
import { getWithHooks } from "./with-hooks.mjs";
|
|
6
|
-
import { getCurrentAdapter,
|
|
6
|
+
import { getCurrentAdapter, getCurrentAuthEndpointContext, queueAfterTransactionHook, runWithTransaction, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
7
7
|
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
8
8
|
import { APIError, BetterAuthError } from "@better-auth/core/error";
|
|
9
9
|
import { generateId } from "@better-auth/core/utils/id";
|
|
@@ -153,7 +153,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
153
153
|
assertValidUserInfoSource(validationSource);
|
|
154
154
|
let endpointContext;
|
|
155
155
|
try {
|
|
156
|
-
endpointContext =
|
|
156
|
+
endpointContext = getCurrentAuthEndpointContext();
|
|
157
157
|
} catch (error) {
|
|
158
158
|
logger.error("Unable to run validateUserInfo: missing endpoint context", error);
|
|
159
159
|
throw new APIError("FORBIDDEN", {
|
|
@@ -247,7 +247,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
247
247
|
},
|
|
248
248
|
createSession: async (userId, dontRememberMe, override, overrideAll, storageOptions) => {
|
|
249
249
|
const headers = await (async () => {
|
|
250
|
-
const ctx =
|
|
250
|
+
const ctx = tryGetCurrentAuthEndpointContext();
|
|
251
251
|
return ctx?.headers || ctx?.request?.headers;
|
|
252
252
|
})();
|
|
253
253
|
const storeInDb = options.session?.storeSessionInDatabase;
|
package/dist/db/with-hooks.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { getCurrentAdapter,
|
|
1
|
+
import { getCurrentAdapter, queueAfterTransactionHook, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
2
2
|
import { ATTR_CONTEXT, ATTR_DB_COLLECTION_NAME, ATTR_HOOK_TYPE, withSpan } from "@better-auth/core/instrumentation";
|
|
3
3
|
//#region src/db/with-hooks.ts
|
|
4
4
|
function getWithHooks(adapter, ctx) {
|
|
5
5
|
const hooksEntries = ctx.hooks;
|
|
6
6
|
async function createWithHooks(data, model, customCreateFn) {
|
|
7
|
-
const context =
|
|
7
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
8
8
|
let actualData = data;
|
|
9
9
|
for (const { source, hooks } of hooksEntries) {
|
|
10
10
|
const toRun = hooks[model]?.create?.before;
|
|
@@ -41,7 +41,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
41
41
|
return created;
|
|
42
42
|
}
|
|
43
43
|
async function updateWithHooks(data, where, model, customUpdateFn) {
|
|
44
|
-
const context =
|
|
44
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
45
45
|
let actualData = data;
|
|
46
46
|
for (const { source, hooks } of hooksEntries) {
|
|
47
47
|
const toRun = hooks[model]?.update?.before;
|
|
@@ -77,7 +77,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
77
77
|
return updated;
|
|
78
78
|
}
|
|
79
79
|
async function updateManyWithHooks(data, where, model, customUpdateFn) {
|
|
80
|
-
const context =
|
|
80
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
81
81
|
let actualData = data;
|
|
82
82
|
for (const { source, hooks } of hooksEntries) {
|
|
83
83
|
const toRun = hooks[model]?.update?.before;
|
|
@@ -113,7 +113,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
113
113
|
return updated;
|
|
114
114
|
}
|
|
115
115
|
async function deleteWithHooks(where, model, customDeleteFn) {
|
|
116
|
-
const context =
|
|
116
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
117
117
|
let entityToDelete = null;
|
|
118
118
|
try {
|
|
119
119
|
entityToDelete = (await (await getCurrentAdapter(adapter)).findMany({
|
|
@@ -150,7 +150,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
150
150
|
return deleted;
|
|
151
151
|
}
|
|
152
152
|
async function deleteManyWithHooks(where, model, customDeleteFn) {
|
|
153
|
-
const context =
|
|
153
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
154
154
|
let entitiesToDelete = [];
|
|
155
155
|
try {
|
|
156
156
|
entitiesToDelete = await (await getCurrentAdapter(adapter)).findMany({
|
|
@@ -202,7 +202,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
202
202
|
* the helper resolves to `null` (no `consumeFn` call, no after hooks).
|
|
203
203
|
*/
|
|
204
204
|
async function consumeOneWithHooks(model, hookWhere, consumeFn, preSnapshot) {
|
|
205
|
-
const context =
|
|
205
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
206
206
|
const beforeHooks = hooksEntries.flatMap(({ source, hooks }) => {
|
|
207
207
|
const fn = hooks[model]?.delete?.before;
|
|
208
208
|
return fn ? [{
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProt
|
|
|
2
2
|
import { generateGenericState, parseGenericState } from "./state.mjs";
|
|
3
3
|
import { generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
|
|
4
4
|
import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
|
|
5
|
+
import "./utils/index.mjs";
|
|
5
6
|
import { APIError } from "./api/index.mjs";
|
|
6
7
|
import { betterAuth } from "./auth/full.mjs";
|
|
7
8
|
import { getCurrentAdapter } from "@better-auth/core/context";
|
package/dist/oauth2/errors.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
1
2
|
//#region src/oauth2/errors.ts
|
|
2
3
|
/**
|
|
3
4
|
* Error codes used in OAuth callback redirects (`?error=<code>`). These are
|
|
@@ -33,8 +34,8 @@ const HANDLING_DOCS_URL = "https://www.better-auth.com/docs/concepts/oauth#handl
|
|
|
33
34
|
function redirectOnError(ctx, errorURL, error, description) {
|
|
34
35
|
const params = new URLSearchParams({ error });
|
|
35
36
|
if (description) params.set("error_description", description);
|
|
36
|
-
const
|
|
37
|
-
throw ctx.redirect(
|
|
37
|
+
const redirectURL = appendQueryParams(errorURL, params);
|
|
38
|
+
throw ctx.redirect(redirectURL);
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* Build the logger message shown when an OAuth provider does not return an
|
|
@@ -42,7 +43,7 @@ function redirectOnError(ctx, errorURL, error, description) {
|
|
|
42
43
|
* the same workaround docs.
|
|
43
44
|
*/
|
|
44
45
|
function missingEmailLogMessage(providerId, options) {
|
|
45
|
-
return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or
|
|
46
|
+
return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or create a placeholder via \`mapProfileToUser\`. See ${HANDLING_DOCS_URL}`;
|
|
46
47
|
}
|
|
47
48
|
//#endregion
|
|
48
49
|
export { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage, redirectOnError };
|
package/dist/oauth2/state.mjs
CHANGED
|
@@ -55,7 +55,7 @@ async function parseState(c) {
|
|
|
55
55
|
let redirectErrorURL = errorURL;
|
|
56
56
|
if (error instanceof StateError) {
|
|
57
57
|
code = error.code === "state_security_mismatch" ? "state_mismatch" : error.code;
|
|
58
|
-
redirectErrorURL = error.errorURL
|
|
58
|
+
redirectErrorURL = error.errorURL || errorURL;
|
|
59
59
|
}
|
|
60
60
|
redirectOnError(c, redirectErrorURL, code);
|
|
61
61
|
}
|
package/dist/package.mjs
CHANGED
|
@@ -534,7 +534,7 @@ const banUser = (opts) => createAuthEndpoint("/admin/ban-user", {
|
|
|
534
534
|
const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
|
|
535
535
|
banned: true,
|
|
536
536
|
banReason: ctx.body.banReason || opts?.defaultBanReason || "No reason",
|
|
537
|
-
banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") :
|
|
537
|
+
banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") : null,
|
|
538
538
|
updatedAt: /* @__PURE__ */ new Date()
|
|
539
539
|
});
|
|
540
540
|
await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
|
|
@@ -10,6 +10,7 @@ import { schema } from "./schema.mjs";
|
|
|
10
10
|
import { generateId } from "@better-auth/core/utils/id";
|
|
11
11
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
12
12
|
import * as z from "zod";
|
|
13
|
+
import { createPlaceholderEmail } from "@better-auth/core/utils/email";
|
|
13
14
|
//#region src/plugins/anonymous/index.ts
|
|
14
15
|
/**
|
|
15
16
|
* Resolves the anonymous session being upgraded during an account-link callback.
|
|
@@ -51,7 +52,10 @@ async function getAnonUserEmail(options) {
|
|
|
51
52
|
}
|
|
52
53
|
const id = generateId();
|
|
53
54
|
if (options?.emailDomainName) return `temp-${id}@${options.emailDomainName}`;
|
|
54
|
-
return
|
|
55
|
+
return createPlaceholderEmail({
|
|
56
|
+
identifier: id,
|
|
57
|
+
namespace: "anonymous"
|
|
58
|
+
});
|
|
55
59
|
}
|
|
56
60
|
const anonymous = (options) => {
|
|
57
61
|
return {
|
|
@@ -18,9 +18,8 @@ interface UserWithAnonymous extends User {
|
|
|
18
18
|
}
|
|
19
19
|
interface AnonymousOptions {
|
|
20
20
|
/**
|
|
21
|
-
* Configure
|
|
22
|
-
*
|
|
23
|
-
* @default "baseURL"
|
|
21
|
+
* Configure a custom domain for anonymous user email addresses.
|
|
22
|
+
* @default "anonymous.placeholder.invalid"
|
|
24
23
|
*/
|
|
25
24
|
emailDomainName?: string | undefined;
|
|
26
25
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { decodeJwt } from "jose";
|
|
2
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
+
import { createPlaceholderEmail } from "@better-auth/core/utils/email";
|
|
3
4
|
//#region src/plugins/generic-oauth/providers/microsoft-entra-id.ts
|
|
4
5
|
function getMicrosoftProfileName(profile) {
|
|
5
6
|
return profile.name ?? (`${profile.given_name ?? profile.givenname ?? ""} ${profile.family_name ?? profile.familyname ?? ""}`.trim() || void 0);
|
|
@@ -46,25 +47,34 @@ function microsoftEntraId(options) {
|
|
|
46
47
|
} catch {
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
const oid = typeof tokenProfile.oid === "string" && tokenProfile.oid.trim().length > 0 ? tokenProfile.oid : void 0;
|
|
51
|
+
if (!oid) return null;
|
|
52
|
+
const tokenEmail = tokenProfile.email;
|
|
50
53
|
const tokenUserInfo = {
|
|
51
54
|
...tokenProfile,
|
|
52
55
|
name: getMicrosoftProfileName(tokenProfile),
|
|
53
|
-
email:
|
|
56
|
+
email: tokenEmail ?? createPlaceholderEmail({
|
|
57
|
+
identifier: oid,
|
|
58
|
+
namespace: "microsoft-entra-id"
|
|
59
|
+
}),
|
|
54
60
|
image: tokenProfile.picture,
|
|
55
|
-
emailVerified: tokenProfile.email_verified ?? false
|
|
61
|
+
emailVerified: tokenEmail ? tokenProfile.email_verified ?? false : false
|
|
56
62
|
};
|
|
57
63
|
if (!tokens.accessToken) return tokenUserInfo;
|
|
58
64
|
const { data: profile, error } = await betterFetch(userInfoUrl, { headers: { Authorization: `Bearer ${tokens.accessToken}` } });
|
|
59
65
|
if (error || !profile) return tokenUserInfo;
|
|
60
66
|
if (typeof tokenProfile.sub !== "string" || profile.sub !== tokenProfile.sub) return tokenUserInfo;
|
|
67
|
+
const emailClaim = tokenProfile.email ?? profile.email;
|
|
61
68
|
return {
|
|
62
69
|
...profile,
|
|
63
70
|
...tokenProfile,
|
|
64
71
|
name: getMicrosoftProfileName(tokenProfile) ?? getMicrosoftProfileName(profile),
|
|
65
|
-
email:
|
|
72
|
+
email: emailClaim ?? createPlaceholderEmail({
|
|
73
|
+
identifier: oid,
|
|
74
|
+
namespace: "microsoft-entra-id"
|
|
75
|
+
}),
|
|
66
76
|
image: tokenProfile.picture ?? profile.picture,
|
|
67
|
-
emailVerified: tokenProfile.email_verified ?? profile.email_verified ?? false
|
|
77
|
+
emailVerified: emailClaim != null ? tokenProfile.email_verified ?? profile.email_verified ?? false : false
|
|
68
78
|
};
|
|
69
79
|
};
|
|
70
80
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
2
2
|
import { APIError } from "../../api/index.mjs";
|
|
3
3
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
5
5
|
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
|
|
6
6
|
import { createHash } from "@better-auth/utils/hash";
|
|
7
7
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -46,7 +46,7 @@ const haveIBeenPwned = (options) => {
|
|
|
46
46
|
...ctx.password,
|
|
47
47
|
async hash(password) {
|
|
48
48
|
if (options?.enabled === false) return originalHash(password);
|
|
49
|
-
const c =
|
|
49
|
+
const c = getCurrentAuthEndpointContext();
|
|
50
50
|
if (!c.path || !paths.includes(c.path)) return originalHash(password);
|
|
51
51
|
await checkPasswordCompromise(password, options?.customPasswordCompromisedMessage);
|
|
52
52
|
return originalHash(password);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getJwksAdapter } from "./adapter.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { getCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
3
3
|
import { base64 } from "@better-auth/utils/base64";
|
|
4
4
|
import { importJWK, jwtVerify } from "jose";
|
|
5
5
|
//#region src/plugins/jwt/verify.ts
|
|
@@ -8,7 +8,7 @@ import { importJWK, jwtVerify } from "jose";
|
|
|
8
8
|
* Returns the payload if valid, null otherwise
|
|
9
9
|
*/
|
|
10
10
|
async function verifyJWT(token, options) {
|
|
11
|
-
const ctx =
|
|
11
|
+
const ctx = getCurrentAuthEndpointContext();
|
|
12
12
|
try {
|
|
13
13
|
const parts = token.split(".");
|
|
14
14
|
if (parts.length !== 3) return null;
|
|
@@ -4,6 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
6
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
|
+
import "../../utils/index.mjs";
|
|
7
8
|
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
|
|
8
9
|
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
9
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
2
|
-
import { getOrigin } from "../../utils/url.mjs";
|
|
3
2
|
import { mergeSchema } from "../../db/schema.mjs";
|
|
4
3
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
4
|
import { APIError } from "../../api/index.mjs";
|
|
@@ -10,6 +9,7 @@ import { schema } from "./schema.mjs";
|
|
|
10
9
|
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
11
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
12
11
|
import * as z from "zod";
|
|
12
|
+
import { createPlaceholderEmail } from "@better-auth/core/utils/email";
|
|
13
13
|
//#region src/plugins/siwe/index.ts
|
|
14
14
|
const signedWalletAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/).length(42);
|
|
15
15
|
const SIWE_VERIFICATION_IDENTIFIER_PREFIX = "siwe:";
|
|
@@ -162,9 +162,11 @@ const siwe = (options) => {
|
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
if (!user) {
|
|
165
|
-
const domain = options.emailDomainName ?? getOrigin(ctx.context.baseURL);
|
|
166
165
|
const normalizedEmail = email?.toLowerCase();
|
|
167
|
-
const walletEmail = `${walletAddress}@${
|
|
166
|
+
const walletEmail = options.emailDomainName ? `${walletAddress}@${options.emailDomainName}` : createPlaceholderEmail({
|
|
167
|
+
identifier: walletAddress,
|
|
168
|
+
namespace: "siwe"
|
|
169
|
+
});
|
|
168
170
|
let userEmail = walletEmail;
|
|
169
171
|
let emailClaimIdentifier;
|
|
170
172
|
if (!isAnon && normalizedEmail) {
|
|
@@ -61,7 +61,7 @@ declare function getHttpTestInstance<O extends Partial<BetterAuthOptions>, C ext
|
|
|
61
61
|
port: number;
|
|
62
62
|
auth: Auth<O>;
|
|
63
63
|
client: {
|
|
64
|
-
hydrateSession
|
|
64
|
+
hydrateSession(session: null): void;
|
|
65
65
|
useSession: _$nanostores.Atom<{
|
|
66
66
|
data: never;
|
|
67
67
|
error: _$_better_fetch_fetch0.BetterFetchError | null;
|
|
@@ -7,7 +7,7 @@ import { createAuthClient } from "../client/vanilla.mjs";
|
|
|
7
7
|
import { bearer } from "../plugins/bearer/index.mjs";
|
|
8
8
|
import { sql } from "kysely";
|
|
9
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
11
|
import { afterAll } from "vitest";
|
|
12
12
|
//#region src/test-utils/test-instance.ts
|
|
13
13
|
const cleanupSet = /* @__PURE__ */ new Set();
|
|
@@ -18,6 +18,17 @@ afterAll(async () => {
|
|
|
18
18
|
cleanupSet.delete(cleanup);
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
|
+
const TEST_PASSWORD_HASH_PREFIX = "$test$sha256$";
|
|
22
|
+
/**
|
|
23
|
+
* Intentionally fast for tests
|
|
24
|
+
*/
|
|
25
|
+
function createTestPasswordHash(password) {
|
|
26
|
+
return `${TEST_PASSWORD_HASH_PREFIX}${createHash("sha256").update(password.normalize("NFKC")).digest("hex")}`;
|
|
27
|
+
}
|
|
28
|
+
const testPassword = {
|
|
29
|
+
hash: async (password) => createTestPasswordHash(password),
|
|
30
|
+
verify: async ({ hash, password }) => hash === createTestPasswordHash(password)
|
|
31
|
+
};
|
|
21
32
|
async function getTestInstance(options, config) {
|
|
22
33
|
const testWith = config?.testWith || "sqlite";
|
|
23
34
|
const postgresSchema = testWith === "postgres" ? `ba_test_${randomUUID().replaceAll("-", "_")}` : void 0;
|
|
@@ -83,6 +94,11 @@ async function getTestInstance(options, config) {
|
|
|
83
94
|
baseURL: "http://localhost:" + (config?.port || 3e3),
|
|
84
95
|
...opts,
|
|
85
96
|
...options,
|
|
97
|
+
emailAndPassword: {
|
|
98
|
+
...opts.emailAndPassword,
|
|
99
|
+
...options?.emailAndPassword,
|
|
100
|
+
password: options?.emailAndPassword?.password ?? testPassword
|
|
101
|
+
},
|
|
86
102
|
plugins: [bearer(), ...options?.plugins || []]
|
|
87
103
|
});
|
|
88
104
|
const testUser = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -473,13 +473,13 @@
|
|
|
473
473
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
474
474
|
"nanostores": "^1.3.0",
|
|
475
475
|
"zod": "^4.3.6",
|
|
476
|
-
"@better-auth/core": "1.7.
|
|
477
|
-
"@better-auth/drizzle-adapter": "1.7.
|
|
478
|
-
"@better-auth/kysely-adapter": "1.7.
|
|
479
|
-
"@better-auth/memory-adapter": "1.7.
|
|
480
|
-
"@better-auth/mongo-adapter": "1.7.
|
|
481
|
-
"@better-auth/prisma-adapter": "1.7.
|
|
482
|
-
"@better-auth/telemetry": "1.7.
|
|
476
|
+
"@better-auth/core": "1.7.2",
|
|
477
|
+
"@better-auth/drizzle-adapter": "1.7.2",
|
|
478
|
+
"@better-auth/kysely-adapter": "1.7.2",
|
|
479
|
+
"@better-auth/memory-adapter": "1.7.2",
|
|
480
|
+
"@better-auth/mongo-adapter": "1.7.2",
|
|
481
|
+
"@better-auth/prisma-adapter": "1.7.2",
|
|
482
|
+
"@better-auth/telemetry": "1.7.2"
|
|
483
483
|
},
|
|
484
484
|
"devDependencies": {
|
|
485
485
|
"@lynx-js/react": "^0.121.2",
|
|
@@ -504,7 +504,7 @@
|
|
|
504
504
|
"tsdown": "0.21.10",
|
|
505
505
|
"type-fest": "^5.7.0",
|
|
506
506
|
"typescript": "^6.0.3",
|
|
507
|
-
"vitest": "^4.
|
|
507
|
+
"vitest": "^4.1.10",
|
|
508
508
|
"vue": "^3.5.29"
|
|
509
509
|
},
|
|
510
510
|
"peerDependencies": {
|