fix(github)!: align profile types with API responses

Co-authored-by: waleed <waleedmirza667@gmail.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Taesu
2026-10-16 09:59:12 +09:00
co-authored by waleed Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 2c7833fad6
commit 1754637696
4 changed files with 123 additions and 23 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@better-auth/core": minor
---
Allow custom GitHub `getUserInfo` callbacks to return public or private API responses without type assertions. `GithubProfile` now uses numeric IDs and counters, nullable profile attributes, and optional account details.
Consumers may need to handle null or omitted fields and use `String(profile.id)` when a string ID is required.
@@ -72,6 +72,12 @@ That's all! Now you can copy the Client ID and Client Secret of your app!
If you get an `email_not_found` error, it is most often because either (a) you created a GitHub App and did not grant the "Email addresses: Read-only" permission, or (b) the user has set their primary email to private, in which case `GET /user` returns `email: null`. See [Handling Providers Without Email](/docs/concepts/oauth#handling-providers-without-email) for the recommended `mapProfileToUser` fallback; the user's private addresses are also available via [`/user/emails`](https://docs.github.com/en/rest/users/emails).
</Callout>
### Custom profile data
Custom `getUserInfo` callbacks can return GitHub's [public or private `GET /user` response](https://docs.github.com/en/rest/users/users#get-the-authenticated-user) as `data`. `GithubProfile` uses numeric IDs and counters and allows nullable profile attributes. `twitter_username`, private account details, and `plan` can be omitted.
When reading profile data, use `String(profile.id)` if you need a string ID, `profile.name ?? profile.login` for a name fallback, and `profile.plan?.name` for an optional plan.
### Why don't I have a refresh token?
Github doesn't issue refresh tokens for OAuth apps. For regular OAuth apps,
@@ -0,0 +1,87 @@
import { assertType, describe, expect, it } from "vitest";
import type { GithubProfile } from "./github";
import { github } from "./github";
const publicProfile = {
login: "octocat",
id: 583231,
node_id: "MDQ6VXNlcjU4MzIzMQ==",
avatar_url: "https://avatars.githubusercontent.com/u/583231?v=4",
gravatar_id: null,
url: "https://api.github.com/users/octocat",
html_url: "https://github.com/octocat",
followers_url: "https://api.github.com/users/octocat/followers",
following_url: "https://api.github.com/users/octocat/following{/other_user}",
gists_url: "https://api.github.com/users/octocat/gists{/gist_id}",
starred_url: "https://api.github.com/users/octocat/starred{/owner}{/repo}",
subscriptions_url: "https://api.github.com/users/octocat/subscriptions",
organizations_url: "https://api.github.com/users/octocat/orgs",
repos_url: "https://api.github.com/users/octocat/repos",
events_url: "https://api.github.com/users/octocat/events{/privacy}",
received_events_url: "https://api.github.com/users/octocat/received_events",
type: "User",
site_admin: false,
name: null,
company: null,
blog: null,
location: null,
email: null,
hireable: null,
bio: null,
public_repos: 8,
public_gists: 8,
followers: 20,
following: 9,
created_at: "2011-01-25T18:44:36Z",
updated_at: "2026-01-01T00:00:00Z",
};
const privateProfile = {
...publicProfile,
private_gists: 81,
total_private_repos: 100,
owned_private_repos: 100,
disk_usage: 10_000,
collaborators: 8,
two_factor_authentication: true,
};
/**
* @see https://github.com/better-auth/better-auth/issues/11040
* @see https://docs.github.com/en/rest/users/users#get-the-authenticated-user
*/
describe("github profile", () => {
it("accepts public and private API responses", () => {
assertType<GithubProfile>(publicProfile);
assertType<GithubProfile>(privateProfile);
assertType<GithubProfile>({
...publicProfile,
twitter_username: "octocat",
});
assertType<GithubProfile>({
...privateProfile,
twitter_username: null,
plan: { name: "Medium", space: 400, private_repos: 20, collaborators: 0 },
});
});
it.each([
{ kind: "public", profile: publicProfile },
{ kind: "private", profile: privateProfile },
])("accepts a raw $kind response from getUserInfo", async ({ profile }) => {
const provider = github({
clientId: "github-app",
getUserInfo: async () => ({
user: {
name: profile.name ?? profile.login,
email: profile.email,
emailVerified: false,
},
data: profile,
}),
});
const result = await provider.getUserInfo({ accessToken: "access-token" });
expect(result?.data).toBe(profile);
});
});
+23 -23
View File
@@ -10,10 +10,10 @@ import { authorizationCodeRequest } from "../oauth2/validate-authorization-code"
export interface GithubProfile {
login: string;
id: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
gravatar_id: string | null;
url: string;
html_url: string;
followers_url: string;
@@ -27,31 +27,31 @@ export interface GithubProfile {
received_events_url: string;
type: string;
site_admin: boolean;
name: string;
company: string;
blog: string;
location: string;
name: string | null;
company: string | null;
blog: string | null;
location: string | null;
email: string | null;
hireable: boolean;
bio: string;
twitter_username: string;
public_repos: string;
public_gists: string;
followers: string;
following: string;
hireable: boolean | null;
bio: string | null;
twitter_username?: string | null;
public_repos: number;
public_gists: number;
followers: number;
following: number;
created_at: string;
updated_at: string;
private_gists: string;
total_private_repos: string;
owned_private_repos: string;
disk_usage: string;
collaborators: string;
two_factor_authentication: boolean;
plan: {
private_gists?: number;
total_private_repos?: number;
owned_private_repos?: number;
disk_usage?: number;
collaborators?: number;
two_factor_authentication?: boolean;
plan?: {
name: string;
space: string;
private_repos: string;
collaborators: string;
space: number;
private_repos: number;
collaborators: number;
};
}