fix(organization): split invitation verification gates (#9877)

This commit is contained in:
Gustavo Valverde
2026-06-02 16:45:44 -04:00
committed by GitHub
parent 5a2d642bc7
commit 2d9781a83d
6 changed files with 337 additions and 152 deletions
@@ -0,0 +1,7 @@
---
"better-auth": patch
---
Restore the normal emailed-invitation flow while documenting the stricter verification posture for organization invitations.
Client-side `listUserInvitations` now always requires a verified session email because it enumerates invitation IDs from `session.user.email`. The `requireEmailVerificationOnInvitation` option now controls recipient calls that carry an invitation ID (`acceptInvitation`, `rejectInvitation`, `getInvitation`). When unset, Better Auth keeps the emailed-invitation sign-up flow for built-in opaque invitation IDs, including the default generator or `advanced.database.generateId: "uuid"`, and requires verified email when invitation IDs are externally controlled or predictable, such as `advanced.database.generateId: "serial"` / `false` or custom ID generation. Apps that expose invitation IDs outside the invited user's mailbox, expose organization invitation lists to members, or require stricter ownership proof should set `requireEmailVerificationOnInvitation: true` or require verified email before sign-in.
+16 -7
View File
@@ -924,7 +924,9 @@ Make sure to call the `acceptInvitation` function after the user is logged in.
#### Email Verification Requirement
If the `requireEmailVerificationOnInvitation` option is enabled in your organization configuration, users must verify their email address before they can accept invitations. This adds an extra security layer to ensure that only verified users can join your organization.
By default, accepting an invitation requires the invitation ID from the email link and a logged-in session whose email matches the invitation. When Better Auth uses built-in opaque invitation IDs, including the default generator or `advanced.database.generateId: "uuid"`, that remains enough for the normal emailed-invitation flow. If invitation IDs are externally controlled or predictable, such as `advanced.database.generateId: "serial"` / `false` or custom ID generation, Better Auth also requires verified email unless you explicitly set `requireEmailVerificationOnInvitation` to `false`.
Set `requireEmailVerificationOnInvitation` to `true` for the stricter posture. This is recommended when invitation IDs can be visible outside the invited user's mailbox, when your app exposes organization invitation lists to members, when you use custom invitation delivery, or when unverified email/password sessions are allowed and organization membership is sensitive. Requiring verified email before sign-in provides the same ownership proof earlier in the flow.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
@@ -975,10 +977,8 @@ If this user has received an invitation, but wants to decline it, this method wi
</APIMethod>
<Callout type="info">
Like accepting invitations, rejecting invitations also requires email
verification when the `requireEmailVerificationOnInvitation` option is
enabled. Users with unverified emails will receive an error when attempting to
reject invitations.
Rejecting invitations follows the same email-verification policy as accepting
invitations by ID.
</Callout>
### Get Invitation
@@ -996,6 +996,11 @@ To get an invitation you can use the `organization.getInvitation` function provi
```
</APIMethod>
<Callout type="info">
Getting an invitation by ID follows the same email-verification policy as
accepting invitations by ID.
</Callout>
### List Invitations
To list all invitations for a given organization you can use the `listInvitations` function provided by the client.
@@ -1011,6 +1016,8 @@ To list all invitations for a given organization you can use the `listInvitation
```
</APIMethod>
The response includes invitation IDs. Treat those IDs as action-capable invitation links. If users who can list organization invitations should not be able to use those IDs with unverified recipient sessions, enable `requireEmailVerificationOnInvitation` or add app-level permission checks around this endpoint.
### List user invitations
To list all invitations for a given user you can use the `listUserInvitations` function provided by the client.
@@ -1021,7 +1028,9 @@ import { authClient } from "@/lib/auth-client"
const invitations = await authClient.organization.listUserInvitations();
```
On the server, you can pass the user ID as a query parameter.
Client-side `listUserInvitations` calls require the session user's email to be verified. This endpoint enumerates pending invitation IDs from the session email, so email string matching alone is not enough ownership proof.
On the server, you can pass the user's email address as a query parameter.
```ts title="list-user-invitations.ts"
const invitations = await auth.api.listUserInvitations({
@@ -2515,4 +2524,4 @@ await client.organization.create({
**invitationLimit**: `number` | `((user: User) => Promise<boolean> | boolean)` - The maximum number of invitations allowed for a user. By default, it's `100`. You can set it to any number you want or a function that returns a boolean.
**requireEmailVerificationOnInvitation**: `boolean` - Whether to require email verification before accepting or rejecting invitations. By default, it's `false`. When enabled, users must have verified their email address before they can accept or reject organization invitations.
**requireEmailVerificationOnInvitation**: `boolean | undefined` - Whether to require email verification before recipient invitation calls that carry an invitation ID (`acceptInvitation`, `rejectInvitation`, `getInvitation`). When unset, Better Auth preserves the normal emailed-invitation flow for built-in opaque invitation IDs, including the default generator and `advanced.database.generateId: "uuid"`. It requires verification for externally controlled or predictable invitation IDs, such as `advanced.database.generateId: "serial"` / `false` or custom ID generation. Set this option to `true` when invitation IDs may be visible outside the invited user's mailbox, when organization invitation lists are exposed to members, or when you want verified email to be the ownership proof for by-ID invitation actions. Client-side `listUserInvitations` always requires a verified session email because it enumerates invitation IDs from the session email.
@@ -959,12 +959,14 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
options?.invitationExpiresIn || defaultExpiration,
"sec",
);
const invitationId = context.generateId({ model: "invitation" });
const invite = await adapter.create<
InvitationInput,
InferInvitation<O, false>
>({
model: "invitation",
data: {
...(invitationId !== false ? { id: invitationId } : {}),
status: "pending",
expiresAt,
createdAt: new Date(),
@@ -1,18 +1,56 @@
import type { BetterAuthOptions, GenerateIdFn } from "@better-auth/core";
import { describe, expect, it } from "vitest";
import { getTestInstance } from "../../../test-utils/test-instance";
import { organizationClient } from "../client";
import { organization } from "../organization";
import type { OrganizationOptions } from "../types";
/**
* @see https://github.com/better-auth/better-auth/security/advisories/GHSA-fmh4-wcc4-5jm3
*/
describe("organization invitation recipient gate requires emailVerified", async () => {
describe("organization invitation recipient ownership gates", async () => {
const VICTIM_EMAIL = "victim@target.example";
const ATTACKER_PASSWORD = "attacker-password-123";
async function setupInvite() {
type SetupInviteOptions = {
authOptions?: Partial<BetterAuthOptions>;
organizationOptions?: OrganizationOptions;
};
type AuthOptionsWithAdvancedGenerateId = Partial<BetterAuthOptions> & {
advanced: NonNullable<Partial<BetterAuthOptions>["advanced"]> & {
generateId: GenerateIdFn;
};
};
const databaseOwnedIdAuthOptions = {
advanced: {
database: {
generateId: "serial",
},
},
} satisfies Partial<BetterAuthOptions>;
let customIdSequence = 0;
const customAdvancedIdAuthOptions = {
advanced: {
cookies: {},
generateId: ({ model }) => `${model}-custom-id-${customIdSequence++}`,
},
} satisfies AuthOptionsWithAdvancedGenerateId;
async function setupInvite({
authOptions,
organizationOptions,
}: SetupInviteOptions = {}) {
const helpers = await getTestInstance(
{ plugins: [organization()] },
{
...authOptions,
plugins: [
organization(organizationOptions),
...(authOptions?.plugins ?? []),
],
},
{ clientOptions: { plugins: [organizationClient()] } },
);
const { client, signInWithTestUser, cookieSetter } = helpers;
@@ -35,18 +73,18 @@ describe("organization invitation recipient gate requires emailVerified", async
...helpers,
adminHeaders,
orgId: org.data!.id,
invitationId: invite.data!.id!,
invitationId: String(invite.data!.id!),
};
}
async function signUpUnverifiedVictim(
async function signUpUnverifiedRecipient(
client: Awaited<ReturnType<typeof setupInvite>>["client"],
signInWithUser: Awaited<ReturnType<typeof setupInvite>>["signInWithUser"],
) {
await client.signUp.email({
email: VICTIM_EMAIL,
password: ATTACKER_PASSWORD,
name: "attacker",
name: "recipient",
});
const { headers, res } = await signInWithUser(
VICTIM_EMAIL,
@@ -57,64 +95,63 @@ describe("organization invitation recipient gate requires emailVerified", async
return headers;
}
it("rejects acceptInvitation from an unverified session that matches the invitation email", async () => {
it("accepts an invitation by ID from an unverified matching session by default", async () => {
const { client, signInWithUser, invitationId, auth, adminHeaders } =
await setupInvite();
const attackerHeaders = await signUpUnverifiedVictim(
const recipientHeaders = await signUpUnverifiedRecipient(
client,
signInWithUser,
);
const accept = await client.organization.acceptInvitation({
invitationId,
fetchOptions: { headers: attackerHeaders },
fetchOptions: { headers: recipientHeaders },
});
expect(accept.data).toBeNull();
expect(accept.error?.status).toBe(403);
expect(accept.error).toBeNull();
expect(accept.data?.invitation?.status).toBe("accepted");
const orgAfter = await auth.api.getFullOrganization({
headers: adminHeaders,
});
const memberEmails = (orgAfter?.members ?? []).map((m) => m.user.email);
expect(memberEmails).not.toContain(VICTIM_EMAIL);
expect(memberEmails).toContain(VICTIM_EMAIL);
});
it("rejects rejectInvitation from an unverified session that matches the invitation email", async () => {
it("marks an invitation rejected by ID from an unverified matching session by default", async () => {
const { client, signInWithUser, invitationId } = await setupInvite();
const attackerHeaders = await signUpUnverifiedVictim(
const recipientHeaders = await signUpUnverifiedRecipient(
client,
signInWithUser,
);
const reject = await client.organization.rejectInvitation({
invitationId,
fetchOptions: { headers: attackerHeaders },
fetchOptions: { headers: recipientHeaders },
});
expect(reject.data).toBeNull();
expect(reject.error?.status).toBe(403);
expect(reject.error).toBeNull();
});
it("rejects getInvitation from an unverified session that matches the invitation email", async () => {
it("gets an invitation by ID from an unverified matching session by default", async () => {
const { client, signInWithUser, invitationId } = await setupInvite();
const attackerHeaders = await signUpUnverifiedVictim(
const recipientHeaders = await signUpUnverifiedRecipient(
client,
signInWithUser,
);
const got = await client.organization.getInvitation({
query: { id: invitationId },
fetchOptions: { headers: attackerHeaders },
fetchOptions: { headers: recipientHeaders },
});
expect(got.data).toBeNull();
expect(got.error?.status).toBe(403);
expect(got.error).toBeNull();
expect(got.data?.email).toBe(VICTIM_EMAIL);
});
it("rejects listUserInvitations from an unverified session", async () => {
const { client, signInWithUser } = await setupInvite();
const attackerHeaders = await signUpUnverifiedVictim(
const attackerHeaders = await signUpUnverifiedRecipient(
client,
signInWithUser,
);
@@ -130,114 +167,172 @@ describe("organization invitation recipient gate requires emailVerified", async
/**
* @see https://github.com/better-auth/better-auth/security/advisories/GHSA-fmh4-wcc4-5jm3
*/
it("respects requireEmailVerificationOnInvitation: false (legacy permissive opt-out)", async () => {
const helpers = await getTestInstance(
{
plugins: [
organization({ requireEmailVerificationOnInvitation: false }),
],
},
{ clientOptions: { plugins: [organizationClient()] } },
);
const { client, signInWithTestUser, signInWithUser, cookieSetter } =
helpers;
const { headers: adminHeaders } = await signInWithTestUser();
const org = await client.organization.create({
name: "Permissive Org",
slug: "permissive-org",
fetchOptions: {
headers: adminHeaders,
onSuccess: cookieSetter(adminHeaders),
it("keeps listUserInvitations gated when invitation ID verification is disabled", async () => {
const { client, signInWithUser } = await setupInvite({
organizationOptions: {
requireEmailVerificationOnInvitation: false,
},
});
const invite = await client.organization.inviteMember({
organizationId: org.data!.id,
email: VICTIM_EMAIL,
role: "member",
fetchOptions: { headers: adminHeaders },
});
await client.signUp.email({
email: VICTIM_EMAIL,
password: ATTACKER_PASSWORD,
name: "unverified",
});
const { headers: unverifiedHeaders } = await signInWithUser(
VICTIM_EMAIL,
ATTACKER_PASSWORD,
const attackerHeaders = await signUpUnverifiedRecipient(
client,
signInWithUser,
);
// Read-side endpoints should also ignore the gate when opted out so the
// option is consistent across the four documented recipient calls.
const got = await client.organization.getInvitation({
query: { id: invite.data!.id! },
fetchOptions: { headers: unverifiedHeaders },
});
expect(got.error).toBeNull();
expect(got.data?.email).toBe(VICTIM_EMAIL);
const list = await client.organization.listUserInvitations({
fetchOptions: { headers: unverifiedHeaders },
fetchOptions: { headers: attackerHeaders },
});
expect(list.error).toBeNull();
expect(list.data?.length ?? 0).toBeGreaterThanOrEqual(1);
expect(list.data).toBeNull();
expect(list.error?.status).toBe(403);
});
it("requires verified email for invitation ID calls when explicitly enabled", async () => {
const acceptSetup = await setupInvite({
organizationOptions: {
requireEmailVerificationOnInvitation: true,
},
});
const acceptHeaders = await signUpUnverifiedRecipient(
acceptSetup.client,
acceptSetup.signInWithUser,
);
const accept = await acceptSetup.client.organization.acceptInvitation({
invitationId: acceptSetup.invitationId,
fetchOptions: { headers: acceptHeaders },
});
expect(accept.data).toBeNull();
expect(accept.error?.status).toBe(403);
const getSetup = await setupInvite({
organizationOptions: {
requireEmailVerificationOnInvitation: true,
},
});
const getHeaders = await signUpUnverifiedRecipient(
getSetup.client,
getSetup.signInWithUser,
);
const got = await getSetup.client.organization.getInvitation({
query: { id: getSetup.invitationId },
fetchOptions: { headers: getHeaders },
});
expect(got.data).toBeNull();
expect(got.error?.status).toBe(403);
const rejectSetup = await setupInvite({
organizationOptions: {
requireEmailVerificationOnInvitation: true,
},
});
const rejectHeaders = await signUpUnverifiedRecipient(
rejectSetup.client,
rejectSetup.signInWithUser,
);
const reject = await rejectSetup.client.organization.rejectInvitation({
invitationId: rejectSetup.invitationId,
fetchOptions: { headers: rejectHeaders },
});
expect(reject.data).toBeNull();
expect(reject.error?.status).toBe(403);
});
it("requires verified email for invitation ID calls when the database owns IDs", async () => {
const acceptSetup = await setupInvite({
authOptions: databaseOwnedIdAuthOptions,
});
const acceptHeaders = await signUpUnverifiedRecipient(
acceptSetup.client,
acceptSetup.signInWithUser,
);
const accept = await acceptSetup.client.organization.acceptInvitation({
invitationId: acceptSetup.invitationId,
fetchOptions: { headers: acceptHeaders },
});
expect(accept.data).toBeNull();
expect(accept.error?.status).toBe(403);
const getSetup = await setupInvite({
authOptions: databaseOwnedIdAuthOptions,
});
const getHeaders = await signUpUnverifiedRecipient(
getSetup.client,
getSetup.signInWithUser,
);
const got = await getSetup.client.organization.getInvitation({
query: { id: getSetup.invitationId },
fetchOptions: { headers: getHeaders },
});
expect(got.data).toBeNull();
expect(got.error?.status).toBe(403);
const rejectSetup = await setupInvite({
authOptions: databaseOwnedIdAuthOptions,
});
const rejectHeaders = await signUpUnverifiedRecipient(
rejectSetup.client,
rejectSetup.signInWithUser,
);
const reject = await rejectSetup.client.organization.rejectInvitation({
invitationId: rejectSetup.invitationId,
fetchOptions: { headers: rejectHeaders },
});
expect(reject.data).toBeNull();
expect(reject.error?.status).toBe(403);
});
it("requires verified email for invitation ID calls when advanced generateId is custom", async () => {
const { client, signInWithUser, invitationId } = await setupInvite({
authOptions: customAdvancedIdAuthOptions,
});
const recipientHeaders = await signUpUnverifiedRecipient(
client,
signInWithUser,
);
const accept = await client.organization.acceptInvitation({
invitationId: invite.data!.id!,
fetchOptions: { headers: unverifiedHeaders },
invitationId,
fetchOptions: { headers: recipientHeaders },
});
expect(accept.data).toBeNull();
expect(accept.error?.status).toBe(403);
});
it("accepts an invitation by ID with database-owned IDs when verification is explicitly disabled", async () => {
const { client, signInWithUser, invitationId, auth, adminHeaders } =
await setupInvite({
authOptions: databaseOwnedIdAuthOptions,
organizationOptions: {
requireEmailVerificationOnInvitation: false,
},
});
const recipientHeaders = await signUpUnverifiedRecipient(
client,
signInWithUser,
);
const accept = await client.organization.acceptInvitation({
invitationId,
fetchOptions: { headers: recipientHeaders },
});
expect(accept.error).toBeNull();
expect(accept.data?.invitation?.status).toBe("accepted");
const orgAfter = await auth.api.getFullOrganization({
headers: adminHeaders,
});
const memberEmails = (orgAfter?.members ?? []).map((m) => m.user.email);
expect(memberEmails).toContain(VICTIM_EMAIL);
});
/**
* @see https://github.com/better-auth/better-auth/security/advisories/GHSA-fmh4-wcc4-5jm3
*/
it("respects opt-out on rejectInvitation", async () => {
const helpers = await getTestInstance(
{
plugins: [
organization({ requireEmailVerificationOnInvitation: false }),
],
},
{ clientOptions: { plugins: [organizationClient()] } },
);
const { client, signInWithTestUser, signInWithUser, cookieSetter } =
helpers;
const { headers: adminHeaders } = await signInWithTestUser();
const org = await client.organization.create({
name: "Permissive Reject Org",
slug: "permissive-reject",
fetchOptions: {
headers: adminHeaders,
onSuccess: cookieSetter(adminHeaders),
},
});
const invite = await client.organization.inviteMember({
organizationId: org.data!.id,
email: VICTIM_EMAIL,
role: "member",
fetchOptions: { headers: adminHeaders },
});
await client.signUp.email({
email: VICTIM_EMAIL,
password: ATTACKER_PASSWORD,
name: "unverified",
});
const { headers: unverifiedHeaders } = await signInWithUser(
VICTIM_EMAIL,
ATTACKER_PASSWORD,
);
const reject = await client.organization.rejectInvitation({
invitationId: invite.data!.id!,
fetchOptions: { headers: unverifiedHeaders },
});
expect(reject.error).toBeNull();
});
it("accepts the invitation once the recipient verifies their email", async () => {
it("accepts the invitation once the recipient verifies their email when verification is required", async () => {
const { client, signInWithUser, invitationId, auth, adminHeaders } =
await setupInvite();
await setupInvite({
organizationOptions: {
requireEmailVerificationOnInvitation: true,
},
});
await client.signUp.email({
email: VICTIM_EMAIL,
password: ATTACKER_PASSWORD,
@@ -253,6 +348,14 @@ describe("organization invitation recipient gate requires emailVerified", async
ATTACKER_PASSWORD,
);
const list = await client.organization.listUserInvitations({
fetchOptions: { headers: victimHeaders },
});
expect(list.error).toBeNull();
expect(
list.data?.some((invitation) => invitation.id === invitationId),
).toBe(true);
const accept = await client.organization.acceptInvitation({
invitationId,
fetchOptions: { headers: victimHeaders },
@@ -1,4 +1,4 @@
import type { LiteralString } from "@better-auth/core";
import type { GenerateIdFn, LiteralString } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
@@ -79,6 +79,54 @@ type OrganizationInvitationRole<O extends OrganizationOptions> =
| InferOrganizationRolesFromOption<O>
| DynamicOrganizationRole<O>;
type ConfiguredGenerateIdOption =
| GenerateIdFn
| false
| "serial"
| "uuid"
| undefined;
const getAdvancedGenerateId = (
advancedOptions: unknown,
): GenerateIdFn | undefined => {
if (typeof advancedOptions !== "object" || advancedOptions === null) {
return undefined;
}
const generateId = (advancedOptions as { generateId?: unknown }).generateId;
if (typeof generateId !== "function") {
return undefined;
}
return generateId as GenerateIdFn;
};
const hasBuiltInOpaqueInvitationIdGeneration = ({
advancedGenerateId,
databaseGenerateId,
}: {
advancedGenerateId: GenerateIdFn | undefined;
databaseGenerateId: ConfiguredGenerateIdOption;
}) =>
advancedGenerateId === undefined &&
(databaseGenerateId === undefined || databaseGenerateId === "uuid");
const shouldRequireVerifiedEmailForInvitationIdAction = ({
organizationOptions,
advancedGenerateId,
databaseGenerateId,
}: {
organizationOptions: OrganizationOptions;
advancedGenerateId: GenerateIdFn | undefined;
databaseGenerateId: ConfiguredGenerateIdOption;
}) => {
if (organizationOptions.requireEmailVerificationOnInvitation !== undefined) {
return organizationOptions.requireEmailVerificationOnInvitation;
}
return !hasBuiltInOpaqueInvitationIdGeneration({
advancedGenerateId,
databaseGenerateId,
});
};
export const createInvitation = <O extends OrganizationOptions>(option: O) => {
const additionalFieldsSchema = toZodSchema({
fields: option?.schema?.invitation?.additionalFields || {},
@@ -615,13 +663,15 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
);
}
// Email-string equality is not ownership proof: a session whose user.email
// matches the invitation but has not been verified must not be treated as
// the invitation recipient. Gate is on by default; apps that intentionally
// allow unverified accept can opt out with `requireEmailVerificationOnInvitation: false`.
// FIXME(next-minor): drop the option and make the gate unconditional.
if (
(ctx.context.orgOptions.requireEmailVerificationOnInvitation ?? true) &&
shouldRequireVerifiedEmailForInvitationIdAction({
organizationOptions: ctx.context.orgOptions,
advancedGenerateId: getAdvancedGenerateId(
ctx.context.options.advanced,
),
databaseGenerateId:
ctx.context.options.advanced?.database?.generateId,
}) &&
!session.user.emailVerified
) {
throw APIError.from(
@@ -818,9 +868,15 @@ export const rejectInvitation = <O extends OrganizationOptions>(options: O) =>
);
}
// FIXME(next-minor): drop the option and make the gate unconditional.
if (
(ctx.context.orgOptions.requireEmailVerificationOnInvitation ?? true) &&
shouldRequireVerifiedEmailForInvitationIdAction({
organizationOptions: ctx.context.orgOptions,
advancedGenerateId: getAdvancedGenerateId(
ctx.context.options.advanced,
),
databaseGenerateId:
ctx.context.options.advanced?.database?.generateId,
}) &&
!session.user.emailVerified
) {
throw APIError.from(
@@ -1083,9 +1139,15 @@ export const getInvitation = <O extends OrganizationOptions>(options: O) =>
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
);
}
// FIXME(next-minor): drop the option and make the gate unconditional.
if (
(ctx.context.orgOptions.requireEmailVerificationOnInvitation ?? true) &&
shouldRequireVerifiedEmailForInvitationIdAction({
organizationOptions: ctx.context.orgOptions,
advancedGenerateId: getAdvancedGenerateId(
ctx.context.options.advanced,
),
databaseGenerateId:
ctx.context.options.advanced?.database?.generateId,
}) &&
!session.user.emailVerified
) {
throw APIError.from(
@@ -1276,12 +1338,7 @@ export const listUserInvitations = <O extends OrganizationOptions>(
// When the caller has a session, require an ownership signal stronger
// than the email string before enumerating invitations targeted at it.
// Server-side SDK calls without a session are trusted and skip the gate.
// FIXME(next-minor): drop the option and make the gate unconditional.
if (
session &&
(ctx.context.orgOptions.requireEmailVerificationOnInvitation ?? true) &&
!session.user.emailVerified
) {
if (session && !session.user.emailVerified) {
throw APIError.from(
"FORBIDDEN",
ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION,
@@ -205,19 +205,26 @@ export interface OrganizationOptions {
*/
cancelPendingInvitationsOnReInvite?: boolean | undefined;
/**
* Require email verification on session-authenticated recipient invitation
* calls (accept, reject, get, list). Defaults to `true` so unverified
* accounts registered against a victim's email cannot accept, read, or
* enumerate invitations targeted at that email. Server-side
* `listUserInvitations` calls without a session (caller passes
* `ctx.query.email`) continue to bypass the gate because the caller is
* trusted. Set to `false` for backward compatibility on apps that do not
* require email verification; understand the takeover risk before doing so.
* Require email verification before session-authenticated recipient
* invitation calls that carry an invitation ID (accept, reject, get).
*
* @default true
* When unset, Better Auth preserves the normal emailed-invitation flow for
* built-in opaque invitation IDs, including the default generator and
* `advanced.database.generateId: "uuid"`. It requires verification for
* externally controlled or predictable invitation IDs, such as
* `advanced.database.generateId: "serial"` / `false` or custom ID
* generation.
*
* @deprecated The option will be removed on the next minor; the gate will
* become unconditional. Plan to verify emails before invitation acceptance.
* Set this option to `true` when invitation IDs may be visible outside the
* invited user's mailbox, when organization invitation lists are exposed to
* members, or when verified email should be the ownership proof for by-ID
* invitation actions. Client-side `listUserInvitations` calls always require
* a verified session email because they enumerate invitation IDs from
* `session.user.email`. Server-side `listUserInvitations` calls without a
* session (caller passes `ctx.query.email`) continue to bypass the gate
* because the caller is trusted.
*
* @default undefined
*/
requireEmailVerificationOnInvitation?: boolean | undefined;
/**