better-auth 1.6.11 → 1.6.13
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 +12 -48
- package/dist/api/routes/account.d.mts +2 -23
- package/dist/api/routes/account.mjs +94 -73
- package/dist/api/routes/callback.d.mts +1 -1
- package/dist/api/routes/callback.mjs +39 -42
- package/dist/api/routes/email-verification.d.mts +1 -0
- package/dist/api/routes/email-verification.mjs +4 -3
- package/dist/api/routes/password.mjs +1 -1
- package/dist/api/routes/session.mjs +15 -10
- package/dist/api/routes/sign-in.d.mts +1 -0
- package/dist/api/routes/sign-in.mjs +3 -2
- package/dist/api/routes/sign-up.d.mts +1 -0
- package/dist/api/routes/sign-up.mjs +9 -7
- package/dist/api/routes/update-user.mjs +7 -7
- package/dist/client/fetch-plugins.mjs +2 -1
- package/dist/client/parser.mjs +0 -1
- package/dist/client/plugins/index.d.mts +3 -3
- package/dist/client/proxy.mjs +2 -1
- package/dist/context/create-context.mjs +10 -14
- package/dist/context/helpers.mjs +3 -2
- package/dist/cookies/cookie-utils.d.mts +24 -1
- package/dist/cookies/cookie-utils.mjs +85 -22
- package/dist/cookies/index.d.mts +2 -3
- package/dist/cookies/index.mjs +39 -11
- package/dist/cookies/session-store.mjs +4 -23
- package/dist/db/get-migration.mjs +4 -4
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +3 -2
- package/dist/db/internal-adapter.mjs +56 -50
- package/dist/db/schema.d.mts +15 -2
- package/dist/db/schema.mjs +26 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/errors.mjs +16 -1
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +3 -3
- package/dist/oauth2/link-account.d.mts +27 -1
- package/dist/oauth2/link-account.mjs +27 -4
- package/dist/oauth2/state.mjs +8 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +11 -6
- package/dist/plugins/admin/admin.mjs +0 -4
- package/dist/plugins/admin/client.d.mts +1 -1
- package/dist/plugins/admin/routes.mjs +3 -3
- package/dist/plugins/anonymous/index.mjs +2 -2
- package/dist/plugins/bearer/index.mjs +4 -9
- package/dist/plugins/captcha/index.mjs +2 -2
- package/dist/plugins/email-otp/routes.mjs +1 -1
- package/dist/plugins/generic-oauth/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.mjs +6 -6
- package/dist/plugins/generic-oauth/routes.mjs +37 -34
- package/dist/plugins/generic-oauth/types.d.mts +7 -0
- package/dist/plugins/last-login-method/client.mjs +2 -2
- package/dist/plugins/magic-link/index.mjs +0 -1
- package/dist/plugins/mcp/index.mjs +2 -5
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +45 -32
- package/dist/plugins/oauth-proxy/utils.mjs +3 -10
- package/dist/plugins/oidc-provider/index.mjs +2 -5
- package/dist/plugins/one-tap/client.mjs +9 -2
- package/dist/plugins/one-tap/index.mjs +16 -39
- package/dist/plugins/open-api/generator.mjs +16 -5
- package/dist/plugins/organization/adapter.mjs +61 -56
- package/dist/plugins/organization/client.d.mts +2 -1
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
- package/dist/plugins/organization/routes/crud-org.d.mts +4 -4
- package/dist/plugins/organization/routes/crud-org.mjs +2 -2
- package/dist/plugins/organization/types.d.mts +3 -3
- package/dist/plugins/phone-number/routes.mjs +1 -1
- package/dist/plugins/two-factor/backup-codes/index.d.mts +4 -3
- package/dist/plugins/two-factor/client.mjs +2 -1
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/plugins/username/index.d.mts +24 -2
- package/dist/plugins/username/index.mjs +49 -3
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +18 -4
- package/dist/test-utils/headers.mjs +2 -7
- package/dist/test-utils/test-instance.d.mts +36 -144
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +9 -3
- package/package.json +15 -14
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { db_exports } from "../../db/index.mjs";
|
|
2
2
|
import { getEndpoints } from "../../api/index.mjs";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { toPascalCase } from "@better-auth/core/utils/string";
|
|
4
5
|
//#region src/plugins/open-api/generator.ts
|
|
5
6
|
const allowedType = new Set([
|
|
6
7
|
"string",
|
|
@@ -156,7 +157,7 @@ function getResponse(responses) {
|
|
|
156
157
|
} } },
|
|
157
158
|
description: "Internal Server Error. This is a problem with the server that you cannot fix."
|
|
158
159
|
},
|
|
159
|
-
...responses
|
|
160
|
+
...responses ? structuredClone(responses) : {}
|
|
160
161
|
};
|
|
161
162
|
}
|
|
162
163
|
function toOpenApiPath(path) {
|
|
@@ -196,6 +197,16 @@ async function generator(ctx, options) {
|
|
|
196
197
|
return acc;
|
|
197
198
|
}, {}) } };
|
|
198
199
|
const paths = {};
|
|
200
|
+
const seenOperationIds = /* @__PURE__ */ new Set();
|
|
201
|
+
const uniqueOperationId = (operationId, method) => {
|
|
202
|
+
if (!operationId) return void 0;
|
|
203
|
+
const base = seenOperationIds.has(operationId) ? `${operationId}${toPascalCase(method)}` : operationId;
|
|
204
|
+
let result = base;
|
|
205
|
+
let n = 2;
|
|
206
|
+
while (seenOperationIds.has(result)) result = `${base}${n++}`;
|
|
207
|
+
seenOperationIds.add(result);
|
|
208
|
+
return result;
|
|
209
|
+
};
|
|
199
210
|
Object.entries(baseEndpoints.api).forEach(([_, value]) => {
|
|
200
211
|
if (!value.path || ctx.options.disabledPaths?.includes(value.path)) return;
|
|
201
212
|
const options = value.options;
|
|
@@ -207,7 +218,7 @@ async function generator(ctx, options) {
|
|
|
207
218
|
[method.toLowerCase()]: {
|
|
208
219
|
tags: ["Default", ...options.metadata?.openapi?.tags || []],
|
|
209
220
|
description: options.metadata?.openapi?.description,
|
|
210
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
221
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
211
222
|
security: [{ bearerAuth: [] }],
|
|
212
223
|
parameters: getParameters(options),
|
|
213
224
|
responses: getResponse(options.metadata?.openapi?.responses)
|
|
@@ -220,7 +231,7 @@ async function generator(ctx, options) {
|
|
|
220
231
|
[method.toLowerCase()]: {
|
|
221
232
|
tags: ["Default", ...options.metadata?.openapi?.tags || []],
|
|
222
233
|
description: options.metadata?.openapi?.description,
|
|
223
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
234
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
224
235
|
security: [{ bearerAuth: [] }],
|
|
225
236
|
parameters: getParameters(options),
|
|
226
237
|
...body ? { requestBody: body } : { requestBody: { content: { "application/json": { schema: {
|
|
@@ -253,7 +264,7 @@ async function generator(ctx, options) {
|
|
|
253
264
|
[method.toLowerCase()]: {
|
|
254
265
|
tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
|
|
255
266
|
description: options.metadata?.openapi?.description,
|
|
256
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
267
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
257
268
|
security: [{ bearerAuth: [] }],
|
|
258
269
|
parameters: getParameters(options),
|
|
259
270
|
responses: getResponse(options.metadata?.openapi?.responses)
|
|
@@ -264,7 +275,7 @@ async function generator(ctx, options) {
|
|
|
264
275
|
[method.toLowerCase()]: {
|
|
265
276
|
tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
|
|
266
277
|
description: options.metadata?.openapi?.description,
|
|
267
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
278
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
268
279
|
security: [{ bearerAuth: [] }],
|
|
269
280
|
parameters: getParameters(options),
|
|
270
281
|
requestBody: getRequestBody(options),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getDate } from "../../utils/date.mjs";
|
|
2
2
|
import { parseJSON } from "../../client/parser.mjs";
|
|
3
|
-
import { getCurrentAdapter } from "@better-auth/core/context";
|
|
3
|
+
import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
|
|
4
4
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
5
|
import { filterOutputFields } from "@better-auth/core/utils/db";
|
|
6
6
|
//#region src/plugins/organization/adapter.ts
|
|
@@ -184,46 +184,49 @@ const getOrgAdapter = (context, options) => {
|
|
|
184
184
|
});
|
|
185
185
|
},
|
|
186
186
|
deleteMember: async ({ memberId, organizationId, userId: _userId }) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
return runWithTransaction(baseAdapter, async () => {
|
|
188
|
+
const adapter = await getCurrentAdapter(baseAdapter);
|
|
189
|
+
let userId;
|
|
190
|
+
if (!_userId) {
|
|
191
|
+
const member = await adapter.findOne({
|
|
192
|
+
model: "member",
|
|
193
|
+
where: [{
|
|
194
|
+
field: "id",
|
|
195
|
+
value: memberId
|
|
196
|
+
}]
|
|
197
|
+
});
|
|
198
|
+
if (!member) throw new BetterAuthError("Member not found");
|
|
199
|
+
userId = member.userId;
|
|
200
|
+
} else userId = _userId;
|
|
201
|
+
const member = await adapter.delete({
|
|
191
202
|
model: "member",
|
|
192
203
|
where: [{
|
|
193
204
|
field: "id",
|
|
194
205
|
value: memberId
|
|
195
206
|
}]
|
|
196
207
|
});
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
if (options?.teams?.enabled) {
|
|
209
|
+
const teams = await adapter.findMany({
|
|
210
|
+
model: "team",
|
|
211
|
+
where: [{
|
|
212
|
+
field: "organizationId",
|
|
213
|
+
value: organizationId
|
|
214
|
+
}]
|
|
215
|
+
});
|
|
216
|
+
if (teams.length > 0) await adapter.deleteMany({
|
|
217
|
+
model: "teamMember",
|
|
218
|
+
where: [{
|
|
219
|
+
field: "userId",
|
|
220
|
+
value: userId
|
|
221
|
+
}, {
|
|
222
|
+
field: "teamId",
|
|
223
|
+
value: teams.map((team) => team.id),
|
|
224
|
+
operator: "in"
|
|
225
|
+
}]
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return member;
|
|
206
229
|
});
|
|
207
|
-
if (options?.teams?.enabled) {
|
|
208
|
-
const teams = await adapter.findMany({
|
|
209
|
-
model: "team",
|
|
210
|
-
where: [{
|
|
211
|
-
field: "organizationId",
|
|
212
|
-
value: organizationId
|
|
213
|
-
}]
|
|
214
|
-
});
|
|
215
|
-
await Promise.all(teams.map((team) => adapter.deleteMany({
|
|
216
|
-
model: "teamMember",
|
|
217
|
-
where: [{
|
|
218
|
-
field: "teamId",
|
|
219
|
-
value: team.id
|
|
220
|
-
}, {
|
|
221
|
-
field: "userId",
|
|
222
|
-
value: userId
|
|
223
|
-
}]
|
|
224
|
-
})));
|
|
225
|
-
}
|
|
226
|
-
return member;
|
|
227
230
|
},
|
|
228
231
|
updateOrganization: async (organizationId, data) => {
|
|
229
232
|
const organization = await (await getCurrentAdapter(baseAdapter)).update({
|
|
@@ -244,29 +247,31 @@ const getOrgAdapter = (context, options) => {
|
|
|
244
247
|
}, orgAdditionalFields);
|
|
245
248
|
},
|
|
246
249
|
deleteOrganization: async (organizationId) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
250
|
+
return runWithTransaction(baseAdapter, async () => {
|
|
251
|
+
const adapter = await getCurrentAdapter(baseAdapter);
|
|
252
|
+
await adapter.deleteMany({
|
|
253
|
+
model: "member",
|
|
254
|
+
where: [{
|
|
255
|
+
field: "organizationId",
|
|
256
|
+
value: organizationId
|
|
257
|
+
}]
|
|
258
|
+
});
|
|
259
|
+
await adapter.deleteMany({
|
|
260
|
+
model: "invitation",
|
|
261
|
+
where: [{
|
|
262
|
+
field: "organizationId",
|
|
263
|
+
value: organizationId
|
|
264
|
+
}]
|
|
265
|
+
});
|
|
266
|
+
await adapter.delete({
|
|
267
|
+
model: "organization",
|
|
268
|
+
where: [{
|
|
269
|
+
field: "id",
|
|
270
|
+
value: organizationId
|
|
271
|
+
}]
|
|
272
|
+
});
|
|
273
|
+
return organizationId;
|
|
268
274
|
});
|
|
269
|
-
return organizationId;
|
|
270
275
|
},
|
|
271
276
|
setActiveOrganization: async (sessionToken, organizationId, ctx) => {
|
|
272
277
|
return await context.internalAdapter.updateSession(sessionToken, { activeOrganizationId: organizationId });
|
|
@@ -273,6 +273,7 @@ declare const organizationClient: <CO extends OrganizationClientOptions>(options
|
|
|
273
273
|
ROLE_NAME_IS_ALREADY_TAKEN: _better_auth_core_utils_error_codes0.RawError<"ROLE_NAME_IS_ALREADY_TAKEN">;
|
|
274
274
|
CANNOT_DELETE_A_PRE_DEFINED_ROLE: _better_auth_core_utils_error_codes0.RawError<"CANNOT_DELETE_A_PRE_DEFINED_ROLE">;
|
|
275
275
|
ROLE_IS_ASSIGNED_TO_MEMBERS: _better_auth_core_utils_error_codes0.RawError<"ROLE_IS_ASSIGNED_TO_MEMBERS">;
|
|
276
|
+
INVALID_TEAM_ID: _better_auth_core_utils_error_codes0.RawError<"INVALID_TEAM_ID">;
|
|
276
277
|
};
|
|
277
278
|
};
|
|
278
279
|
declare const inferOrgAdditionalFields: <O extends {
|
|
@@ -371,4 +372,4 @@ declare const inferOrgAdditionalFields: <O extends {
|
|
|
371
372
|
additionalFields: unknown;
|
|
372
373
|
} ? K : never]: S_1[K] } : undefined : undefined : undefined : S;
|
|
373
374
|
//#endregion
|
|
374
|
-
export { clientSideHasPermission, inferOrgAdditionalFields, organizationClient };
|
|
375
|
+
export { OrganizationClientOptions, clientSideHasPermission, inferOrgAdditionalFields, organizationClient };
|
|
@@ -60,6 +60,7 @@ declare const ORGANIZATION_ERROR_CODES: {
|
|
|
60
60
|
ROLE_NAME_IS_ALREADY_TAKEN: _better_auth_core_utils_error_codes0.RawError<"ROLE_NAME_IS_ALREADY_TAKEN">;
|
|
61
61
|
CANNOT_DELETE_A_PRE_DEFINED_ROLE: _better_auth_core_utils_error_codes0.RawError<"CANNOT_DELETE_A_PRE_DEFINED_ROLE">;
|
|
62
62
|
ROLE_IS_ASSIGNED_TO_MEMBERS: _better_auth_core_utils_error_codes0.RawError<"ROLE_IS_ASSIGNED_TO_MEMBERS">;
|
|
63
|
+
INVALID_TEAM_ID: _better_auth_core_utils_error_codes0.RawError<"INVALID_TEAM_ID">;
|
|
63
64
|
};
|
|
64
65
|
//#endregion
|
|
65
66
|
export { ORGANIZATION_ERROR_CODES };
|
|
@@ -58,7 +58,8 @@ const ORGANIZATION_ERROR_CODES = defineErrorCodes({
|
|
|
58
58
|
INVALID_RESOURCE: "The provided permission includes an invalid resource",
|
|
59
59
|
ROLE_NAME_IS_ALREADY_TAKEN: "That role name is already taken",
|
|
60
60
|
CANNOT_DELETE_A_PRE_DEFINED_ROLE: "Cannot delete a pre-defined role",
|
|
61
|
-
ROLE_IS_ASSIGNED_TO_MEMBERS: "Cannot delete a role that is assigned to members. Please reassign the members to a different role first"
|
|
61
|
+
ROLE_IS_ASSIGNED_TO_MEMBERS: "Cannot delete a role that is assigned to members. Please reassign the members to a different role first",
|
|
62
|
+
INVALID_TEAM_ID: "Team id contains a reserved character"
|
|
62
63
|
});
|
|
63
64
|
//#endregion
|
|
64
65
|
export { ORGANIZATION_ERROR_CODES };
|
|
@@ -155,6 +155,9 @@ const createInvitation = (option) => {
|
|
|
155
155
|
member
|
|
156
156
|
}, ctx.context) : ctx.context.orgOptions.invitationLimit ?? 100;
|
|
157
157
|
if ((await adapter.findPendingInvitations({ organizationId })).length >= invitationLimit) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED);
|
|
158
|
+
if (ctx.context.orgOptions.teams?.enabled && "teamId" in ctx.body && ctx.body.teamId) {
|
|
159
|
+
if ((typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId).some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID);
|
|
160
|
+
}
|
|
158
161
|
if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined" && "teamId" in ctx.body && ctx.body.teamId) {
|
|
159
162
|
const teamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
|
|
160
163
|
for (const teamId of teamIds) {
|
|
@@ -15,7 +15,7 @@ declare const createOrganization: <O extends OrganizationOptions>(options?: O |
|
|
|
15
15
|
name: z.ZodString;
|
|
16
16
|
slug: z.ZodString;
|
|
17
17
|
userId: z.ZodOptional<z.ZodCoercedString<unknown>>;
|
|
18
|
-
logo: z.ZodOptional<z.ZodString
|
|
18
|
+
logo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
19
19
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
20
20
|
keepCurrentActiveOrganization: z.ZodOptional<z.ZodBoolean>;
|
|
21
21
|
}, z.core.$strip>;
|
|
@@ -55,7 +55,7 @@ declare const createOrganization: <O extends OrganizationOptions>(options?: O |
|
|
|
55
55
|
name: string;
|
|
56
56
|
slug: string;
|
|
57
57
|
userId?: string | undefined;
|
|
58
|
-
logo?: string | undefined;
|
|
58
|
+
logo?: string | null | undefined;
|
|
59
59
|
metadata?: Record<string, any> | undefined;
|
|
60
60
|
keepCurrentActiveOrganization?: boolean | undefined;
|
|
61
61
|
};
|
|
@@ -165,7 +165,7 @@ declare const updateOrganization: <O extends OrganizationOptions>(options?: O |
|
|
|
165
165
|
data: z.ZodObject<{
|
|
166
166
|
name: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
167
167
|
slug: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
168
|
-
logo: z.ZodOptional<z.ZodOptional<z.ZodString
|
|
168
|
+
logo: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
169
169
|
metadata: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
170
170
|
}, z.core.$strip>;
|
|
171
171
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
@@ -207,7 +207,7 @@ declare const updateOrganization: <O extends OrganizationOptions>(options?: O |
|
|
|
207
207
|
data: {
|
|
208
208
|
name?: string | undefined;
|
|
209
209
|
slug?: string | undefined;
|
|
210
|
-
logo?: string | undefined;
|
|
210
|
+
logo?: string | null | undefined;
|
|
211
211
|
metadata?: Record<string, any> | undefined;
|
|
212
212
|
} & Partial<InferAdditionalFieldsFromPluginOptions<"organization", O>>;
|
|
213
213
|
organizationId?: string | undefined;
|
|
@@ -13,7 +13,7 @@ const baseOrganizationSchema = z.object({
|
|
|
13
13
|
name: z.string().min(1).meta({ description: "The name of the organization" }),
|
|
14
14
|
slug: z.string().min(1).meta({ description: "The slug of the organization" }),
|
|
15
15
|
userId: z.coerce.string().meta({ description: "The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server. server-only. Eg: \"user-id\"" }).optional(),
|
|
16
|
-
logo: z.string().meta({ description: "The logo of the organization" }).
|
|
16
|
+
logo: z.string().meta({ description: "The logo of the organization" }).nullish(),
|
|
17
17
|
metadata: z.record(z.string(), z.any()).meta({ description: "The metadata of the organization" }).optional(),
|
|
18
18
|
keepCurrentActiveOrganization: z.boolean().meta({ description: "Whether to keep the current active organization active after creating a new one. Eg: true" }).optional()
|
|
19
19
|
});
|
|
@@ -160,7 +160,7 @@ const checkOrganizationSlug = (options) => createAuthEndpoint("/organization/che
|
|
|
160
160
|
const baseUpdateOrganizationSchema = z.object({
|
|
161
161
|
name: z.string().min(1).meta({ description: "The name of the organization" }).optional(),
|
|
162
162
|
slug: z.string().min(1).meta({ description: "The slug of the organization" }).optional(),
|
|
163
|
-
logo: z.string().meta({ description: "The logo of the organization" }).
|
|
163
|
+
logo: z.string().meta({ description: "The logo of the organization" }).nullish(),
|
|
164
164
|
metadata: z.record(z.string(), z.any()).meta({ description: "The metadata of the organization" }).optional()
|
|
165
165
|
});
|
|
166
166
|
const updateOrganization = (options) => {
|
|
@@ -317,7 +317,7 @@ interface OrganizationOptions {
|
|
|
317
317
|
organization: {
|
|
318
318
|
name?: string;
|
|
319
319
|
slug?: string;
|
|
320
|
-
logo?: string;
|
|
320
|
+
logo?: string | null;
|
|
321
321
|
metadata?: Record<string, any>;
|
|
322
322
|
[key: string]: any;
|
|
323
323
|
};
|
|
@@ -348,7 +348,7 @@ interface OrganizationOptions {
|
|
|
348
348
|
organization: {
|
|
349
349
|
name?: string;
|
|
350
350
|
slug?: string;
|
|
351
|
-
logo?: string;
|
|
351
|
+
logo?: string | null;
|
|
352
352
|
metadata?: Record<string, any>;
|
|
353
353
|
[key: string]: any;
|
|
354
354
|
};
|
|
@@ -358,7 +358,7 @@ interface OrganizationOptions {
|
|
|
358
358
|
data: {
|
|
359
359
|
name?: string;
|
|
360
360
|
slug?: string;
|
|
361
|
-
logo?: string;
|
|
361
|
+
logo?: string | null;
|
|
362
362
|
metadata?: Record<string, any>;
|
|
363
363
|
[key: string]: any;
|
|
364
364
|
};
|
|
@@ -470,7 +470,7 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
470
470
|
else await ctx.context.internalAdapter.updatePassword(user.id, hashedPassword);
|
|
471
471
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(phoneResetIdentifier);
|
|
472
472
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
473
|
-
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.
|
|
473
|
+
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(user.id);
|
|
474
474
|
return ctx.json({ status: true });
|
|
475
475
|
});
|
|
476
476
|
function generateOTP(size) {
|
|
@@ -264,9 +264,10 @@ declare const backupCode2fa: (opts: BackupCodeOptions) => {
|
|
|
264
264
|
backupCodes: string[];
|
|
265
265
|
}>;
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
267
|
+
* A server-only function that returns a user's decrypted two-factor
|
|
268
|
+
* backup codes. It is not exposed over HTTP and has no client method;
|
|
269
|
+
* call it from trusted server code with a `userId` taken from an
|
|
270
|
+
* authenticated session.
|
|
270
271
|
*
|
|
271
272
|
* ### API Methods
|
|
272
273
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
2
2
|
import { TWO_FACTOR_ERROR_CODES } from "./error-code.mjs";
|
|
3
|
+
import { isSafeUrlScheme } from "@better-auth/core/utils/url";
|
|
3
4
|
//#region src/plugins/two-factor/client.ts
|
|
4
5
|
const twoFactorClient = (options) => {
|
|
5
6
|
return {
|
|
@@ -29,7 +30,7 @@ const twoFactorClient = (options) => {
|
|
|
29
30
|
await options.onTwoFactorRedirect({ twoFactorMethods: context.data.twoFactorMethods });
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
|
-
if (options?.twoFactorPage && typeof window !== "undefined") window.location.href = options.twoFactorPage;
|
|
33
|
+
if (options?.twoFactorPage && typeof window !== "undefined" && isSafeUrlScheme(options.twoFactorPage)) window.location.href = options.twoFactorPage;
|
|
33
34
|
}
|
|
34
35
|
} }
|
|
35
36
|
}],
|
|
@@ -2,7 +2,7 @@ import { mergeSchema } from "../../db/schema.mjs";
|
|
|
2
2
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
3
3
|
import { symmetricEncrypt } from "../../crypto/index.mjs";
|
|
4
4
|
import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
5
|
-
import { sessionMiddleware } from "../../api/routes/session.mjs";
|
|
5
|
+
import { sensitiveSessionMiddleware, sessionMiddleware } from "../../api/routes/session.mjs";
|
|
6
6
|
import { shouldRequirePassword, validatePassword } from "../../utils/password.mjs";
|
|
7
7
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
8
8
|
import { TWO_FACTOR_ERROR_CODES } from "./error-code.mjs";
|
|
@@ -138,7 +138,7 @@ const twoFactor = (options) => {
|
|
|
138
138
|
disableTwoFactor: createAuthEndpoint("/two-factor/disable", {
|
|
139
139
|
method: "POST",
|
|
140
140
|
body: disableTwoFactorBodySchema,
|
|
141
|
-
use: [
|
|
141
|
+
use: [sensitiveSessionMiddleware],
|
|
142
142
|
metadata: { openapi: {
|
|
143
143
|
summary: "Disable two factor authentication",
|
|
144
144
|
description: "Use this endpoint to disable two factor authentication.",
|
|
@@ -224,6 +224,7 @@ const twoFactor = (options) => {
|
|
|
224
224
|
*/
|
|
225
225
|
deleteSessionCookie(ctx, true);
|
|
226
226
|
await ctx.context.internalAdapter.deleteSession(data.session.token);
|
|
227
|
+
ctx.context.setNewSession(null);
|
|
227
228
|
const maxAge = options?.twoFactorCookieMaxAge ?? 600;
|
|
228
229
|
const twoFactorCookie = ctx.context.createAuthCookie(TWO_FACTOR_COOKIE_NAME, { maxAge });
|
|
229
230
|
const identifier = `2fa-${generateRandomString(20)}`;
|
|
@@ -90,9 +90,20 @@ declare const username: (options?: UsernameOptions | undefined) => {
|
|
|
90
90
|
name: string;
|
|
91
91
|
image?: string | null | undefined;
|
|
92
92
|
} & Record<string, unknown>, context: _better_auth_core0.GenericEndpointContext | null): Promise<{
|
|
93
|
+
data: {
|
|
94
|
+
username: string;
|
|
95
|
+
displayUsername: string;
|
|
96
|
+
id: string;
|
|
97
|
+
createdAt: Date;
|
|
98
|
+
updatedAt: Date;
|
|
99
|
+
email: string;
|
|
100
|
+
emailVerified: boolean;
|
|
101
|
+
name: string;
|
|
102
|
+
image?: string | null | undefined;
|
|
103
|
+
};
|
|
104
|
+
} | {
|
|
93
105
|
data: {
|
|
94
106
|
displayUsername?: string | undefined;
|
|
95
|
-
username?: string | undefined;
|
|
96
107
|
id: string;
|
|
97
108
|
createdAt: Date;
|
|
98
109
|
updatedAt: Date;
|
|
@@ -115,7 +126,18 @@ declare const username: (options?: UsernameOptions | undefined) => {
|
|
|
115
126
|
}> & Record<string, unknown>, context: _better_auth_core0.GenericEndpointContext | null): Promise<{
|
|
116
127
|
data: {
|
|
117
128
|
displayUsername?: string | undefined;
|
|
118
|
-
username
|
|
129
|
+
username: string;
|
|
130
|
+
id?: string | undefined;
|
|
131
|
+
createdAt?: Date | undefined;
|
|
132
|
+
updatedAt?: Date | undefined;
|
|
133
|
+
email?: string | undefined;
|
|
134
|
+
emailVerified?: boolean | undefined;
|
|
135
|
+
name?: string | undefined;
|
|
136
|
+
image?: string | null | undefined;
|
|
137
|
+
};
|
|
138
|
+
} | {
|
|
139
|
+
data: {
|
|
140
|
+
displayUsername?: string | undefined;
|
|
119
141
|
id?: string | undefined;
|
|
120
142
|
createdAt?: Date | undefined;
|
|
121
143
|
updatedAt?: Date | undefined;
|
|
@@ -28,6 +28,31 @@ const username = (options) => {
|
|
|
28
28
|
const displayUsernameNormalizer = (displayUsername) => {
|
|
29
29
|
return options?.displayUsernameNormalization ? options.displayUsernameNormalization(displayUsername) : displayUsername;
|
|
30
30
|
};
|
|
31
|
+
const minUsernameLength = options?.minUsernameLength || 3;
|
|
32
|
+
const maxUsernameLength = options?.maxUsernameLength || 30;
|
|
33
|
+
const validator = options?.usernameValidator || defaultUsernameValidator;
|
|
34
|
+
const pathsWithHttpHookValidation = ["/sign-up/email", "/update-user"];
|
|
35
|
+
async function validateUsername(username, displayUsername, adapter, currentUserId) {
|
|
36
|
+
const usernameToValidate = options?.validationOrder?.username === "post-normalization" ? normalizer(username) : username;
|
|
37
|
+
if (usernameToValidate.length < minUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
|
|
38
|
+
if (usernameToValidate.length > maxUsernameLength) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
|
|
39
|
+
if (!await validator(usernameToValidate)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_USERNAME);
|
|
40
|
+
const normalizedUsername = normalizer(username);
|
|
41
|
+
const existingUser = await adapter.findOne({
|
|
42
|
+
model: "user",
|
|
43
|
+
where: [{
|
|
44
|
+
field: "username",
|
|
45
|
+
value: normalizedUsername
|
|
46
|
+
}]
|
|
47
|
+
});
|
|
48
|
+
if (existingUser) {
|
|
49
|
+
if (!currentUserId || existingUser.id !== currentUserId) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.USERNAME_IS_ALREADY_TAKEN);
|
|
50
|
+
}
|
|
51
|
+
if (displayUsername && options?.displayUsernameValidator) {
|
|
52
|
+
const displayUsernameToValidate = options?.validationOrder?.displayUsername === "post-normalization" ? displayUsernameNormalizer(displayUsername) : displayUsername;
|
|
53
|
+
if (!await options.displayUsernameValidator(displayUsernameToValidate)) throw APIError.from("BAD_REQUEST", USERNAME_ERROR_CODES.INVALID_DISPLAY_USERNAME);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
return {
|
|
32
57
|
id: "username",
|
|
33
58
|
version: PACKAGE_VERSION,
|
|
@@ -36,18 +61,39 @@ const username = (options) => {
|
|
|
36
61
|
create: { async before(user, context) {
|
|
37
62
|
const username = "username" in user ? user.username : null;
|
|
38
63
|
const displayUsername = "displayUsername" in user ? user.displayUsername : null;
|
|
64
|
+
const currentPath = context?.path;
|
|
65
|
+
const skipValidation = currentPath && pathsWithHttpHookValidation.includes(currentPath);
|
|
66
|
+
if (username) {
|
|
67
|
+
if (!skipValidation) await validateUsername(username, displayUsername, ctx.adapter);
|
|
68
|
+
return { data: {
|
|
69
|
+
...user,
|
|
70
|
+
username: normalizer(username),
|
|
71
|
+
displayUsername: displayUsername ? displayUsernameNormalizer(displayUsername) : username
|
|
72
|
+
} };
|
|
73
|
+
}
|
|
39
74
|
return { data: {
|
|
40
75
|
...user,
|
|
41
|
-
...username ? { username: normalizer(username) } : {},
|
|
42
76
|
...displayUsername ? { displayUsername: displayUsernameNormalizer(displayUsername) } : {}
|
|
43
77
|
} };
|
|
44
78
|
} },
|
|
45
79
|
update: { async before(user, context) {
|
|
46
80
|
const username = "username" in user ? user.username : null;
|
|
47
81
|
const displayUsername = "displayUsername" in user ? user.displayUsername : null;
|
|
82
|
+
const currentPath = context?.path;
|
|
83
|
+
const skipValidation = currentPath && pathsWithHttpHookValidation.includes(currentPath);
|
|
84
|
+
if (username) {
|
|
85
|
+
if (!skipValidation) {
|
|
86
|
+
const currentUserId = context?.context?.session?.user?.id || ("id" in user ? user.id : null);
|
|
87
|
+
await validateUsername(username, displayUsername, ctx.adapter, currentUserId);
|
|
88
|
+
}
|
|
89
|
+
return { data: {
|
|
90
|
+
...user,
|
|
91
|
+
username: normalizer(username),
|
|
92
|
+
...displayUsername ? { displayUsername: displayUsernameNormalizer(displayUsername) } : {}
|
|
93
|
+
} };
|
|
94
|
+
}
|
|
48
95
|
return { data: {
|
|
49
96
|
...user,
|
|
50
|
-
...username ? { username: normalizer(username) } : {},
|
|
51
97
|
...displayUsername ? { displayUsername: displayUsernameNormalizer(displayUsername) } : {}
|
|
52
98
|
} };
|
|
53
99
|
} }
|
|
@@ -153,7 +199,7 @@ const username = (options) => {
|
|
|
153
199
|
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw APIError.from("FORBIDDEN", USERNAME_ERROR_CODES.EMAIL_NOT_VERIFIED);
|
|
154
200
|
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
|
|
155
201
|
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
156
|
-
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
|
|
202
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
157
203
|
await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
158
204
|
user,
|
|
159
205
|
url,
|
package/dist/state.d.mts
CHANGED
|
@@ -27,8 +27,8 @@ declare function generateGenericState(c: GenericEndpointContext, stateData: Stat
|
|
|
27
27
|
state: string;
|
|
28
28
|
codeVerifier: string;
|
|
29
29
|
}>;
|
|
30
|
-
declare function parseGenericState(c: GenericEndpointContext, state: string, settings?: {
|
|
31
|
-
cookieName
|
|
30
|
+
declare function parseGenericState(c: GenericEndpointContext, state: string | undefined, settings?: {
|
|
31
|
+
cookieName?: string;
|
|
32
32
|
skipStateCookieCheck?: boolean;
|
|
33
33
|
}): Promise<{
|
|
34
34
|
[x: string]: unknown;
|
package/dist/state.mjs
CHANGED
|
@@ -20,10 +20,19 @@ const stateDataSchema = z.looseObject({
|
|
|
20
20
|
var StateError = class extends BetterAuthError {
|
|
21
21
|
code;
|
|
22
22
|
details;
|
|
23
|
+
/**
|
|
24
|
+
* The per-flow `errorCallbackURL` recovered from the parsed state, when the
|
|
25
|
+
* failure happened after the state was successfully parsed (for example a
|
|
26
|
+
* nonce or state-cookie mismatch). It was origin-validated at sign-in, so
|
|
27
|
+
* the callback can safely redirect there instead of the default error page.
|
|
28
|
+
* Absent when the state could not be parsed at all.
|
|
29
|
+
*/
|
|
30
|
+
errorURL;
|
|
23
31
|
constructor(message, options) {
|
|
24
32
|
super(message, options);
|
|
25
33
|
this.code = options.code;
|
|
26
34
|
this.details = options.details;
|
|
35
|
+
this.errorURL = options.errorURL;
|
|
27
36
|
}
|
|
28
37
|
};
|
|
29
38
|
async function generateGenericState(c, stateData, settings) {
|
|
@@ -62,6 +71,7 @@ async function generateGenericState(c, stateData, settings) {
|
|
|
62
71
|
};
|
|
63
72
|
}
|
|
64
73
|
async function parseGenericState(c, state, settings) {
|
|
74
|
+
if (!state) throw new StateError("State not found in OAuth callback", { code: "state_not_found" });
|
|
65
75
|
const storeStateStrategy = c.context.oauthConfig.storeStateStrategy;
|
|
66
76
|
let parsedData;
|
|
67
77
|
if (storeStateStrategy === "cookie") {
|
|
@@ -86,7 +96,8 @@ async function parseGenericState(c, state, settings) {
|
|
|
86
96
|
}
|
|
87
97
|
if (!parsedData.oauthState || parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
|
|
88
98
|
code: "state_security_mismatch",
|
|
89
|
-
details: { state }
|
|
99
|
+
details: { state },
|
|
100
|
+
errorURL: parsedData.errorURL
|
|
90
101
|
});
|
|
91
102
|
expireCookie(c, stateCookie);
|
|
92
103
|
} else {
|
|
@@ -98,20 +109,23 @@ async function parseGenericState(c, state, settings) {
|
|
|
98
109
|
parsedData = stateDataSchema.parse(JSON.parse(data.value));
|
|
99
110
|
if (parsedData.oauthState !== void 0 && parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
|
|
100
111
|
code: "state_security_mismatch",
|
|
101
|
-
details: { state }
|
|
112
|
+
details: { state },
|
|
113
|
+
errorURL: parsedData.errorURL
|
|
102
114
|
});
|
|
103
115
|
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state");
|
|
104
116
|
const stateCookieValue = await c.getSignedCookie(stateCookie.name, c.context.secret);
|
|
105
117
|
if (!(settings?.skipStateCookieCheck ?? c.context.oauthConfig.skipStateCookieCheck) && (!stateCookieValue || stateCookieValue !== state)) throw new StateError("State mismatch: State not persisted correctly", {
|
|
106
118
|
code: "state_security_mismatch",
|
|
107
|
-
details: { state }
|
|
119
|
+
details: { state },
|
|
120
|
+
errorURL: parsedData.errorURL
|
|
108
121
|
});
|
|
109
122
|
expireCookie(c, stateCookie);
|
|
110
123
|
await c.context.internalAdapter.deleteVerificationByIdentifier(state);
|
|
111
124
|
}
|
|
112
125
|
if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", {
|
|
113
126
|
code: "state_mismatch",
|
|
114
|
-
details: { expiresAt: parsedData.expiresAt }
|
|
127
|
+
details: { expiresAt: parsedData.expiresAt },
|
|
128
|
+
errorURL: parsedData.errorURL
|
|
115
129
|
});
|
|
116
130
|
return parsedData;
|
|
117
131
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applySetCookies } from "../cookies/cookie-utils.mjs";
|
|
1
2
|
//#region src/test-utils/headers.ts
|
|
2
3
|
/**
|
|
3
4
|
* converts set cookie containing headers to
|
|
@@ -9,13 +10,7 @@ function convertSetCookieToCookie(headers) {
|
|
|
9
10
|
if (name.toLowerCase() === "set-cookie") setCookieHeaders.push(value);
|
|
10
11
|
});
|
|
11
12
|
if (setCookieHeaders.length === 0) return headers;
|
|
12
|
-
|
|
13
|
-
const cookies = existingCookies ? existingCookies.split("; ") : [];
|
|
14
|
-
setCookieHeaders.forEach((setCookie) => {
|
|
15
|
-
const cookiePair = setCookie.split(";")[0];
|
|
16
|
-
cookies.push(cookiePair.trim());
|
|
17
|
-
});
|
|
18
|
-
headers.set("cookie", cookies.join("; "));
|
|
13
|
+
applySetCookies(headers, setCookieHeaders);
|
|
19
14
|
return headers;
|
|
20
15
|
}
|
|
21
16
|
//#endregion
|