better-auth 1.6.15 → 1.6.17

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.
Files changed (105) hide show
  1. package/dist/api/index.d.mts +2 -2
  2. package/dist/api/index.mjs +3 -4
  3. package/dist/api/middlewares/origin-check.mjs +6 -1
  4. package/dist/api/rate-limiter/index.mjs +259 -73
  5. package/dist/api/routes/account.mjs +31 -11
  6. package/dist/api/routes/callback.mjs +3 -3
  7. package/dist/api/routes/index.d.mts +1 -1
  8. package/dist/api/routes/password.mjs +3 -4
  9. package/dist/api/routes/session.d.mts +12 -1
  10. package/dist/api/routes/session.mjs +16 -2
  11. package/dist/api/routes/sign-in.mjs +5 -5
  12. package/dist/api/routes/sign-up.mjs +2 -2
  13. package/dist/api/routes/update-session.mjs +9 -4
  14. package/dist/api/routes/update-user.mjs +10 -12
  15. package/dist/auth/base.mjs +11 -7
  16. package/dist/client/equality.d.mts +19 -0
  17. package/dist/client/equality.mjs +42 -0
  18. package/dist/client/index.d.mts +5 -4
  19. package/dist/client/index.mjs +2 -1
  20. package/dist/client/lynx/index.d.mts +6 -5
  21. package/dist/client/path-to-object.d.mts +5 -2
  22. package/dist/client/plugins/index.d.mts +4 -1
  23. package/dist/client/plugins/index.mjs +4 -1
  24. package/dist/client/query.d.mts +4 -3
  25. package/dist/client/query.mjs +27 -17
  26. package/dist/client/react/index.d.mts +6 -5
  27. package/dist/client/session-atom.mjs +129 -4
  28. package/dist/client/session-refresh.d.mts +3 -18
  29. package/dist/client/session-refresh.mjs +38 -49
  30. package/dist/client/solid/index.d.mts +6 -5
  31. package/dist/client/svelte/index.d.mts +6 -5
  32. package/dist/client/types.d.mts +2 -2
  33. package/dist/client/vanilla.d.mts +6 -5
  34. package/dist/client/vue/index.d.mts +6 -5
  35. package/dist/context/create-context.mjs +3 -2
  36. package/dist/context/store-capabilities.mjs +12 -0
  37. package/dist/cookies/index.mjs +30 -2
  38. package/dist/db/internal-adapter.mjs +56 -0
  39. package/dist/oauth2/link-account.d.mts +13 -0
  40. package/dist/oauth2/link-account.mjs +1 -1
  41. package/dist/package.mjs +1 -1
  42. package/dist/plugins/access/access.mjs +49 -19
  43. package/dist/plugins/admin/access/statement.d.mts +10 -10
  44. package/dist/plugins/admin/access/statement.mjs +2 -0
  45. package/dist/plugins/admin/admin.d.mts +6 -3
  46. package/dist/plugins/admin/client.d.mts +6 -4
  47. package/dist/plugins/admin/error-codes.d.mts +2 -0
  48. package/dist/plugins/admin/error-codes.mjs +3 -1
  49. package/dist/plugins/admin/routes.mjs +73 -5
  50. package/dist/plugins/admin/schema.d.mts +1 -0
  51. package/dist/plugins/admin/schema.mjs +2 -1
  52. package/dist/plugins/captcha/constants.mjs +8 -1
  53. package/dist/plugins/captcha/index.mjs +8 -2
  54. package/dist/plugins/captcha/types.d.mts +21 -0
  55. package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
  56. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
  57. package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
  58. package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
  59. package/dist/plugins/device-authorization/routes.mjs +16 -9
  60. package/dist/plugins/email-otp/routes.mjs +23 -53
  61. package/dist/plugins/generic-oauth/index.mjs +7 -2
  62. package/dist/plugins/generic-oauth/routes.mjs +20 -9
  63. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  64. package/dist/plugins/haveibeenpwned/index.mjs +5 -1
  65. package/dist/plugins/index.d.mts +5 -1
  66. package/dist/plugins/index.mjs +4 -1
  67. package/dist/plugins/jwt/index.mjs +2 -2
  68. package/dist/plugins/mcp/client/index.mjs +1 -0
  69. package/dist/plugins/mcp/index.mjs +8 -0
  70. package/dist/plugins/multi-session/index.mjs +7 -5
  71. package/dist/plugins/oauth-popup/client.d.mts +82 -0
  72. package/dist/plugins/oauth-popup/client.mjs +203 -0
  73. package/dist/plugins/oauth-popup/constants.d.mts +11 -0
  74. package/dist/plugins/oauth-popup/constants.mjs +11 -0
  75. package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
  76. package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
  77. package/dist/plugins/oauth-popup/index.d.mts +67 -0
  78. package/dist/plugins/oauth-popup/index.mjs +227 -0
  79. package/dist/plugins/oauth-popup/types.d.mts +30 -0
  80. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  81. package/dist/plugins/oauth-proxy/utils.mjs +16 -2
  82. package/dist/plugins/oidc-provider/index.mjs +10 -0
  83. package/dist/plugins/one-tap/client.mjs +12 -6
  84. package/dist/plugins/one-tap/index.d.mts +1 -0
  85. package/dist/plugins/one-tap/index.mjs +9 -5
  86. package/dist/plugins/one-time-token/index.mjs +1 -3
  87. package/dist/plugins/open-api/generator.mjs +7 -4
  88. package/dist/plugins/organization/adapter.d.mts +29 -1
  89. package/dist/plugins/organization/adapter.mjs +66 -6
  90. package/dist/plugins/organization/organization.mjs +2 -0
  91. package/dist/plugins/organization/routes/crud-invites.mjs +55 -31
  92. package/dist/plugins/organization/routes/crud-members.mjs +42 -6
  93. package/dist/plugins/organization/routes/crud-team.mjs +51 -5
  94. package/dist/plugins/organization/schema.d.mts +2 -0
  95. package/dist/plugins/phone-number/routes.mjs +41 -36
  96. package/dist/plugins/siwe/index.mjs +30 -3
  97. package/dist/plugins/siwe/parse-message.mjs +60 -0
  98. package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
  99. package/dist/plugins/two-factor/index.mjs +9 -1
  100. package/dist/plugins/two-factor/otp/index.mjs +11 -13
  101. package/dist/plugins/two-factor/totp/index.mjs +1 -1
  102. package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
  103. package/dist/plugins/username/index.mjs +6 -6
  104. package/dist/test-utils/test-instance.d.mts +6 -5
  105. package/package.json +10 -10
@@ -32,6 +32,7 @@ declare const oneTap: (options?: OneTapOptions | undefined) => {
32
32
  method: "POST";
33
33
  body: z.ZodObject<{
34
34
  idToken: z.ZodString;
35
+ callbackURL: z.ZodOptional<z.ZodString>;
35
36
  }, z.core.$strip>;
36
37
  metadata: {
37
38
  openapi: {
@@ -8,7 +8,10 @@ import { createAuthEndpoint } from "@better-auth/core/api";
8
8
  import * as z from "zod";
9
9
  import { createRemoteJWKSet, jwtVerify } from "jose";
10
10
  //#region src/plugins/one-tap/index.ts
11
- const oneTapCallbackBodySchema = z.object({ idToken: z.string().meta({ description: "Google ID token, which the client obtains from the One Tap API" }) });
11
+ const oneTapCallbackBodySchema = z.object({
12
+ idToken: z.string().meta({ description: "Google ID token, which the client obtains from the One Tap API" }),
13
+ callbackURL: z.string().meta({ description: "URL to redirect to after a successful sign-in" }).optional()
14
+ });
12
15
  const oneTap = (options) => ({
13
16
  id: "one-tap",
14
17
  version: PACKAGE_VERSION,
@@ -34,13 +37,14 @@ const oneTap = (options) => ({
34
37
  } }
35
38
  }, async (ctx) => {
36
39
  const { idToken } = ctx.body;
40
+ const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
41
+ const audience = options?.clientId || googleProvider?.clientId;
42
+ if (!audience || Array.isArray(audience) && audience.length === 0) throw new APIError("BAD_REQUEST", { message: "Google client ID is required for One Tap. Set it on the oneTap plugin (clientId) or on socialProviders.google." });
37
43
  let payload;
38
44
  try {
39
- const JWKS = createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs"));
40
- const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
41
- const { payload: verifiedPayload } = await jwtVerify(idToken, JWKS, {
45
+ const { payload: verifiedPayload } = await jwtVerify(idToken, createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs")), {
42
46
  issuer: ["https://accounts.google.com", "accounts.google.com"],
43
- audience: options?.clientId || googleProvider?.clientId
47
+ audience
44
48
  });
45
49
  payload = verifiedPayload;
46
50
  } catch {
@@ -47,10 +47,8 @@ const oneTimeToken = (options) => {
47
47
  }, async (c) => {
48
48
  const { token } = c.body;
49
49
  const storedToken = await storeToken(c, token);
50
- const verificationValue = await c.context.internalAdapter.findVerificationValue(`one-time-token:${storedToken}`);
50
+ const verificationValue = await c.context.internalAdapter.consumeVerificationValue(`one-time-token:${storedToken}`);
51
51
  if (!verificationValue) throw c.error("BAD_REQUEST", { message: "Invalid token" });
52
- await c.context.internalAdapter.deleteVerificationByIdentifier(`one-time-token:${storedToken}`);
53
- if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw c.error("BAD_REQUEST", { message: "Token expired" });
54
52
  const session = await c.context.internalAdapter.findSession(verificationValue.value);
55
53
  if (!session) throw c.error("BAD_REQUEST", { message: "Session not found" });
56
54
  if (!opts?.disableSetSessionCookie) await setSessionCookie(c, session);
@@ -178,12 +178,15 @@ async function generator(ctx, options) {
178
178
  const components = { schemas: { ...Object.entries(tables).reduce((acc, [key, value]) => {
179
179
  const modelName = key.charAt(0).toUpperCase() + key.slice(1);
180
180
  const fields = value.fields;
181
- const required = [];
182
- const properties = { id: { type: "string" } };
181
+ const required = new Set(["id"]);
182
+ const properties = { id: {
183
+ type: "string",
184
+ readOnly: true
185
+ } };
183
186
  Object.entries(fields).forEach(([fieldKey, fieldValue]) => {
184
187
  if (!fieldValue) return;
185
188
  properties[fieldKey] = getFieldSchema(fieldValue);
186
- if (fieldValue.required && fieldValue.input !== false) required.push(fieldKey);
189
+ if (fieldValue.required && fieldValue.returned !== false) required.add(fieldKey);
187
190
  });
188
191
  Object.entries(properties).forEach(([key, prop]) => {
189
192
  const field = value.fields[key];
@@ -192,7 +195,7 @@ async function generator(ctx, options) {
192
195
  acc[modelName] = {
193
196
  type: "object",
194
197
  properties,
195
- required
198
+ required: Array.from(required)
196
199
  };
197
200
  return acc;
198
201
  }, {}) } };
@@ -536,6 +536,28 @@ declare const getOrgAdapter: <O extends OrganizationOptions>(context: AuthContex
536
536
  userId: string;
537
537
  createdAt: Date;
538
538
  }>;
539
+ /**
540
+ * Adds a user to a team only when the team is below its member limit,
541
+ * reading the count and creating the membership in one transaction.
542
+ * Returns the existing membership unchanged (no capacity charge) when the
543
+ * user already belongs to the team.
544
+ *
545
+ * FIXME(team-cap-race): the count-then-create is not atomic under READ
546
+ * COMMITTED, so two concurrent adds can both pass the count check and
547
+ * exceed maximumMembersPerTeam. A durable fix needs a unique constraint on
548
+ * teamMember(teamId, userId) or serializable isolation. Affects every
549
+ * caller (acceptInvitation, addMember, addTeamMember).
550
+ */
551
+ addTeamMemberWithLimit: (data: {
552
+ teamId: string;
553
+ userId: string;
554
+ maximumMembersPerTeam: number;
555
+ }) => Promise<{
556
+ status: "added";
557
+ member: TeamMember;
558
+ } | {
559
+ status: "limitReached";
560
+ }>;
539
561
  removeTeamMember: (data: {
540
562
  teamId: string;
541
563
  userId: string;
@@ -757,7 +779,13 @@ declare const getOrgAdapter: <O extends OrganizationOptions>(context: AuthContex
757
779
  } ? FieldAttributeToObject<Field> : {}) extends infer T ? { [K in keyof T]: T[K] } : never)[]>;
758
780
  updateInvitation: (data: {
759
781
  invitationId: string;
760
- status: "accepted" | "canceled" | "rejected";
782
+ status: "pending" | "accepted" | "canceled" | "rejected";
783
+ /**
784
+ * Only transition when the invitation is currently in this status. The
785
+ * guarded update is atomic, so a concurrent caller racing the same
786
+ * transition gets `null` instead of both proceeding.
787
+ */
788
+ fromStatus?: "pending";
761
789
  }) => Promise<((O["teams"] extends {
762
790
  enabled: true;
763
791
  } ? {
@@ -4,6 +4,23 @@ 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
7
+ /**
8
+ * Resolves the configured per-team member cap to a concrete number for a given
9
+ * team-add. Returns `undefined` only when no cap is configured. Throws when the
10
+ * cap is a function but no session is available to evaluate it, so a sessionless
11
+ * server-side add fails closed instead of silently bypassing the limit.
12
+ */
13
+ async function resolveMaximumMembersPerTeam(teams, context) {
14
+ const maximumMembersPerTeam = teams?.maximumMembersPerTeam;
15
+ if (maximumMembersPerTeam === void 0) return void 0;
16
+ if (typeof maximumMembersPerTeam === "number") return maximumMembersPerTeam;
17
+ if (!context.session) throw new BetterAuthError("`teams.maximumMembersPerTeam` is configured as a function but no session is available to evaluate it. Provide a session-bearing request or configure a numeric limit.");
18
+ return await maximumMembersPerTeam({
19
+ teamId: context.teamId,
20
+ session: context.session,
21
+ organizationId: context.organizationId
22
+ });
23
+ }
7
24
  const getOrgAdapter = (context, options) => {
8
25
  const baseAdapter = context.adapter;
9
26
  const orgAdditionalFields = options?.schema?.organization?.additionalFields;
@@ -513,6 +530,43 @@ const getOrgAdapter = (context, options) => {
513
530
  }
514
531
  });
515
532
  },
533
+ addTeamMemberWithLimit: async (data) => {
534
+ return runWithTransaction(baseAdapter, async () => {
535
+ const adapter = await getCurrentAdapter(baseAdapter);
536
+ const existing = await adapter.findOne({
537
+ model: "teamMember",
538
+ where: [{
539
+ field: "teamId",
540
+ value: data.teamId
541
+ }, {
542
+ field: "userId",
543
+ value: data.userId
544
+ }]
545
+ });
546
+ if (existing) return {
547
+ status: "added",
548
+ member: existing
549
+ };
550
+ if (await adapter.count({
551
+ model: "teamMember",
552
+ where: [{
553
+ field: "teamId",
554
+ value: data.teamId
555
+ }]
556
+ }) >= data.maximumMembersPerTeam) return { status: "limitReached" };
557
+ return {
558
+ status: "added",
559
+ member: await adapter.create({
560
+ model: "teamMember",
561
+ data: {
562
+ teamId: data.teamId,
563
+ userId: data.userId,
564
+ createdAt: /* @__PURE__ */ new Date()
565
+ }
566
+ })
567
+ };
568
+ });
569
+ },
516
570
  removeTeamMember: async (data) => {
517
571
  await (await getCurrentAdapter(baseAdapter)).deleteMany({
518
572
  model: "teamMember",
@@ -615,16 +669,22 @@ const getOrgAdapter = (context, options) => {
615
669
  });
616
670
  },
617
671
  updateInvitation: async (data) => {
618
- return await (await getCurrentAdapter(baseAdapter)).update({
672
+ const adapter = await getCurrentAdapter(baseAdapter);
673
+ const where = [{
674
+ field: "id",
675
+ value: data.invitationId
676
+ }];
677
+ if (data.fromStatus) where.push({
678
+ field: "status",
679
+ value: data.fromStatus
680
+ });
681
+ return await adapter.update({
619
682
  model: "invitation",
620
- where: [{
621
- field: "id",
622
- value: data.invitationId
623
- }],
683
+ where,
624
684
  update: { status: data.status }
625
685
  });
626
686
  }
627
687
  };
628
688
  };
629
689
  //#endregion
630
- export { getOrgAdapter };
690
+ export { getOrgAdapter, resolveMaximumMembersPerTeam };
@@ -394,11 +394,13 @@ function organization(options) {
394
394
  activeOrganizationId: {
395
395
  type: "string",
396
396
  required: false,
397
+ input: false,
397
398
  fieldName: opts.schema?.session?.fields?.activeOrganizationId
398
399
  },
399
400
  ...teamSupport ? { activeTeamId: {
400
401
  type: "string",
401
402
  required: false,
403
+ input: false,
402
404
  fieldName: opts.schema?.session?.fields?.activeTeamId
403
405
  } } : {}
404
406
  } }
@@ -4,10 +4,11 @@ import { toZodSchema } from "../../../db/to-zod.mjs";
4
4
  import { getSessionFromCtx } from "../../../api/routes/session.mjs";
5
5
  import { defaultRoles } from "../access/statement.mjs";
6
6
  import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
7
- import { getOrgAdapter } from "../adapter.mjs";
7
+ import { getOrgAdapter, resolveMaximumMembersPerTeam } from "../adapter.mjs";
8
8
  import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
9
9
  import { hasPermission } from "../has-permission.mjs";
10
10
  import { parseRoles } from "../organization.mjs";
11
+ import { runWithTransaction } from "@better-auth/core/context";
11
12
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
12
13
  import { createAuthEndpoint } from "@better-auth/core/api";
13
14
  import * as z from "zod";
@@ -170,7 +171,12 @@ const createInvitation = (option) => {
170
171
  }, ctx.context) : ctx.context.orgOptions.invitationLimit ?? 100;
171
172
  if ((await adapter.findPendingInvitations({ organizationId })).length >= invitationLimit) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED);
172
173
  if (ctx.context.orgOptions.teams?.enabled && "teamId" in ctx.body && ctx.body.teamId) {
173
- 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);
174
+ const requestedTeamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
175
+ if (requestedTeamIds.some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID);
176
+ for (const teamId of requestedTeamIds) if (!await adapter.findTeamById({
177
+ teamId,
178
+ organizationId
179
+ })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
174
180
  }
175
181
  if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined" && "teamId" in ctx.body && ctx.body.teamId) {
176
182
  const teamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
@@ -278,40 +284,58 @@ const acceptInvitation = (options) => createAuthEndpoint("/organization/accept-i
278
284
  });
279
285
  const acceptedI = await adapter.updateInvitation({
280
286
  invitationId: ctx.body.invitationId,
281
- status: "accepted"
287
+ status: "accepted",
288
+ fromStatus: "pending"
282
289
  });
283
- if (!acceptedI) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.FAILED_TO_RETRIEVE_INVITATION);
284
- if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && "teamId" in acceptedI && acceptedI.teamId) {
285
- const teamIds = acceptedI.teamId.split(",");
286
- const onlyOne = teamIds.length === 1;
287
- for (const teamId of teamIds) {
288
- await adapter.findOrCreateTeamMember({
289
- teamId,
290
- userId: session.user.id
291
- });
292
- if (typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined") {
293
- if (await adapter.countTeamMembers({ teamId }) >= (typeof ctx.context.orgOptions.teams.maximumMembersPerTeam === "function" ? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
290
+ if (!acceptedI) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND);
291
+ const member = await runWithTransaction(ctx.context.adapter, async () => {
292
+ if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && "teamId" in acceptedI && acceptedI.teamId) {
293
+ const teamIds = acceptedI.teamId.split(",");
294
+ const onlyOne = teamIds.length === 1;
295
+ for (const teamId of teamIds) {
296
+ if (!await adapter.findTeamById({
294
297
  teamId,
295
- session,
296
- organizationId: invitation.organizationId
297
- }) : ctx.context.orgOptions.teams.maximumMembersPerTeam)) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
298
+ organizationId: acceptedI.organizationId
299
+ })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
300
+ const maximumMembersPerTeam = await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, {
301
+ teamId,
302
+ organizationId: acceptedI.organizationId,
303
+ session
304
+ });
305
+ if (maximumMembersPerTeam !== void 0) {
306
+ if ((await adapter.addTeamMemberWithLimit({
307
+ teamId,
308
+ userId: session.user.id,
309
+ maximumMembersPerTeam
310
+ })).status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
311
+ } else await adapter.findOrCreateTeamMember({
312
+ teamId,
313
+ userId: session.user.id
314
+ });
315
+ }
316
+ if (onlyOne) {
317
+ const teamId = teamIds[0];
318
+ await setSessionCookie(ctx, {
319
+ session: await adapter.setActiveTeam(session.session.token, teamId, ctx),
320
+ user: session.user
321
+ });
298
322
  }
299
323
  }
300
- if (onlyOne) {
301
- const teamId = teamIds[0];
302
- await setSessionCookie(ctx, {
303
- session: await adapter.setActiveTeam(session.session.token, teamId, ctx),
304
- user: session.user
305
- });
306
- }
307
- }
308
- const member = await adapter.createMember({
309
- organizationId: invitation.organizationId,
310
- userId: session.user.id,
311
- role: invitation.role,
312
- createdAt: /* @__PURE__ */ new Date()
324
+ const createdMember = await adapter.createMember({
325
+ organizationId: acceptedI.organizationId,
326
+ userId: session.user.id,
327
+ role: acceptedI.role,
328
+ createdAt: /* @__PURE__ */ new Date()
329
+ });
330
+ await adapter.setActiveOrganization(session.session.token, acceptedI.organizationId, ctx);
331
+ return createdMember;
332
+ }).catch(async (error) => {
333
+ await adapter.updateInvitation({
334
+ invitationId: ctx.body.invitationId,
335
+ status: "pending"
336
+ });
337
+ throw error;
313
338
  });
314
- await adapter.setActiveOrganization(session.session.token, invitation.organizationId, ctx);
315
339
  if (options?.organizationHooks?.afterAcceptInvitation) await options?.organizationHooks.afterAcceptInvitation({
316
340
  invitation: acceptedI,
317
341
  member,
@@ -1,7 +1,8 @@
1
1
  import { toZodSchema } from "../../../db/to-zod.mjs";
2
2
  import { getSessionFromCtx, sessionMiddleware } from "../../../api/routes/session.mjs";
3
+ import { defaultRoles } from "../access/statement.mjs";
3
4
  import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
4
- import { getOrgAdapter } from "../adapter.mjs";
5
+ import { getOrgAdapter, resolveMaximumMembersPerTeam } from "../adapter.mjs";
5
6
  import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
6
7
  import { hasPermission } from "../has-permission.mjs";
7
8
  import { parseRoles } from "../organization.mjs";
@@ -21,7 +22,7 @@ const addMember = (option) => {
21
22
  fields: option?.schema?.member?.additionalFields || {},
22
23
  isClientSide: true
23
24
  });
24
- return createAuthEndpoint({
25
+ return createAuthEndpoint.serverOnly({
25
26
  method: "POST",
26
27
  body: z.object({
27
28
  ...baseMemberSchema.shape,
@@ -87,11 +88,22 @@ const addMember = (option) => {
87
88
  ...response.data
88
89
  };
89
90
  }
90
- const createdMember = await adapter.createMember(memberData);
91
- if (teamId) await adapter.findOrCreateTeamMember({
91
+ const maximumMembersPerTeam = teamId ? await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, {
92
+ teamId,
93
+ organizationId: orgId,
94
+ session
95
+ }) : void 0;
96
+ if (teamId) if (maximumMembersPerTeam !== void 0) {
97
+ if ((await adapter.addTeamMemberWithLimit({
98
+ teamId,
99
+ userId: user.id,
100
+ maximumMembersPerTeam
101
+ })).status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
102
+ } else await adapter.findOrCreateTeamMember({
92
103
  userId: user.id,
93
104
  teamId
94
105
  });
106
+ const createdMember = await adapter.createMember(memberData);
95
107
  if (option?.organizationHooks?.afterAddMember) await option?.organizationHooks.afterAddMember({
96
108
  member: createdMember,
97
109
  user,
@@ -241,7 +253,31 @@ const updateMemberRole = (option) => createAuthEndpoint("/organization/update-me
241
253
  const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
242
254
  if (!organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
243
255
  const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
244
- const roleToSet = Array.isArray(ctx.body.role) ? ctx.body.role : ctx.body.role ? [ctx.body.role] : [];
256
+ const roleToSet = (Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role]).flatMap((role) => role.split(",")).map((role) => role.trim()).filter(Boolean);
257
+ if (roleToSet.length === 0) throw APIError.fromStatus("BAD_REQUEST");
258
+ const validStaticRoles = new Set([...Object.keys(defaultRoles), ...Object.keys(ctx.context.orgOptions.roles || {})]);
259
+ const unknownRoles = roleToSet.filter((role) => !validStaticRoles.has(role));
260
+ if (unknownRoles.length > 0) if (ctx.context.orgOptions.dynamicAccessControl?.enabled) {
261
+ const foundRoleNames = (await ctx.context.adapter.findMany({
262
+ model: "organizationRole",
263
+ where: [{
264
+ field: "organizationId",
265
+ value: organizationId
266
+ }, {
267
+ field: "role",
268
+ value: unknownRoles,
269
+ operator: "in"
270
+ }]
271
+ })).map((r) => r.role);
272
+ const stillInvalid = unknownRoles.filter((r) => !foundRoleNames.includes(r));
273
+ if (stillInvalid.length > 0) throw new APIError("BAD_REQUEST", {
274
+ code: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code,
275
+ message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code}: ${stillInvalid.join(", ")}`
276
+ });
277
+ } else throw new APIError("BAD_REQUEST", {
278
+ code: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code,
279
+ message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code}: ${unknownRoles.join(", ")}`
280
+ });
245
281
  const member = await adapter.findMemberByOrgId({
246
282
  userId: session.user.id,
247
283
  organizationId
@@ -280,7 +316,7 @@ const updateMemberRole = (option) => createAuthEndpoint("/organization/update-me
280
316
  const userBeingUpdated = await ctx.context.internalAdapter.findUserById(toBeUpdatedMember.userId);
281
317
  if (!userBeingUpdated) throw APIError.fromStatus("BAD_REQUEST", { message: "User not found" });
282
318
  const previousRole = toBeUpdatedMember.role;
283
- const newRole = parseRoles(ctx.body.role);
319
+ const newRole = parseRoles(roleToSet);
284
320
  if (option?.organizationHooks?.beforeUpdateMemberRole) {
285
321
  const response = await option?.organizationHooks.beforeUpdateMemberRole({
286
322
  member: toBeUpdatedMember,
@@ -2,10 +2,11 @@ import { setSessionCookie } from "../../../cookies/index.mjs";
2
2
  import { toZodSchema } from "../../../db/to-zod.mjs";
3
3
  import { getSessionFromCtx } from "../../../api/routes/session.mjs";
4
4
  import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
5
- import { getOrgAdapter } from "../adapter.mjs";
5
+ import { getOrgAdapter, resolveMaximumMembersPerTeam } from "../adapter.mjs";
6
6
  import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
7
7
  import { hasPermission } from "../has-permission.mjs";
8
8
  import { teamSchema } from "../schema.mjs";
9
+ import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
9
10
  import { APIError } from "@better-auth/core/error";
10
11
  import { createAuthEndpoint } from "@better-auth/core/api";
11
12
  import * as z from "zod";
@@ -185,7 +186,25 @@ const removeTeam = (options) => createAuthEndpoint("/organization/remove-team",
185
186
  user: session?.user,
186
187
  organization
187
188
  });
188
- await adapter.deleteTeam(team.id);
189
+ await runWithTransaction(ctx.context.adapter, async () => {
190
+ await adapter.deleteTeam(team.id);
191
+ const pendingInvitations = await adapter.findPendingInvitations({ organizationId });
192
+ const trx = await getCurrentAdapter(ctx.context.adapter);
193
+ for (const invitation of pendingInvitations) {
194
+ if (!("teamId" in invitation) || !invitation.teamId) continue;
195
+ const teamIds = invitation.teamId.split(",");
196
+ if (!teamIds.includes(team.id)) continue;
197
+ const remainingTeamIds = teamIds.filter((id) => id !== team.id);
198
+ await trx.update({
199
+ model: "invitation",
200
+ where: [{
201
+ field: "id",
202
+ value: invitation.id
203
+ }],
204
+ update: { teamId: remainingTeamIds.length > 0 ? remainingTeamIds.join(",") : null }
205
+ });
206
+ }
207
+ });
189
208
  if (options?.organizationHooks?.afterDeleteTeam) await options?.organizationHooks.afterDeleteTeam({
190
209
  team,
191
210
  user: session?.user,
@@ -441,8 +460,15 @@ const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-t
441
460
  use: [orgMiddleware, orgSessionMiddleware]
442
461
  }, async (ctx) => {
443
462
  const session = ctx.context.session;
444
- const teams = await getOrgAdapter(ctx.context, ctx.context.orgOptions).listTeamsByUser({ userId: session.user.id });
445
- return ctx.json(teams);
463
+ const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
464
+ const teams = await adapter.listTeamsByUser({ userId: session.user.id });
465
+ const orgIds = [...new Set(teams.map((team) => team.organizationId))];
466
+ const memberships = await Promise.all(orgIds.map((organizationId) => adapter.checkMembership({
467
+ userId: session.user.id,
468
+ organizationId
469
+ })));
470
+ const memberOrgIds = new Set(orgIds.filter((_, index) => memberships[index]));
471
+ return ctx.json(teams.filter((team) => memberOrgIds.has(team.organizationId)));
446
472
  });
447
473
  const listTeamMembersQuerySchema = z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) }));
448
474
  const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team-members", {
@@ -494,6 +520,12 @@ const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team
494
520
  const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
495
521
  const teamId = ctx.query?.teamId || session?.session.activeTeamId;
496
522
  if (!teamId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM);
523
+ const team = await adapter.findTeamById({ teamId });
524
+ if (!team) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
525
+ if (!await adapter.checkMembership({
526
+ userId: session.user.id,
527
+ organizationId: team.organizationId
528
+ })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM);
497
529
  if (!await adapter.findTeamMember({
498
530
  userId: session.user.id,
499
531
  teamId
@@ -587,7 +619,21 @@ const addTeamMember = (options) => createAuthEndpoint("/organization/add-team-me
587
619
  });
588
620
  if (response && typeof response === "object" && "data" in response) {}
589
621
  }
590
- const teamMember = await adapter.findOrCreateTeamMember({
622
+ const maximumMembersPerTeam = await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, {
623
+ teamId: ctx.body.teamId,
624
+ organizationId,
625
+ session
626
+ });
627
+ let teamMember;
628
+ if (maximumMembersPerTeam !== void 0) {
629
+ const result = await adapter.addTeamMemberWithLimit({
630
+ teamId: ctx.body.teamId,
631
+ userId: ctx.body.userId,
632
+ maximumMembersPerTeam
633
+ });
634
+ if (result.status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
635
+ teamMember = result.member;
636
+ } else teamMember = await adapter.findOrCreateTeamMember({
591
637
  teamId: ctx.body.teamId,
592
638
  userId: ctx.body.userId
593
639
  });
@@ -183,6 +183,7 @@ interface SessionDefaultFields {
183
183
  activeOrganizationId: {
184
184
  type: "string";
185
185
  required: false;
186
+ input: false;
186
187
  };
187
188
  }
188
189
  type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessControl"] extends {
@@ -222,6 +223,7 @@ type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessContro
222
223
  activeTeamId: {
223
224
  type: "string";
224
225
  required: false;
226
+ input: false;
225
227
  };
226
228
  } : {});
227
229
  };