better-auth 1.7.7 → 1.7.9
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.mjs +6 -2
- package/dist/api/routes/password.mjs +5 -11
- package/dist/api/routes/sign-in.mjs +0 -1
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +10 -5
- package/dist/cookies/index.d.mts +5 -5
- package/dist/db/get-migration.mjs +13 -11
- package/dist/db/get-schema.mjs +27 -11
- package/dist/db/internal-adapter.mjs +73 -56
- package/dist/db/to-zod.d.mts +8 -6
- package/dist/db/to-zod.mjs +12 -2
- package/dist/index.mjs +0 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/custom-session/index.mjs +6 -13
- package/dist/plugins/email-otp/routes.mjs +19 -10
- package/dist/plugins/generic-oauth/index.d.mts +13 -0
- package/dist/plugins/generic-oauth/index.mjs +232 -189
- package/dist/plugins/last-login-method/index.mjs +3 -3
- package/dist/plugins/magic-link/index.mjs +6 -12
- package/dist/plugins/open-api/index.mjs +0 -1
- package/dist/plugins/organization/routes/crud-members.d.mts +1 -42
- package/dist/plugins/organization/routes/crud-members.mjs +1 -1
- package/dist/plugins/phone-number/routes.mjs +33 -16
- package/dist/utils/password.mjs +33 -1
- package/package.json +13 -13
- package/dist/utils/index.mjs +0 -5
package/dist/api/index.mjs
CHANGED
|
@@ -192,11 +192,15 @@ const router = (ctx, options) => {
|
|
|
192
192
|
}
|
|
193
193
|
return res;
|
|
194
194
|
},
|
|
195
|
-
onError(e) {
|
|
195
|
+
async onError(e) {
|
|
196
196
|
if (isAPIError(e) && e.status === "FOUND") return;
|
|
197
197
|
if (options.onAPIError?.throw) throw e;
|
|
198
198
|
if (options.onAPIError?.onError) {
|
|
199
|
-
|
|
199
|
+
try {
|
|
200
|
+
await options.onAPIError.onError(e, ctx);
|
|
201
|
+
} catch (hookError) {
|
|
202
|
+
ctx.logger.error("onAPIError.onError threw", hookError);
|
|
203
|
+
}
|
|
200
204
|
return;
|
|
201
205
|
}
|
|
202
206
|
const optLogLevel = options.logger?.level;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { originCheck } from "../middlewares/origin-check.mjs";
|
|
2
2
|
import { getDate } from "../../utils/date.mjs";
|
|
3
3
|
import { sensitiveSessionMiddleware } from "./session.mjs";
|
|
4
|
-
import { assertPasswordNotTooLong, assertPasswordNotTooShort, validatePassword } from "../../utils/password.mjs";
|
|
4
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort, resetCredentialPassword, validatePassword } from "../../utils/password.mjs";
|
|
5
5
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
6
6
|
import { generateId } from "@better-auth/core/utils/id";
|
|
7
7
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -152,21 +152,15 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
152
152
|
assertPasswordNotTooShort(ctx, newPassword);
|
|
153
153
|
assertPasswordNotTooLong(ctx, newPassword);
|
|
154
154
|
const id = `reset-password:${token}`;
|
|
155
|
-
const verification = await ctx.context.internalAdapter.
|
|
156
|
-
if (!verification) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
155
|
+
const verification = await ctx.context.internalAdapter.findVerificationValue(id);
|
|
156
|
+
if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
157
157
|
const userId = verification.value;
|
|
158
158
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
159
159
|
if (!user) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
userId,
|
|
163
|
-
providerId: "credential",
|
|
164
|
-
accountId: user.id,
|
|
165
|
-
password: hashedPassword
|
|
160
|
+
await resetCredentialPassword(ctx, userId, await ctx.context.password.hash(newPassword), async () => {
|
|
161
|
+
if (!await ctx.context.internalAdapter.consumeVerificationValue(id)) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
166
162
|
});
|
|
167
|
-
else await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
168
163
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
169
|
-
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
170
164
|
return ctx.json({ status: true });
|
|
171
165
|
});
|
|
172
166
|
const verifyPassword = createAuthEndpoint("/verify-password", {
|
|
@@ -10,7 +10,6 @@ import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
|
|
|
10
10
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
11
11
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
12
12
|
import { assertPasswordNotTooLong } from "../../utils/password.mjs";
|
|
13
|
-
import "../../utils/index.mjs";
|
|
14
13
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
15
14
|
import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
16
15
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -252,7 +252,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
252
252
|
token: null,
|
|
253
253
|
user: parseUserOutput(ctx.context.options, createdUser)
|
|
254
254
|
});
|
|
255
|
-
const session = await ctx.context.internalAdapter.createSession(createdUser.id, rememberMe === false);
|
|
255
|
+
const session = await ctx.context.internalAdapter.createSession(createdUser.id, rememberMe === false, void 0, void 0, { deferSecondaryStorageWrites: true });
|
|
256
256
|
if (!session) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
257
257
|
await setSessionCookie(ctx, {
|
|
258
258
|
session,
|
|
@@ -5,6 +5,7 @@ import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
|
5
5
|
import { getSessionFromCtx, isStateful, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
6
6
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
7
7
|
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
8
|
+
import { runWithTransaction } from "@better-auth/core/context";
|
|
8
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
9
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
10
11
|
import * as z from "zod";
|
|
@@ -169,12 +170,16 @@ const changePassword = createAuthEndpoint("/change-password", {
|
|
|
169
170
|
hash: account.password,
|
|
170
171
|
password: currentPassword
|
|
171
172
|
})) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
|
|
172
|
-
await ctx.context.
|
|
173
|
+
const newSession = await runWithTransaction(ctx.context.adapter, async () => {
|
|
174
|
+
if (revokeOtherSessions) await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
175
|
+
await ctx.context.internalAdapter.updateAccount(account.id, { password: passwordHash });
|
|
176
|
+
if (!revokeOtherSessions) return null;
|
|
177
|
+
const created = await ctx.context.internalAdapter.createSession(session.user.id, void 0, void 0, void 0, { deferSecondaryStorageWrites: true });
|
|
178
|
+
if (!created) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
|
|
179
|
+
return created;
|
|
180
|
+
});
|
|
173
181
|
let token = null;
|
|
174
|
-
if (
|
|
175
|
-
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
176
|
-
const newSession = await ctx.context.internalAdapter.createSession(session.user.id);
|
|
177
|
-
if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
|
|
182
|
+
if (newSession) {
|
|
178
183
|
await setSessionCookie(ctx, {
|
|
179
184
|
session: newSession,
|
|
180
185
|
user: session.user
|
package/dist/cookies/index.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ import { JSONWebKeySet } from "jose";
|
|
|
10
10
|
declare function createCookieGetter(options: BetterAuthOptions): (cookieName: string, overrideAttributes?: Partial<CookieOptions>) => {
|
|
11
11
|
name: string;
|
|
12
12
|
attributes: {
|
|
13
|
-
domain?: string;
|
|
13
|
+
domain?: string | undefined;
|
|
14
14
|
expires?: Date;
|
|
15
15
|
httpOnly: boolean;
|
|
16
16
|
maxAge?: number;
|
|
@@ -25,7 +25,7 @@ declare function getCookies(options: BetterAuthOptions): {
|
|
|
25
25
|
sessionToken: {
|
|
26
26
|
name: string;
|
|
27
27
|
attributes: {
|
|
28
|
-
domain?: string;
|
|
28
|
+
domain?: string | undefined;
|
|
29
29
|
expires?: Date;
|
|
30
30
|
httpOnly: boolean;
|
|
31
31
|
maxAge?: number;
|
|
@@ -43,7 +43,7 @@ declare function getCookies(options: BetterAuthOptions): {
|
|
|
43
43
|
sessionData: {
|
|
44
44
|
name: string;
|
|
45
45
|
attributes: {
|
|
46
|
-
domain?: string;
|
|
46
|
+
domain?: string | undefined;
|
|
47
47
|
expires?: Date;
|
|
48
48
|
httpOnly: boolean;
|
|
49
49
|
maxAge?: number;
|
|
@@ -57,7 +57,7 @@ declare function getCookies(options: BetterAuthOptions): {
|
|
|
57
57
|
dontRememberToken: {
|
|
58
58
|
name: string;
|
|
59
59
|
attributes: {
|
|
60
|
-
domain?: string;
|
|
60
|
+
domain?: string | undefined;
|
|
61
61
|
expires?: Date;
|
|
62
62
|
httpOnly: boolean;
|
|
63
63
|
maxAge?: number;
|
|
@@ -71,7 +71,7 @@ declare function getCookies(options: BetterAuthOptions): {
|
|
|
71
71
|
accountData: {
|
|
72
72
|
name: string;
|
|
73
73
|
attributes: {
|
|
74
|
-
domain?: string;
|
|
74
|
+
domain?: string | undefined;
|
|
75
75
|
expires?: Date;
|
|
76
76
|
httpOnly: boolean;
|
|
77
77
|
maxAge?: number;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getAuthTables } from "@better-auth/core/db";
|
|
1
|
+
import { buildSchema } from "./get-schema.mjs";
|
|
3
2
|
import { createLogger } from "@better-auth/core/env";
|
|
4
3
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
4
|
import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
|
|
@@ -342,8 +341,7 @@ function matchType(columnDataType, fieldType, dbType) {
|
|
|
342
341
|
* existing or already-planned index.
|
|
343
342
|
*/
|
|
344
343
|
async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
345
|
-
const betterAuthSchema =
|
|
346
|
-
const authTables = getAuthTables(config);
|
|
344
|
+
const { schema: betterAuthSchema, tables: authTables, referenceKeys } = buildSchema(config);
|
|
347
345
|
const logger = createLogger(config.logger);
|
|
348
346
|
const unsafeChanges = [];
|
|
349
347
|
const reportUnsafeChange = (message) => {
|
|
@@ -562,14 +560,18 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
562
560
|
schema: authTables,
|
|
563
561
|
usePlural: false
|
|
564
562
|
});
|
|
565
|
-
function getReferencePath(
|
|
563
|
+
function getReferencePath(field) {
|
|
564
|
+
const reference = field.references;
|
|
565
|
+
if (!reference) throw new BetterAuthError("Cannot resolve a field without references.");
|
|
566
|
+
const keys = referenceKeys.get(field);
|
|
567
|
+
if (!keys) throw new BetterAuthError(`Missing migration reference metadata for "${reference.model}.${reference.field}".`);
|
|
566
568
|
try {
|
|
567
|
-
return `${getModelName(
|
|
568
|
-
model,
|
|
569
|
-
field
|
|
569
|
+
return `${getModelName(keys.modelKey)}.${getFieldName({
|
|
570
|
+
model: keys.modelKey,
|
|
571
|
+
field: keys.fieldKey
|
|
570
572
|
})}`;
|
|
571
573
|
} catch {
|
|
572
|
-
return `${model}.${field}`;
|
|
574
|
+
return `${reference.model}.${reference.field}`;
|
|
573
575
|
}
|
|
574
576
|
}
|
|
575
577
|
const deferredIndexes = [];
|
|
@@ -614,7 +616,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
614
616
|
}
|
|
615
617
|
const built = builder.addColumn(fieldName, type, (col) => {
|
|
616
618
|
col = field.required !== false ? col.notNull() : col;
|
|
617
|
-
if (field.references) col = col.references(getReferencePath(field
|
|
619
|
+
if (field.references) col = col.references(getReferencePath(field)).onDelete(field.references.onDelete || "cascade");
|
|
618
620
|
if (timestampDefault) if (dbType === "mysql") col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`);
|
|
619
621
|
else col = col.defaultTo(sql`CURRENT_TIMESTAMP`);
|
|
620
622
|
else if (staticDefault) col = col.defaultTo(typeof field.defaultValue === "boolean" && (dbType === "sqlite" || dbType === "mssql") ? field.defaultValue ? 1 : 0 : field.defaultValue);
|
|
@@ -642,7 +644,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
642
644
|
const type = getType(field, fieldName, getTableIndexStringLength(table.table, fieldName));
|
|
643
645
|
dbT = dbT.addColumn(fieldName, type, (col) => {
|
|
644
646
|
col = field.required !== false ? col.notNull() : col;
|
|
645
|
-
if (field.references) col = col.references(getReferencePath(field
|
|
647
|
+
if (field.references) col = col.references(getReferencePath(field)).onDelete(field.references.onDelete || "cascade");
|
|
646
648
|
if (field.unique) col = col.unique();
|
|
647
649
|
if (field.type === "date" && typeof field.defaultValue === "function" && (dbType === "postgres" || dbType === "mysql" || dbType === "mssql")) if (dbType === "mysql") col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`);
|
|
648
650
|
else col = col.defaultTo(sql`CURRENT_TIMESTAMP`);
|
package/dist/db/get-schema.mjs
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import { getAuthTablesWithResolvedIndexes } from "@better-auth/core/db/internal";
|
|
2
2
|
//#region src/db/get-schema.ts
|
|
3
3
|
function getSchema(config) {
|
|
4
|
+
return buildSchema(config).schema;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
function buildSchema(config) {
|
|
4
10
|
const { indexesByTable, tables } = getAuthTablesWithResolvedIndexes(config);
|
|
11
|
+
const referenceKeys = /* @__PURE__ */ new Map();
|
|
5
12
|
const schema = {};
|
|
6
13
|
for (const key in tables) {
|
|
7
14
|
const table = tables[key];
|
|
8
15
|
const fields = table.fields;
|
|
9
16
|
const actualFields = {};
|
|
10
17
|
Object.entries(fields).forEach(([key, field]) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
}
|
|
18
|
+
const reference = field.references;
|
|
19
|
+
const refTable = reference ? tables[reference.model] : void 0;
|
|
20
|
+
const actualField = reference && refTable ? {
|
|
21
|
+
...field,
|
|
22
|
+
references: {
|
|
23
|
+
...reference,
|
|
24
|
+
model: refTable.modelName
|
|
25
|
+
}
|
|
26
|
+
} : field;
|
|
27
|
+
actualFields[field.fieldName || key] = actualField;
|
|
28
|
+
if (reference) referenceKeys.set(actualField, {
|
|
29
|
+
modelKey: reference.model,
|
|
30
|
+
fieldKey: reference.field
|
|
31
|
+
});
|
|
20
32
|
});
|
|
21
33
|
if (schema[table.modelName]) {
|
|
22
34
|
schema[table.modelName].fields = {
|
|
@@ -33,7 +45,11 @@ function getSchema(config) {
|
|
|
33
45
|
};
|
|
34
46
|
}
|
|
35
47
|
for (const [tableName, indexes] of indexesByTable) if (schema[tableName]) schema[tableName].indexes = indexes;
|
|
36
|
-
return
|
|
48
|
+
return {
|
|
49
|
+
schema,
|
|
50
|
+
tables,
|
|
51
|
+
referenceKeys
|
|
52
|
+
};
|
|
37
53
|
}
|
|
38
54
|
//#endregion
|
|
39
|
-
export { getSchema };
|
|
55
|
+
export { buildSchema, getSchema };
|
|
@@ -7,8 +7,8 @@ import { getCurrentAdapter, getCurrentAuthEndpointContext, queueAfterTransaction
|
|
|
7
7
|
import { APIError, BetterAuthError } from "@better-auth/core/error";
|
|
8
8
|
import { generateId } from "@better-auth/core/utils/id";
|
|
9
9
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
10
|
+
import { encodeDeterministicId } from "@better-auth/core/db/internal";
|
|
10
11
|
import { getIP } from "@better-auth/core/utils/ip";
|
|
11
|
-
import { base64Url } from "@better-auth/utils/base64";
|
|
12
12
|
import { createHash } from "@better-auth/utils/hash";
|
|
13
13
|
//#region src/db/internal-adapter.ts
|
|
14
14
|
function getTTLSeconds(expiresAt, now = Date.now()) {
|
|
@@ -21,6 +21,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
21
21
|
const secondaryStorage = options.secondaryStorage;
|
|
22
22
|
const verificationConsumeLocks = /* @__PURE__ */ new Map();
|
|
23
23
|
const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7;
|
|
24
|
+
const databaseSessionFallbackEnabled = !!secondaryStorage && options.session?.storeSessionInDatabase === true && options.session?.preserveSessionInDatabase !== true;
|
|
24
25
|
const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx);
|
|
25
26
|
/**
|
|
26
27
|
* Ends the live session rows matched by `where` without physically deleting
|
|
@@ -175,6 +176,17 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
175
176
|
}, "account", void 0);
|
|
176
177
|
},
|
|
177
178
|
listSessions: async (userId, options) => {
|
|
179
|
+
if (databaseSessionFallbackEnabled) return (await (await getCurrentAdapter(adapter)).findMany({
|
|
180
|
+
model: "session",
|
|
181
|
+
where: [{
|
|
182
|
+
field: "userId",
|
|
183
|
+
value: userId
|
|
184
|
+
}, {
|
|
185
|
+
field: "expiresAt",
|
|
186
|
+
value: /* @__PURE__ */ new Date(),
|
|
187
|
+
operator: "gt"
|
|
188
|
+
}]
|
|
189
|
+
})).map((session) => parseSessionOutput(ctx.options, session));
|
|
178
190
|
if (secondaryStorage) {
|
|
179
191
|
const currentList = await secondaryStorage.get(`active-sessions-${userId}`);
|
|
180
192
|
if (!currentList) return [];
|
|
@@ -250,7 +262,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
250
262
|
return ctx?.headers || ctx?.request?.headers;
|
|
251
263
|
})();
|
|
252
264
|
const storeInDb = options.session?.storeSessionInDatabase;
|
|
253
|
-
const
|
|
265
|
+
const deferMirror = storageOptions?.deferSecondaryStorageWrites === true && databaseSessionFallbackEnabled;
|
|
254
266
|
const { id: _, ...rest } = override || {};
|
|
255
267
|
let sessionId;
|
|
256
268
|
if (secondaryStorage && !storeInDb) {
|
|
@@ -305,18 +317,18 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
305
317
|
}), sessionTTL);
|
|
306
318
|
return sessionData;
|
|
307
319
|
};
|
|
308
|
-
|
|
320
|
+
return await createWithHooks(data, "session", secondaryStorage ? {
|
|
309
321
|
fn: async (sessionData) => {
|
|
310
|
-
|
|
322
|
+
if (!deferMirror) return mirrorSessionToSecondaryStorage(sessionData);
|
|
323
|
+
await queueAfterTransactionHook(async () => {
|
|
324
|
+
await mirrorSessionToSecondaryStorage(sessionData);
|
|
325
|
+
}, { onError(error) {
|
|
326
|
+
logger.error("Failed to mirror committed session to secondary storage", error);
|
|
327
|
+
} });
|
|
328
|
+
return sessionData;
|
|
311
329
|
},
|
|
312
330
|
executeMainFn: storeInDb
|
|
313
331
|
} : void 0);
|
|
314
|
-
if (secondaryStorage && storageOptions?.deferSecondaryStorageWrites && res) await queueAfterTransactionHook(async () => {
|
|
315
|
-
await mirrorSessionToSecondaryStorage(res);
|
|
316
|
-
}, databaseSessionFallbackEnabled ? { onError(error) {
|
|
317
|
-
logger.error("Failed to mirror committed session to secondary storage", error);
|
|
318
|
-
} } : void 0);
|
|
319
|
-
return res;
|
|
320
332
|
},
|
|
321
333
|
findSession: async (token) => {
|
|
322
334
|
if (secondaryStorage) {
|
|
@@ -357,54 +369,57 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
357
369
|
};
|
|
358
370
|
},
|
|
359
371
|
findSessions: async (sessionTokens, options) => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
372
|
+
const uniqueTokens = [...new Set(sessionTokens)];
|
|
373
|
+
const sessionsByToken = /* @__PURE__ */ new Map();
|
|
374
|
+
const missingTokens = [];
|
|
375
|
+
if (secondaryStorage) for (const sessionToken of uniqueTokens) {
|
|
376
|
+
const sessionStringified = await secondaryStorage.get(sessionToken);
|
|
377
|
+
if (sessionStringified) try {
|
|
378
|
+
const s = typeof sessionStringified === "string" ? JSON.parse(sessionStringified) : sessionStringified;
|
|
379
|
+
if (!s) continue;
|
|
380
|
+
const expiresAt = new Date(s.session.expiresAt);
|
|
381
|
+
if (options?.onlyActiveSessions && expiresAt <= /* @__PURE__ */ new Date()) continue;
|
|
382
|
+
const session = {
|
|
383
|
+
session: {
|
|
384
|
+
...s.session,
|
|
385
|
+
expiresAt: new Date(s.session.expiresAt)
|
|
386
|
+
},
|
|
387
|
+
user: {
|
|
388
|
+
...s.user,
|
|
389
|
+
createdAt: new Date(s.user.createdAt),
|
|
390
|
+
updatedAt: new Date(s.user.updatedAt)
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
sessionsByToken.set(sessionToken, session);
|
|
394
|
+
} catch {
|
|
395
|
+
continue;
|
|
384
396
|
}
|
|
385
|
-
|
|
397
|
+
else missingTokens.push(sessionToken);
|
|
386
398
|
}
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const { user, ...session }
|
|
404
|
-
return {
|
|
399
|
+
const databaseTokens = secondaryStorage ? missingTokens : uniqueTokens;
|
|
400
|
+
if (databaseTokens.length > 0 && (!secondaryStorage || databaseSessionFallbackEnabled)) {
|
|
401
|
+
const databaseSessions = await (await getCurrentAdapter(adapter)).findMany({
|
|
402
|
+
model: "session",
|
|
403
|
+
limit: databaseTokens.length,
|
|
404
|
+
where: [{
|
|
405
|
+
field: "token",
|
|
406
|
+
value: databaseTokens,
|
|
407
|
+
operator: "in"
|
|
408
|
+
}, ...options?.onlyActiveSessions ? [{
|
|
409
|
+
field: "expiresAt",
|
|
410
|
+
value: /* @__PURE__ */ new Date(),
|
|
411
|
+
operator: "gt"
|
|
412
|
+
}] : []],
|
|
413
|
+
join: { user: true }
|
|
414
|
+
});
|
|
415
|
+
for (const { user, ...session } of databaseSessions) if (user) sessionsByToken.set(session.token, {
|
|
405
416
|
session,
|
|
406
417
|
user
|
|
407
|
-
};
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return uniqueTokens.flatMap((token) => {
|
|
421
|
+
const session = sessionsByToken.get(token);
|
|
422
|
+
return session ? [session] : [];
|
|
408
423
|
});
|
|
409
424
|
},
|
|
410
425
|
updateSession: async (sessionToken, session) => {
|
|
@@ -868,7 +883,9 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
868
883
|
* already taken.
|
|
869
884
|
*
|
|
870
885
|
* The `verification.identifier` column is non-unique, so uniqueness comes
|
|
871
|
-
* from a deterministic primary key (`SHA-256` of `reserve:<identifier
|
|
886
|
+
* from a deterministic primary key (`SHA-256` of `reserve:<identifier>`,
|
|
887
|
+
* encoded by `encodeDeterministicId` so the configured id strategy keeps
|
|
888
|
+
* it; see its note on `generateId: "serial"`).
|
|
872
889
|
* The database path is atomic: the primary key turns the INSERT into the
|
|
873
890
|
* first-writer-wins gate, and a duplicate is detected portably by
|
|
874
891
|
* re-reading the row rather than matching adapter-specific errors.
|
|
@@ -882,7 +899,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
882
899
|
* is best-effort there (it is intended for development and tests).
|
|
883
900
|
*/
|
|
884
901
|
reserveVerificationValue: async (data) => {
|
|
885
|
-
const reservationId =
|
|
902
|
+
const reservationId = encodeDeterministicId(new Uint8Array(await createHash("SHA-256").digest(new TextEncoder().encode("reserve:" + data.identifier))), options);
|
|
886
903
|
const storageOption = getStorageOption(data.identifier, options.verification?.storeIdentifier);
|
|
887
904
|
const storedIdentifier = await processIdentifier(data.identifier, storageOption);
|
|
888
905
|
if (secondaryStorage && !options.verification?.storeInDatabase) throw new BetterAuthError("reserveVerificationValue requires database-backed verification storage. Set verification.storeInDatabase to true for flows that reserve verification values.");
|
package/dist/db/to-zod.d.mts
CHANGED
|
@@ -11,10 +11,10 @@ declare function toZodSchema<Fields extends Record<string, DBFieldAttribute | ne
|
|
|
11
11
|
* If true, then any fields that have `input: false` will be removed from the schema to prevent user input.
|
|
12
12
|
*/
|
|
13
13
|
isClientSide: IsClientSide;
|
|
14
|
-
}):
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
}): ZodSchemaForSide<Fields, IsClientSide>;
|
|
15
|
+
type ZodSchemaForSide<Fields extends Record<string, DBFieldAttribute | never>, IsClientSide extends boolean> = IsClientSide extends true ? z.ZodObject<SchemaShape<Fields, true>, z.core.$strip> : z.ZodObject<SchemaShape<Fields, false>, z.core.$strip>;
|
|
16
|
+
type SchemaShape<Fields extends Record<string, DBFieldAttribute | never>, IsClientSide extends boolean> = RemoveNeverProps<{ [key in keyof Fields]: FieldAttributeToSchema<Fields[key], IsClientSide> }>;
|
|
17
|
+
type FieldAttributeToSchema<Field extends DBFieldAttribute | Record<string, never>, isClientSide extends boolean = false> = Field extends DBFieldAttribute ? GetSchemaForSide<isClientSide, Field, GetRequired<Field, GetType<Field>>> : Record<string, never>;
|
|
18
18
|
type GetType<F extends DBFieldAttribute> = F extends {
|
|
19
19
|
type: "string";
|
|
20
20
|
} ? z.ZodString : F extends {
|
|
@@ -27,9 +27,11 @@ type GetType<F extends DBFieldAttribute> = F extends {
|
|
|
27
27
|
type GetRequired<F extends DBFieldAttribute, Schema extends z.core.SomeType> = F extends {
|
|
28
28
|
required: false;
|
|
29
29
|
} ? z.ZodOptional<z.ZodNullable<Schema>> : Schema;
|
|
30
|
-
type
|
|
30
|
+
type GetSchemaForSide<isClientSide extends boolean, Field extends DBFieldAttribute, Schema extends z.core.SomeType> = isClientSide extends true ? Field extends {
|
|
31
31
|
input: false;
|
|
32
|
-
} ?
|
|
32
|
+
} ? never : Schema : Field extends {
|
|
33
|
+
returned: false;
|
|
34
|
+
} ? never : Schema;
|
|
33
35
|
type RemoveNeverProps<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };
|
|
34
36
|
//#endregion
|
|
35
37
|
export { FieldAttributeToSchema, toZodSchema };
|
package/dist/db/to-zod.mjs
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
//#region src/db/to-zod.ts
|
|
3
|
+
/**
|
|
4
|
+
* Static lookup keeps `zod` tree-shakeable. `z[field.type]()` forces bundlers
|
|
5
|
+
* to retain every `zod` export, including all locales.
|
|
6
|
+
*/
|
|
7
|
+
const scalarSchemas = {
|
|
8
|
+
string: z.string,
|
|
9
|
+
number: z.number,
|
|
10
|
+
boolean: z.boolean,
|
|
11
|
+
date: z.date
|
|
12
|
+
};
|
|
3
13
|
function toZodSchema({ fields, isClientSide }) {
|
|
4
14
|
const zodFields = Object.keys(fields).reduce((acc, key) => {
|
|
5
15
|
const field = fields[key];
|
|
6
16
|
if (!field) return acc;
|
|
7
17
|
if (isClientSide && field.input === false) return acc;
|
|
8
18
|
let schema;
|
|
9
|
-
if (field.type === "json") schema = z.json
|
|
19
|
+
if (field.type === "json") schema = z.json();
|
|
10
20
|
else if (field.type === "string[]" || field.type === "number[]") schema = z.array(field.type === "string[]" ? z.string() : z.number());
|
|
11
21
|
else if (Array.isArray(field.type)) schema = z.any();
|
|
12
|
-
else schema =
|
|
22
|
+
else schema = scalarSchemas[field.type]();
|
|
13
23
|
if (field?.required === false) schema = schema.nullish();
|
|
14
24
|
if (!isClientSide && field?.returned === false) return acc;
|
|
15
25
|
return {
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,6 @@ 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";
|
|
6
5
|
import { APIError } from "./api/index.mjs";
|
|
7
6
|
import { betterAuth } from "./auth/full.mjs";
|
|
8
7
|
import { getCurrentAdapter } from "@better-auth/core/context";
|
package/dist/package.mjs
CHANGED
|
@@ -37,25 +37,18 @@ const customSession = (fn, options, pluginOptions) => {
|
|
|
37
37
|
},
|
|
38
38
|
requireHeaders: true
|
|
39
39
|
}, async (ctx) => {
|
|
40
|
-
const
|
|
40
|
+
const { headers, response } = await getSession()({
|
|
41
41
|
...ctx,
|
|
42
42
|
method: "GET",
|
|
43
43
|
asResponse: false,
|
|
44
44
|
headers: ctx.headers,
|
|
45
45
|
returnHeaders: true
|
|
46
|
-
}).catch((e) => {
|
|
47
|
-
return null;
|
|
48
46
|
});
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
session.headers.delete("set-cookie");
|
|
55
|
-
session.headers.forEach((value, key) => {
|
|
56
|
-
ctx.setHeader(key, value);
|
|
57
|
-
});
|
|
58
|
-
return ctx.json(fnResult);
|
|
47
|
+
const sessionPayload = response ? await fn(response, ctx) : null;
|
|
48
|
+
for (const cookie of headers.getSetCookie()) for (const [name, attributes] of parseSetCookieHeader(cookie)) ctx.setCookie(name, attributes.value, toCookieOptions(attributes));
|
|
49
|
+
headers.delete("set-cookie");
|
|
50
|
+
headers.forEach((value, name) => ctx.setHeader(name, value));
|
|
51
|
+
return ctx.json(sessionPayload);
|
|
59
52
|
}) },
|
|
60
53
|
$Infer: { Session: {} },
|
|
61
54
|
options: pluginOptions
|
|
@@ -6,7 +6,7 @@ import { symmetricDecrypt } from "../../crypto/index.mjs";
|
|
|
6
6
|
import { setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
|
|
7
7
|
import { revokeUnprovenAccountAccess } from "../../db/revoke-unproven-account-access.mjs";
|
|
8
8
|
import { getSessionFromCtx, sensitiveSessionMiddleware } from "../../api/routes/session.mjs";
|
|
9
|
-
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
9
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort, resetCredentialPassword } from "../../utils/password.mjs";
|
|
10
10
|
import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
11
11
|
import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
|
|
12
12
|
import { splitAtLastColon, toOTPIdentifier } from "./utils.mjs";
|
|
@@ -584,20 +584,18 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
|
|
|
584
584
|
const email = ctx.body.email.toLowerCase();
|
|
585
585
|
assertPasswordNotTooShort(ctx, ctx.body.password);
|
|
586
586
|
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
587
|
-
|
|
587
|
+
const identifier = toOTPIdentifier("forget-password", email);
|
|
588
|
+
let otpConsumed = false;
|
|
589
|
+
if (!await matchesUsableStoredOTP(ctx, opts, identifier, ctx.body.otp)) {
|
|
590
|
+
await atomicVerifyOTP(ctx, opts, identifier, ctx.body.otp);
|
|
591
|
+
otpConsumed = true;
|
|
592
|
+
}
|
|
588
593
|
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
589
594
|
if (!user) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
590
595
|
const passwordHash = await ctx.context.password.hash(ctx.body.password);
|
|
591
|
-
|
|
592
|
-
userId: user.user.id,
|
|
593
|
-
providerId: "credential",
|
|
594
|
-
accountId: user.user.id,
|
|
595
|
-
password: passwordHash
|
|
596
|
-
});
|
|
597
|
-
else await ctx.context.internalAdapter.updatePassword(user.user.id, passwordHash);
|
|
596
|
+
await resetCredentialPassword(ctx, user.user.id, passwordHash, otpConsumed ? void 0 : () => atomicVerifyOTP(ctx, opts, identifier, ctx.body.otp));
|
|
598
597
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user: user.user }, ctx.request);
|
|
599
598
|
if (!user.user.emailVerified) await ctx.context.internalAdapter.updateUser(user.user.id, { emailVerified: true });
|
|
600
|
-
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(user.user.id);
|
|
601
599
|
return ctx.json({ success: true });
|
|
602
600
|
});
|
|
603
601
|
const requestEmailChangeEmailOTPBodySchema = z.object({
|
|
@@ -748,6 +746,17 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
|
|
|
748
746
|
});
|
|
749
747
|
const defaultOTPGenerator = (options) => generateRandomString(options.otpLength ?? 6, "0-9");
|
|
750
748
|
/**
|
|
749
|
+
* Reports whether the provided OTP matches a stored, unexpired record with
|
|
750
|
+
* attempts left, without consuming it or counting an attempt.
|
|
751
|
+
*/
|
|
752
|
+
async function matchesUsableStoredOTP(ctx, opts, identifier, providedOTP) {
|
|
753
|
+
const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
754
|
+
if (!existing || existing.expiresAt < /* @__PURE__ */ new Date()) return false;
|
|
755
|
+
const [otpValue, attempts] = splitAtLastColon(existing.value);
|
|
756
|
+
if (parseInt(attempts || "0") >= (opts?.allowedAttempts || 3)) return false;
|
|
757
|
+
return verifyStoredOTP(ctx, opts, otpValue, providedOTP);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
751
760
|
* Verifies a single-use OTP with race-condition protection.
|
|
752
761
|
*
|
|
753
762
|
* The atomic consume is the single gate: only the first concurrent caller
|