Skip to content

Commit 24ec679

Browse files
erquhartclaude
andcommitted
Type the GitHub userinfo responses via the catalog generic
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e20bb8a commit 24ec679

2 files changed

Lines changed: 22 additions & 8 deletions

File tree

packages/core/src/oauth/component/github.test.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { describe, expect, test } from "vitest";
22
import { normalizeGithubProfile } from "./github";
33

4-
/** Call the mapping with the userinfo responses the callback would pass. */
4+
type GithubUserInfo = NonNullable<Parameters<typeof normalizeGithubProfile>[1]>;
5+
6+
/**
7+
* Call the mapping with the userinfo responses the callback would pass. The
8+
* cast lets tests feed loose and partial responses: the mapping's declared
9+
* shape is trusted typing, and these tests exercise the defensive paths
10+
* behind it.
11+
*/
512
function normalize(
613
user: Record<string, unknown>,
714
emails?: Array<{ email: string; primary: boolean; verified: boolean }>,
815
) {
9-
return normalizeGithubProfile(undefined, { user, emails });
16+
return normalizeGithubProfile(undefined, { user, emails } as GithubUserInfo);
1017
}
1118

1219
describe("normalizeGithubProfile", () => {
@@ -57,8 +64,8 @@ describe("normalizeGithubProfile", () => {
5764
});
5865

5966
test("throws when the user response is missing", () => {
60-
expect(() => normalizeGithubProfile(undefined, {})).toThrow(
61-
/missing the `user` entry/,
62-
);
67+
expect(() =>
68+
normalizeGithubProfile(undefined, {} as GithubUserInfo),
69+
).toThrow(/missing the `user` entry/);
6370
});
6471
});

packages/core/src/oauth/component/github.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,29 @@ type GithubUser = {
3636
avatar_url?: string;
3737
};
3838

39+
/**
40+
* The userinfo responses the catalog's endpoints produce, keyed like its
41+
* `userInfoEndpoints`. Trusted typing over what GitHub returns, not runtime
42+
* validation (see {@link OauthProfile}).
43+
*/
44+
type GithubUserInfo = { user: GithubUser; emails: GithubEmail[] };
45+
3946
/**
4047
* Map GitHub's userinfo responses to {@link GithubProfile}. `user` comes from
4148
* `/user` and `emails` from `/user/emails` (the endpoints the catalog fetches).
4249
* Email prefers the primary verified address, then any verified one, then
4350
* whatever `/user` returned — `emailVerified` reports whether the chosen
4451
* address came from a verified entry; `name` falls back to the login handle.
4552
*/
46-
export const normalizeGithubProfile: OauthProfile = (
53+
export const normalizeGithubProfile: OauthProfile<GithubUserInfo> = (
4754
_claims,
4855
userInfoResponses,
4956
) => {
50-
const user = userInfoResponses?.user as GithubUser | undefined;
57+
const user = userInfoResponses?.user;
5158
if (user === undefined) {
5259
throw new Error("GitHub userinfo response is missing the `user` entry");
5360
}
54-
const emails = (userInfoResponses?.emails ?? []) as GithubEmail[];
61+
const emails = userInfoResponses?.emails ?? [];
5562
const best =
5663
emails.find((entry) => entry.primary && entry.verified) ??
5764
emails.find((entry) => entry.verified);

0 commit comments

Comments
 (0)