diff --git a/.changeset/mellow-notion-owner.md b/.changeset/mellow-notion-owner.md index 37ec21a3bb..a5d86b6be0 100644 --- a/.changeset/mellow-notion-owner.md +++ b/.changeset/mellow-notion-owner.md @@ -2,4 +2,4 @@ "@better-auth/core": minor --- -Allow partial Notion owner profiles and nullable display fields in `NotionProfile`, and leave the local image unset when no owner avatar is returned. Existing user images are preserved when profile updates are enabled and the provider returns no image. +Notion profile mappings now handle partial person owners, nullable names and avatars, and the optional email verification claim. Missing avatars preserve stored images when profile updates are enabled; the default email trust policy is unchanged. diff --git a/packages/core/src/social-providers/notion.ts b/packages/core/src/social-providers/notion.ts index 8ac314dffe..a06acc9d3c 100644 --- a/packages/core/src/social-providers/notion.ts +++ b/packages/core/src/social-providers/notion.ts @@ -10,12 +10,13 @@ import { export interface NotionProfile { object: "user"; id: string; - type?: "person" | "bot" | undefined; + type?: "person" | undefined; name?: string | null | undefined; avatar_url?: string | null | undefined; person?: | { email?: string; + email_verified?: boolean; } | undefined; } diff --git a/packages/core/src/social-providers/social-providers.test.ts b/packages/core/src/social-providers/social-providers.test.ts index e1c50f2a42..11c4c3f2b6 100644 --- a/packages/core/src/social-providers/social-providers.test.ts +++ b/packages/core/src/social-providers/social-providers.test.ts @@ -461,6 +461,7 @@ describe("cognito provider", () => { /** * @see https://github.com/makenotion/notion-sdk-js/blob/v2.2.15/src/api-endpoints.ts * @see https://github.com/makenotion/notion-sdk-js/blob/v2.2.15/src/Client.ts + * @see https://developers.notion.com/page/changelog#new-identity-fields-in-oauth-token-responses */ describe("notion owner profiles", () => { it.each([null, "https://example.com/owner.png"])( @@ -472,15 +473,21 @@ describe("notion owner profiles", () => { type: "person", name: null, avatar_url, - person: { email: "owner@example.com" }, + person: { email: "owner@example.com", email_verified: true }, } satisfies NotionProfile; mockedBetterFetch.mockResolvedValueOnce({ data: { object: "user", + type: "bot", id: "notion-bot", name: "Integration Bot", avatar_url: "https://example.com/bot.png", - bot: { owner: { type: "user", user: owner } }, + bot: { + owner: { type: "user", user: owner }, + workspace_name: "Workspace", + workspace_icon: null, + workspace_id: "workspace-id", + }, }, error: null, }); @@ -489,6 +496,9 @@ describe("notion owner profiles", () => { expectTypeOf(profile.avatar_url).toEqualTypeOf< string | null | undefined >(); + expectTypeOf(profile.person?.email_verified).toEqualTypeOf< + boolean | undefined + >(); return {}; }); const provider = notion({ ...credentials, mapProfileToUser }); @@ -521,15 +531,25 @@ describe("notion owner profiles", () => { id: "notion-owner", } satisfies NotionProfile; mockedBetterFetch.mockResolvedValueOnce({ - data: { bot: { owner: { type: "user", user: owner } } }, + data: { + object: "user", + id: "notion-bot", + type: "bot", + name: "Integration Bot", + avatar_url: null, + bot: { + owner: { type: "user", user: owner }, + workspace_name: "Workspace", + workspace_icon: null, + workspace_id: "workspace-id", + }, + }, error: null, }); const provider = notion({ ...credentials, mapProfileToUser(profile) { - expectTypeOf(profile.type).toEqualTypeOf< - "person" | "bot" | undefined - >(); + expectTypeOf(profile.type).toEqualTypeOf<"person" | undefined>(); return { name: profile.name ?? "Mapped Owner", email: profile.person?.email ?? `${profile.id}@example.com`,