mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
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:
co-authored by
waleed
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
2c7833fad6
commit
1754637696
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user