diff --git a/.changeset/email-link-session-reverification.md b/.changeset/email-link-session-reverification.md new file mode 100644 index 00000000000..9e2ba6502fe --- /dev/null +++ b/.changeset/email-link-session-reverification.md @@ -0,0 +1,8 @@ +--- +'@clerk/shared': patch +'@clerk/clerk-js': patch +'@clerk/ui': patch +'@clerk/localizations': patch +--- + +Support email-link first factors in session reverification. The original tab waits for the link callback, then resumes the protected action without changing the configured authentication strategy. diff --git a/integration/testUtils/emailService.ts b/integration/testUtils/emailService.ts index c1cb085494d..55078bc6fa6 100644 --- a/integration/testUtils/emailService.ts +++ b/integration/testUtils/emailService.ts @@ -1,71 +1,86 @@ -import { runWithExponentialBackOff } from '@clerk/shared/utils'; - type Message = { _id: string; + links: string[]; subject: string; }; -export const createEmailService = () => { - const cleanEmail = (email: string) => { - return email.replace(/\+.*@/, '@'); +type InboxPageData = { + props?: { + pageProps?: { + seedInboxMessages?: unknown[]; + }; }; +}; - const fetcher = async (url: string | URL, init?: RequestInit) => { - const headers = new Headers(init?.headers || {}); - return fetch(url, { ...init, headers }); - }; +const consumedMessageIds = new Set(); +function isMessage(value: unknown): value is Message { + return ( + typeof value === 'object' && + value !== null && + '_id' in value && + typeof value._id === 'string' && + 'links' in value && + Array.isArray(value.links) && + value.links.every(link => typeof link === 'string') && + 'subject' in value && + typeof value.subject === 'string' + ); +} + +export const createEmailService = () => { const filterMessagesByAddress = async (email: string, sub?: string) => { - const url = new URL('https://mailsac.com/api/inbox-filter'); - url.searchParams.set('andTo', email); - if (sub) { - url.searchParams.set('andSubjectIncludes', sub); - } + const url = new URL(`https://mailsac.com/inbox/${encodeURIComponent(email)}`); // Retry in case the email delivery is delayed await new Promise(res => setTimeout(res, 1500)); - return runWithExponentialBackOff( - async () => { - const res = await fetcher(url); - const json = (await res.json()) as unknown as { messages: Message[] }; - const message = json.messages[0]; + for (let attempt = 0; attempt < 20; attempt++) { + try { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Email inbox request failed with status ${res.status}`); + } + const html = await res.text(); + const nextData = html.match(/