[GH-ISSUE #7824] acceptInvitation return type is incorrectly nullable due to ctx.json(null) instead of throw #10919

Closed
opened 2026-04-13 07:18:55 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @Lqm1 on GitHub (Feb 6, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7824

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Use auth.api.acceptInvitation in your application
  2. Check the return type of the function - it is inferred as nullable
  3. The problem is in packages/better-auth/src/plugins/organization/routes/crud-invites.ts
  4. When adapter.updateInvitation returns null, the code uses ctx.json(null, ...) instead of throwing an error
  5. This causes TypeScript to infer the return type as nullable, even though it should always return a valid object or throw an error

Current vs. Expected behavior

Current behavior:

  • The return type of acceptInvitation is inferred as nullable
  • This is because there is a code path that returns ctx.json(null, ...) instead of throwing an error
  • The problematic code is located around line 585-592 in crud-invites.ts:
if (!acceptedI) {
    return ctx.json(null, {
        status: 400,
        body: {
            message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND.message,
        },
    });
}

Expected behavior:

  • The return type should not be nullable
  • When adapter.updateInvitation returns null, the code should throw APIError instead of returning ctx.json(null, ...)
  • This would make the type inference correct and consistent with other error handling in the codebase

Root cause:

  • adapter.updateInvitation can return null
  • Instead of throwing an error with throw APIError.from(...), the code returns ctx.json(null, ...)
  • This makes TypeScript infer the return type as nullable

What version of Better Auth are you using?

^1.4.18

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Home",
    "release": "10.0.26200",
    "cpuCount": 24,
    "cpuModel": "Intel(R) Core(TM) i7-14650HX",
    "totalMemory": "47.71 GB",
    "freeMemory": "5.18 GB"
  },
  "node": {
    "version": "v24.11.1",
    "env": "development"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.3.4"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.1.1"
    },
    {
      "name": "react",
      "version": "19.2.3"
    },
    {
      "name": "hono",
      "version": "^4.11.5"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^7.3.0"
    }
  ],
  "betterAuth": {
    "version": "^1.4.18",
    "config": {
      "appName": "next-app",
      "advanced": {
        "database": {
          "generateId": false
        }
      },
      "experimental": {
        "joins": true
      },
      "emailAndPassword": {
        "enabled": true
      },
      "emailVerification": {},
      "plugins": [
        {
          "name": "two-factor",
          "config": {
            "id": "two-factor",
            "endpoints": {},
            "options": {
              "otpOptions": {}
            },
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "user": {
                "fields": {
                  "twoFactorEnabled": {
                    "type": "boolean",
                    "required": false,
                    "defaultValue": false,
                    "input": false
                  }
                }
              },
              "twoFactor": {
                "fields": {
                  "secret": {
                    "type": "string",
                    "required": true,
                    "returned": false,
                    "index": true
                  },
                  "backupCodes": {
                    "type": "string",
                    "required": true,
                    "returned": false
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "returned": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "index": true
                  }
                }
              }
            },
            "rateLimit": [
              {
                "window": 10,
                "max": 3
              }
            ],
            "$ERROR_CODES": {
              "OTP_NOT_ENABLED": "OTP not enabled",
              "OTP_HAS_EXPIRED": "OTP has expired",
              "TOTP_NOT_ENABLED": "TOTP not enabled",
              "TWO_FACTOR_NOT_ENABLED": "Two factor isn't enabled",
              "BACKUP_CODES_NOT_ENABLED": "Backup codes aren't enabled",
              "INVALID_BACKUP_CODE": "Invalid backup code",
              "INVALID_CODE": "Invalid code",
              "TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE": "Too many attempts. Please request a new code.",
              "INVALID_TWO_FACTOR_COOKIE": "Invalid two factor cookie"
            }
          }
        },
        {
          "name": "username",
          "config": {
            "id": "username",
            "endpoints": {},
            "schema": {
              "user": {
                "fields": {
                  "username": {
                    "type": "string",
                    "required": false,
                    "sortable": true,
                    "unique": true,
                    "returned": true,
                    "transform": {}
                  },
                  "displayUsername": {
                    "type": "string",
                    "required": false,
                    "transform": {}
                  }
                }
              }
            },
            "hooks": {
              "before": [
                {},
                {}
              ]
            },
            "$ERROR_CODES": {
              "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]",
              "EMAIL_NOT_VERIFIED": "Email not verified",
              "UNEXPECTED_ERROR": "Unexpected error",
              "USERNAME_IS_ALREADY_TAKEN": "Username is already taken. Please try another.",
              "USERNAME_TOO_SHORT": "Username is too short",
              "USERNAME_TOO_LONG": "Username is too long",
              "INVALID_USERNAME": "Username is invalid",
              "INVALID_DISPLAY_USERNAME": "Display username is invalid"
            }
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself",
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value",
              "YOU_CANNOT_IMPERSONATE_ADMINS": "You cannot impersonate admins",
              "INVALID_ROLE_TYPE": "Invalid role type"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            }
          }
        },
        {
          "name": "organization",
          "config": {
            "id": "organization",
            "endpoints": {},
            "schema": {
              "organization": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": true,
                    "sortable": true
                  },
                  "slug": {
                    "type": "string",
                    "required": true,
                    "unique": true,
                    "sortable": true,
                    "index": true
                  },
                  "logo": {
                    "type": "string",
                    "required": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "metadata": {
                    "type": "string",
                    "required": false
                  }
                }
              },
              "team": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": true
                  },
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    },
                    "index": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": false
                  }
                }
              },
              "teamMember": {
                "fields": {
                  "teamId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "team",
                      "field": "id"
                    },
                    "index": true
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "index": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": false
                  }
                }
              },
              "member": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    },
                    "index": true
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "index": true
                  },
                  "role": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "member"
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  }
                }
              },
              "invitation": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    },
                    "index": true
                  },
                  "email": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "index": true
                  },
                  "role": {
                    "type": "string",
                    "required": false,
                    "sortable": true
                  },
                  "teamId": {
                    "type": "string",
                    "required": false,
                    "sortable": true
                  },
                  "status": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "pending"
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "inviterId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "required": true
                  }
                }
              },
              "session": {
                "fields": {
                  "activeOrganizationId": {
                    "type": "string",
                    "required": false
                  },
                  "activeTeamId": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "$Infer": {
              "Organization": {},
              "Invitation": {},
              "Member": {},
              "Team": {},
              "TeamMember": {},
              "ActiveOrganization": {}
            },
            "$ERROR_CODES": {
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION": "You are not allowed to create a new organization",
              "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS": "You have reached the maximum number of organizations",
              "ORGANIZATION_ALREADY_EXISTS": "Organization already exists",
              "ORGANIZATION_SLUG_ALREADY_TAKEN": "Organization slug already taken",
              "ORGANIZATION_NOT_FOUND": "Organization not found",
              "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the organization",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION": "You are not allowed to update this organization",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION": "You are not allowed to delete this organization",
              "NO_ACTIVE_ORGANIZATION": "No active organization",
              "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION": "User is already a member of this organization",
              "MEMBER_NOT_FOUND": "Member not found",
              "ROLE_NOT_FOUND": "Role not found",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM": "You are not allowed to create a new team",
              "TEAM_ALREADY_EXISTS": "Team already exists",
              "TEAM_NOT_FOUND": "Team not found",
              "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER": "You cannot leave the organization as the only owner",
              "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER": "You cannot leave the organization without an owner",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER": "You are not allowed to delete this member",
              "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION": "You are not allowed to invite users to this organization",
              "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "User is already invited to this organization",
              "INVITATION_NOT_FOUND": "Invitation not found",
              "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION": "You are not the recipient of the invitation",
              "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION": "Email verification required before accepting or rejecting invitation",
              "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION": "You are not allowed to cancel this invitation",
              "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION": "Inviter is no longer a member of the organization",
              "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE": "You are not allowed to invite a user with this role",
              "FAILED_TO_RETRIEVE_INVITATION": "Failed to retrieve invitation",
              "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS": "You have reached the maximum number of teams",
              "UNABLE_TO_REMOVE_LAST_TEAM": "Unable to remove last team",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER": "You are not allowed to update this member",
              "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED": "Organization membership limit reached",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to create teams in this organization",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to delete teams in this organization",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM": "You are not allowed to update this team",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM": "You are not allowed to delete this team",
              "INVITATION_LIMIT_REACHED": "Invitation limit reached",
              "TEAM_MEMBER_LIMIT_REACHED": "Team member limit reached",
              "USER_IS_NOT_A_MEMBER_OF_THE_TEAM": "User is not a member of the team",
              "YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM": "You are not allowed to list the members of this team",
              "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM": "You do not have an active team",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER": "You are not allowed to create a new member",
              "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER": "You are not allowed to remove a team member",
              "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION": "You are not allowed to access this organization as an owner",
              "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION": "You are not a member of this organization",
              "MISSING_AC_INSTANCE": "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information",
              "YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE": "You must be in an organization to create a role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE": "You are not allowed to create a role",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE": "You are not allowed to update a role",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE": "You are not allowed to delete a role",
              "YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE": "You are not allowed to read a role",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE": "You are not allowed to list a role",
              "YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE": "You are not allowed to get a role",
              "TOO_MANY_ROLES": "This organization has too many roles",
              "INVALID_RESOURCE": "The provided permission includes an invalid resource",
              "ROLE_NAME_IS_ALREADY_TAKEN": "That role name is already taken",
              "CANNOT_DELETE_A_PRE_DEFINED_ROLE": "Cannot delete a pre-defined role"
            },
            "options": {
              "allowUserToCreateOrganization": false,
              "teams": {
                "enabled": true,
                "defaultTeam": {
                  "enabled": false
                }
              }
            }
          }
        },
        {
          "name": "captcha",
          "config": {
            "id": "captcha",
            "options": {
              "provider": "cloudflare-turnstile",
              "secretKey": "process.env.TURNSTILE_SECRET_KEY"
            }
          }
        },
        {
          "name": "haveIBeenPwned",
          "config": {
            "id": "haveIBeenPwned",
            "$ERROR_CODES": {
              "PASSWORD_COMPROMISED": "The password you entered has been compromised. Please choose a different password."
            }
          }
        },
        {
          "name": "ip-restriction",
          "config": {
            "id": "ip-restriction",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "ipRestriction": {
                "fields": {
                  "id": {
                    "type": "string",
                    "required": true
                  },
                  "ipAddress": {
                    "type": "string",
                    "required": true
                  },
                  "label": {
                    "type": "string",
                    "required": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": true
                  }
                }
              },
              "organizationIpRestriction": {
                "fields": {
                  "id": {
                    "type": "string",
                    "required": true
                  },
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "ipRestrictionId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "ipRestriction",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": true
                  }
                }
              },
              "teamIpRestriction": {
                "fields": {
                  "id": {
                    "type": "string",
                    "required": true
                  },
                  "teamId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "team",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "ipRestrictionId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "ipRestriction",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": true
                  }
                }
              }
            }
          }
        }
      ]
    }
  }
}

Which area(s) are affected? (Select all that apply)

Backend, Types

Auth config (if applicable)


Additional context

Suggested fix:
Replace the ctx.json(null, ...) return with:

if (!acceptedI) {
    throw APIError.from(
        "BAD_REQUEST",
        ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
    );
}

This will ensure proper error handling and correct type inference.

Note: There's already a null check earlier in the function that throws an error, so this second check might be redundant or unreachable code.

Originally created by @Lqm1 on GitHub (Feb 6, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7824 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Use `auth.api.acceptInvitation` in your application 2. Check the return type of the function - it is inferred as nullable 3. The problem is in [ `packages/better-auth/src/plugins/organization/routes/crud-invites.ts`](https://github.com/better-auth/better-auth/blob/f82960b984ff8443bf18beaf8cb9c0938d11ffff/packages/better-auth/src/plugins/organization/routes/crud-invites.ts#L703C9-L703C18) 4. When `adapter.updateInvitation` returns `null`, the code uses `ctx.json(null, ...)` instead of throwing an error 5. This causes TypeScript to infer the return type as nullable, even though it should always return a valid object or throw an error ### Current vs. Expected behavior **Current behavior:** - The return type of `acceptInvitation` is inferred as nullable - This is because there is a code path that returns `ctx.json(null, ...)` instead of throwing an error - The problematic code is located around line 585-592 in `crud-invites.ts`: ```typescript if (!acceptedI) { return ctx.json(null, { status: 400, body: { message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND.message, }, }); } ``` **Expected behavior:** - The return type should not be nullable - When `adapter.updateInvitation` returns `null`, the code should throw `APIError` instead of returning `ctx.json(null, ...)` - This would make the type inference correct and consistent with other error handling in the codebase **Root cause:** - `adapter.updateInvitation` can return `null` - Instead of throwing an error with `throw APIError.from(...)`, the code returns `ctx.json(null, ...)` - This makes TypeScript infer the return type as nullable ### What version of Better Auth are you using? ^1.4.18 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Home", "release": "10.0.26200", "cpuCount": 24, "cpuModel": "Intel(R) Core(TM) i7-14650HX", "totalMemory": "47.71 GB", "freeMemory": "5.18 GB" }, "node": { "version": "v24.11.1", "env": "development" }, "packageManager": { "name": "bun", "version": "1.3.4" }, "frameworks": [ { "name": "next", "version": "16.1.1" }, { "name": "react", "version": "19.2.3" }, { "name": "hono", "version": "^4.11.5" } ], "databases": [ { "name": "@prisma/client", "version": "^7.3.0" } ], "betterAuth": { "version": "^1.4.18", "config": { "appName": "next-app", "advanced": { "database": { "generateId": false } }, "experimental": { "joins": true }, "emailAndPassword": { "enabled": true }, "emailVerification": {}, "plugins": [ { "name": "two-factor", "config": { "id": "two-factor", "endpoints": {}, "options": { "otpOptions": {} }, "hooks": { "after": [ {} ] }, "schema": { "user": { "fields": { "twoFactorEnabled": { "type": "boolean", "required": false, "defaultValue": false, "input": false } } }, "twoFactor": { "fields": { "secret": { "type": "string", "required": true, "returned": false, "index": true }, "backupCodes": { "type": "string", "required": true, "returned": false }, "userId": { "type": "string", "required": true, "returned": false, "references": { "model": "user", "field": "id" }, "index": true } } } }, "rateLimit": [ { "window": 10, "max": 3 } ], "$ERROR_CODES": { "OTP_NOT_ENABLED": "OTP not enabled", "OTP_HAS_EXPIRED": "OTP has expired", "TOTP_NOT_ENABLED": "TOTP not enabled", "TWO_FACTOR_NOT_ENABLED": "Two factor isn't enabled", "BACKUP_CODES_NOT_ENABLED": "Backup codes aren't enabled", "INVALID_BACKUP_CODE": "Invalid backup code", "INVALID_CODE": "Invalid code", "TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE": "Too many attempts. Please request a new code.", "INVALID_TWO_FACTOR_COOKIE": "Invalid two factor cookie" } } }, { "name": "username", "config": { "id": "username", "endpoints": {}, "schema": { "user": { "fields": { "username": { "type": "string", "required": false, "sortable": true, "unique": true, "returned": true, "transform": {} }, "displayUsername": { "type": "string", "required": false, "transform": {} } } } }, "hooks": { "before": [ {}, {} ] }, "$ERROR_CODES": { "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]", "EMAIL_NOT_VERIFIED": "Email not verified", "UNEXPECTED_ERROR": "Unexpected error", "USERNAME_IS_ALREADY_TAKEN": "Username is already taken. Please try another.", "USERNAME_TOO_SHORT": "Username is too short", "USERNAME_TOO_LONG": "Username is too long", "INVALID_USERNAME": "Username is invalid", "INVALID_DISPLAY_USERNAME": "Display username is invalid" } } }, { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself", "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value", "YOU_CANNOT_IMPERSONATE_ADMINS": "You cannot impersonate admins", "INVALID_ROLE_TYPE": "Invalid role type" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } } } }, { "name": "organization", "config": { "id": "organization", "endpoints": {}, "schema": { "organization": { "fields": { "name": { "type": "string", "required": true, "sortable": true }, "slug": { "type": "string", "required": true, "unique": true, "sortable": true, "index": true }, "logo": { "type": "string", "required": false }, "createdAt": { "type": "date", "required": true }, "metadata": { "type": "string", "required": false } } }, "team": { "fields": { "name": { "type": "string", "required": true }, "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" }, "index": true }, "createdAt": { "type": "date", "required": true }, "updatedAt": { "type": "date", "required": false } } }, "teamMember": { "fields": { "teamId": { "type": "string", "required": true, "references": { "model": "team", "field": "id" }, "index": true }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" }, "index": true }, "createdAt": { "type": "date", "required": false } } }, "member": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" }, "index": true }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" }, "index": true }, "role": { "type": "string", "required": true, "sortable": true, "defaultValue": "member" }, "createdAt": { "type": "date", "required": true } } }, "invitation": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" }, "index": true }, "email": { "type": "string", "required": true, "sortable": true, "index": true }, "role": { "type": "string", "required": false, "sortable": true }, "teamId": { "type": "string", "required": false, "sortable": true }, "status": { "type": "string", "required": true, "sortable": true, "defaultValue": "pending" }, "expiresAt": { "type": "date", "required": true }, "createdAt": { "type": "date", "required": true }, "inviterId": { "type": "string", "references": { "model": "user", "field": "id" }, "required": true } } }, "session": { "fields": { "activeOrganizationId": { "type": "string", "required": false }, "activeTeamId": { "type": "string", "required": false } } } }, "$Infer": { "Organization": {}, "Invitation": {}, "Member": {}, "Team": {}, "TeamMember": {}, "ActiveOrganization": {} }, "$ERROR_CODES": { "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION": "You are not allowed to create a new organization", "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS": "You have reached the maximum number of organizations", "ORGANIZATION_ALREADY_EXISTS": "Organization already exists", "ORGANIZATION_SLUG_ALREADY_TAKEN": "Organization slug already taken", "ORGANIZATION_NOT_FOUND": "Organization not found", "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the organization", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION": "You are not allowed to update this organization", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION": "You are not allowed to delete this organization", "NO_ACTIVE_ORGANIZATION": "No active organization", "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION": "User is already a member of this organization", "MEMBER_NOT_FOUND": "Member not found", "ROLE_NOT_FOUND": "Role not found", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM": "You are not allowed to create a new team", "TEAM_ALREADY_EXISTS": "Team already exists", "TEAM_NOT_FOUND": "Team not found", "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER": "You cannot leave the organization as the only owner", "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER": "You cannot leave the organization without an owner", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER": "You are not allowed to delete this member", "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION": "You are not allowed to invite users to this organization", "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "User is already invited to this organization", "INVITATION_NOT_FOUND": "Invitation not found", "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION": "You are not the recipient of the invitation", "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION": "Email verification required before accepting or rejecting invitation", "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION": "You are not allowed to cancel this invitation", "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION": "Inviter is no longer a member of the organization", "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE": "You are not allowed to invite a user with this role", "FAILED_TO_RETRIEVE_INVITATION": "Failed to retrieve invitation", "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS": "You have reached the maximum number of teams", "UNABLE_TO_REMOVE_LAST_TEAM": "Unable to remove last team", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER": "You are not allowed to update this member", "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED": "Organization membership limit reached", "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to create teams in this organization", "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to delete teams in this organization", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM": "You are not allowed to update this team", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM": "You are not allowed to delete this team", "INVITATION_LIMIT_REACHED": "Invitation limit reached", "TEAM_MEMBER_LIMIT_REACHED": "Team member limit reached", "USER_IS_NOT_A_MEMBER_OF_THE_TEAM": "User is not a member of the team", "YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM": "You are not allowed to list the members of this team", "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM": "You do not have an active team", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER": "You are not allowed to create a new member", "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER": "You are not allowed to remove a team member", "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION": "You are not allowed to access this organization as an owner", "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION": "You are not a member of this organization", "MISSING_AC_INSTANCE": "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information", "YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE": "You must be in an organization to create a role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE": "You are not allowed to create a role", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE": "You are not allowed to update a role", "YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE": "You are not allowed to delete a role", "YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE": "You are not allowed to read a role", "YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE": "You are not allowed to list a role", "YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE": "You are not allowed to get a role", "TOO_MANY_ROLES": "This organization has too many roles", "INVALID_RESOURCE": "The provided permission includes an invalid resource", "ROLE_NAME_IS_ALREADY_TAKEN": "That role name is already taken", "CANNOT_DELETE_A_PRE_DEFINED_ROLE": "Cannot delete a pre-defined role" }, "options": { "allowUserToCreateOrganization": false, "teams": { "enabled": true, "defaultTeam": { "enabled": false } } } } }, { "name": "captcha", "config": { "id": "captcha", "options": { "provider": "cloudflare-turnstile", "secretKey": "process.env.TURNSTILE_SECRET_KEY" } } }, { "name": "haveIBeenPwned", "config": { "id": "haveIBeenPwned", "$ERROR_CODES": { "PASSWORD_COMPROMISED": "The password you entered has been compromised. Please choose a different password." } } }, { "name": "ip-restriction", "config": { "id": "ip-restriction", "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "ipRestriction": { "fields": { "id": { "type": "string", "required": true }, "ipAddress": { "type": "string", "required": true }, "label": { "type": "string", "required": false }, "createdAt": { "type": "date", "required": true }, "updatedAt": { "type": "date", "required": true } } }, "organizationIpRestriction": { "fields": { "id": { "type": "string", "required": true }, "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id", "onDelete": "cascade" } }, "ipRestrictionId": { "type": "string", "required": true, "references": { "model": "ipRestriction", "field": "id", "onDelete": "cascade" } }, "createdAt": { "type": "date", "required": true }, "updatedAt": { "type": "date", "required": true } } }, "teamIpRestriction": { "fields": { "id": { "type": "string", "required": true }, "teamId": { "type": "string", "required": true, "references": { "model": "team", "field": "id", "onDelete": "cascade" } }, "ipRestrictionId": { "type": "string", "required": true, "references": { "model": "ipRestriction", "field": "id", "onDelete": "cascade" } }, "createdAt": { "type": "date", "required": true }, "updatedAt": { "type": "date", "required": true } } } } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Types ### Auth config (if applicable) ```typescript ``` ### Additional context **Suggested fix:** Replace the `ctx.json(null, ...)` return with: ```typescript if (!acceptedI) { throw APIError.from( "BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND, ); } ``` This will ensure proper error handling and correct type inference. Note: There's already a null check earlier in the function that throws an error, so this second check might be redundant or unreachable code.
GiteaMirror added the lockedbug labels 2026-04-13 07:18:55 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Feb 6, 2026):

You're correct—the nullable return type in acceptInvitation is caused by a code path that returns ctx.json(null, ...) instead of throwing an error. This path is actually redundant and unreachable because there's already a null check right after adapter.updateInvitation that throws an APIError if the result is null. Removing the ctx.json(null, ...) return and replacing it with a throw APIError (or just removing it entirely if unreachable) will fix the type inference and align with the rest of the codebase's error handling conventions. This approach is consistent with how errors are handled elsewhere in Better Auth and will ensure the return type is not nullable, as expected. See the relevant code and discussion here.

This pattern (returning ctx.json(null, ...) instead of throwing) appears in a few other places in the organization plugin as well, and standardizing on throwing APIError for all error cases is the recommended approach for consistent type inference and error handling across the codebase.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3860934463 --> @dosubot[bot] commented on GitHub (Feb 6, 2026): <!-- Answer --> You're correct—the nullable return type in `acceptInvitation` is caused by a code path that returns `ctx.json(null, ...)` instead of throwing an error. This path is actually redundant and unreachable because there's already a null check right after `adapter.updateInvitation` that throws an `APIError` if the result is null. Removing the `ctx.json(null, ...)` return and replacing it with a `throw APIError` (or just removing it entirely if unreachable) will fix the type inference and align with the rest of the codebase's error handling conventions. This approach is consistent with how errors are handled elsewhere in Better Auth and will ensure the return type is not nullable, as expected. See the relevant code and discussion [here](https://github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/better-auth/src/plugins/organization/routes/crud-invites.ts#L552-L710). This pattern (returning `ctx.json(null, ...)` instead of throwing) appears in a few other places in the organization plugin as well, and standardizing on throwing `APIError` for all error cases is the recommended approach for consistent type inference and error handling across the codebase. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/faf75f31-9673-49cc-819e-72ef1148a6dd?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/faf75f31-9673-49cc-819e-72ef1148a6dd?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/faf75f31-9673-49cc-819e-72ef1148a6dd?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/faf75f31-9673-49cc-819e-72ef1148a6dd?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/faf75f31-9673-49cc-819e-72ef1148a6dd?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/faf75f31-9673-49cc-819e-72ef1148a6dd?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/faf75f31-9673-49cc-819e-72ef1148a6dd?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=You%27re%20correct%E2%80%94the%20nullable%20return%20type%20in%20%60acceptInvitation%60%20is%20caused%20by%20a%20code%20path%20that%20returns%20%60ctx.json%28null%2C%20...%29%60%20instead%20of%20throwing%20an%20error.%20This%20path%20is%20actually%20redundant%20and%20unreachable%20because%20there%27s%20already%20a%20null%20check%20right%20after%20%60adapter.updateInvitation%60%20that%20throws%20an%20%60APIError%60%20if%20the%20result%20is%20null.%20Removing%20the%20%60ctx.json%28null%2C%20...%29%60%20return%20and%20replacing%20it%20with%20a%20%60throw%20APIError%60%20%28or%20just%20removing%20it%20entirely%20if%20unreachable%29%20will%20fix%20the%20type%20inference%20and%20align%20with%20the%20rest%20of%20the%20codebase%27s%20error%20handling%20conventions.%20This%20approach%20is%20consistent%20with%20how%20errors%20are%20handled%20elsewhere%20in%20Better%20Auth%20and%20will%20ensure%20the%20return%20type%20is%20not%20nullable%2C%20as%20expected.%20See%20the%20relevant%20code%20and%20discussion%20%5Bhere%5D%28https%3A//github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/better-auth/src/plugins/organization/routes/crud-invites.ts%23L552-L710%29.%0A%0AThis%20pattern%20%28returning%20%60ctx.json%28null%2C%20...%29%60%20instead%20of%20throwing%29%20appears%20in%20a%20few%20other%20places%20in%20the%20organization%20plugin%20as%20well%2C%20and%20standardizing%20on%20throwing%20%60APIError%60%20for%20all%20error%20cases%20is%20the%20recommended%20approach%20for%20consistent%20type%20inference%20and%20error%20handling%20across%20the%20codebase.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/7824)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10919