diff --git a/.changeset/afraid-countries-smash.md b/.changeset/afraid-countries-smash.md new file mode 100644 index 00000000000..649af7084a4 --- /dev/null +++ b/.changeset/afraid-countries-smash.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Pass dev_browser to AP via query param, fix AP origin detection util diff --git a/packages/clerk-js/src/core/clerk.test.ts b/packages/clerk-js/src/core/clerk.test.ts index ce4ef203cd3..bdeeece80c9 100644 --- a/packages/clerk-js/src/core/clerk.test.ts +++ b/packages/clerk-js/src/core/clerk.test.ts @@ -1494,5 +1494,41 @@ describe('Clerk singleton', () => { const url = sut.buildUrlWithAuth('foo'); expect(url).toBe('foo'); }); + + it('uses the hash to propagate the dev_browser JWT by default on dev', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://example.com/some-path'); + expect(url).toBe('https://example.com/some-path#__clerk_db_jwt[deadbeef]'); + }); + + it('uses the query param to propagate the dev_browser JWT if specified by option on dev', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://example.com/some-path', { useQueryParam: true }); + expect(url).toBe('https://example.com/some-path?__dev_session=deadbeef'); + }); + + it('uses the query param to propagate the dev_browser JWT to Account Portal pages on dev - non-kima', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://accounts.abcef.12345.dev.lclclerk.com'); + expect(url).toBe('https://accounts.abcef.12345.dev.lclclerk.com/?__dev_session=deadbeef'); + }); + + it('uses the query param to propagate the dev_browser JWT to Account Portal pages on dev - kima', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://rested-anemone-14.accounts.dev'); + expect(url).toBe('https://rested-anemone-14.accounts.dev/?__dev_session=deadbeef'); + }); }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index ed0f6e621d4..0e84b311b94 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -65,7 +65,7 @@ import { ignoreEventValue, inActiveBrowserTab, inBrowser, - isAccountsHostedPages, + isDevAccountPortalOrigin, isDevOrStagingUrl, isError, isRedirectForFAPIInitiatedFlow, @@ -668,9 +668,10 @@ export default class Clerk implements ClerkInterface { return clerkMissingDevBrowserJwt(); } - const asQueryParam = !!options?.useQueryParam; + // Use query param for Account Portal pages so that SSR can access the dev_browser JWT + const asQueryParam = !!options?.useQueryParam || isDevAccountPortalOrigin(toURL.hostname); - return setDevBrowserJWTInURL(toURL.href, devBrowserJwt, asQueryParam); + return setDevBrowserJWTInURL(toURL, devBrowserJwt, asQueryParam).href; } public buildSignInUrl(options?: RedirectOptions): string { @@ -1231,7 +1232,7 @@ export default class Clerk implements ClerkInterface { this.#authService = new SessionCookieService(this); this.#pageLifecycle = createPageLifecycle(); - const isInAccountsHostedPages = isAccountsHostedPages(window?.location.hostname); + const isInAccountsHostedPages = isDevAccountPortalOrigin(window?.location.hostname); this.#setupListeners(); diff --git a/packages/clerk-js/src/core/devBrowserHandler.ts b/packages/clerk-js/src/core/devBrowserHandler.ts index fc28558f8e3..9f79a1c6e2f 100644 --- a/packages/clerk-js/src/core/devBrowserHandler.ts +++ b/packages/clerk-js/src/core/devBrowserHandler.ts @@ -103,7 +103,7 @@ export default function createDevBrowserHandler({ async function setUrlBasedSessionSyncBrowser(): Promise { // 1. Get the JWT from hash search parameters when the redirection comes from Clerk Hosted Pages - const devBrowserToken = getDevBrowserJWTFromURL(window.location.href); + const devBrowserToken = getDevBrowserJWTFromURL(new URL(window.location.href)); if (devBrowserToken) { setDevBrowserJWT(devBrowserToken); return; diff --git a/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts b/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts index 526ed84731b..13a24e86beb 100644 --- a/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts +++ b/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts @@ -1,5 +1,7 @@ import { getDevBrowserJWTFromURL, setDevBrowserJWTInURL } from '../devBrowser'; +const DUMMY_URL_BASE = 'http://clerk-dummy'; + describe('setDevBrowserJWTInURL(url, jwt)', () => { const testCases: Array<[string, string, boolean, string]> = [ ['', 'deadbeef', false, '#__clerk_db_jwt[deadbeef]'], @@ -15,8 +17,10 @@ describe('setDevBrowserJWTInURL(url, jwt)', () => { test.each(testCases)( 'sets the dev browser JWT at the end of the provided url. Params: url=(%s), jwt=(%s), expected url=(%s)', - (hash, paramName, asQueryParam, expectedUrl) => { - expect(setDevBrowserJWTInURL(hash, paramName, asQueryParam)).toEqual(expectedUrl); + (input, paramName, asQueryParam, expected) => { + expect(setDevBrowserJWTInURL(new URL(input, DUMMY_URL_BASE), paramName, asQueryParam).href).toEqual( + new URL(expected, DUMMY_URL_BASE).href, + ); }, ); }); @@ -42,7 +46,7 @@ describe('getDevBrowserJWTFromURL(url,)', () => { }); it('does not replaceState if the url does not contain a dev browser JWT', () => { - expect(getDevBrowserJWTFromURL('/foo')).toEqual(''); + expect(getDevBrowserJWTFromURL(new URL('/foo', DUMMY_URL_BASE))).toEqual(''); expect(replaceStateMock).not.toHaveBeenCalled(); }); @@ -56,12 +60,16 @@ describe('getDevBrowserJWTFromURL(url,)', () => { ['/foo?bar=42#qux__clerk_db_jwt[deadbeef]', 'deadbeef', '/foo?bar=42#qux'], ]; - test.each(testCases)('returns the dev browser JWT from a url. Params: url=(%s), jwt=(%s)', (url, jwt, calledWith) => { - expect(getDevBrowserJWTFromURL(url)).toEqual(jwt); - if (calledWith === null) { - expect(replaceStateMock).not.toHaveBeenCalled(); - } else { - expect(replaceStateMock).toHaveBeenCalledWith(null, '', calledWith); - } - }); + test.each(testCases)( + 'returns the dev browser JWT from a url. Params: url=(%s), jwt=(%s)', + (input, jwt, calledWith) => { + expect(getDevBrowserJWTFromURL(new URL(input, DUMMY_URL_BASE))).toEqual(jwt); + + if (calledWith === null) { + expect(replaceStateMock).not.toHaveBeenCalled(); + } else { + expect(replaceStateMock).toHaveBeenCalledWith(null, '', new URL(calledWith, DUMMY_URL_BASE).href); + } + }, + ); }); diff --git a/packages/clerk-js/src/utils/__tests__/url.test.ts b/packages/clerk-js/src/utils/__tests__/url.test.ts index e8646171634..588676d3f15 100644 --- a/packages/clerk-js/src/utils/__tests__/url.test.ts +++ b/packages/clerk-js/src/utils/__tests__/url.test.ts @@ -8,9 +8,9 @@ import { getSearchParameterFromHash, hasBannedProtocol, hasExternalAccountSignUpError, - isAccountsHostedPages, isAllowedRedirectOrigin, isDataUri, + isDevAccountPortalOrigin, isRedirectForFAPIInitiatedFlow, isValidUrl, mergeFragmentIntoUrl, @@ -18,22 +18,22 @@ import { trimTrailingSlash, } from '../url'; -describe('isAccountsHostedPages(url)', () => { +describe('isDevAccountPortalOrigin(url)', () => { const goodUrls: Array<[string | URL, boolean]> = [ ['clerk.dev.lclclerk.com', false], ['clerk.prod.lclclerk.com', false], ['clerk.abc.efg.lclstage.dev', false], ['clerk.abc.efg.stgstage.dev', false], ['accounts.abc.efg.dev.lclclerk.com', true], - ['https://accounts.abc.efg.stg.lclclerk.com', true], - [new URL('https://clerk.abc.efg.lcl.dev'), false], - [new URL('https://accounts.abc.efg.lcl.dev'), true], - [new URL('https://accounts.abc.efg.stg.dev'), true], + ['rested-anemone-14.accounts.dev', true], + ['rested-anemone-14.accounts.dev.accountsstage.dev', true], + ['rested-anemone-14.accounts.dev.accounts.lclclerk.com', true], + ['rested-anemone-14.clerk.accounts.dev', false], ]; - test.each(goodUrls)('.isAccountsHostedPages(%s)', (a, expected) => { + test.each(goodUrls)('.isDevAccountPortalOrigin(%s)', (a, expected) => { // @ts-ignore - expect(isAccountsHostedPages(a)).toBe(expected); + expect(isDevAccountPortalOrigin(a)).toBe(expected); }); }); diff --git a/packages/clerk-js/src/utils/devBrowser.ts b/packages/clerk-js/src/utils/devBrowser.ts index 5bce5cde1bd..8352c858b52 100644 --- a/packages/clerk-js/src/utils/devBrowser.ts +++ b/packages/clerk-js/src/utils/devBrowser.ts @@ -3,41 +3,62 @@ import { DEV_BROWSER_SSO_JWT_PARAMETER } from '../core/constants'; export const DEV_BROWSER_JWT_MARKER = '__clerk_db_jwt'; const DEV_BROWSER_JWT_MARKER_REGEXP = /__clerk_db_jwt\[(.*)\]/; -function extractDevBrowserJWT(url: string): string { - const matches = url.match(DEV_BROWSER_JWT_MARKER_REGEXP); - return matches ? matches[1] : ''; -} +// Sets the dev_browser JWT in the hash or the search +export function setDevBrowserJWTInURL(url: URL, jwt: string, asQueryParam: boolean): URL { + const resultURL = new URL(url); -export function setDevBrowserJWTInURL(url: string, jwt: string, asQueryParam: boolean): string { - if (asQueryParam) { - const hasQueryParam = (url || '').includes('?'); - return `${url}${hasQueryParam ? '&' : '?'}${DEV_BROWSER_SSO_JWT_PARAMETER}=${(jwt || '').trim()}`; + // extract & strip existing jwt from hash + const jwtFromHash = extractDevBrowserJWTFromHash(resultURL.hash); + resultURL.hash = resultURL.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); + if (resultURL.href.endsWith('#')) { + resultURL.hash = ''; } - const dbJwt = extractDevBrowserJWT(url); - if (dbJwt) { - url.replace(`${DEV_BROWSER_JWT_MARKER}[${dbJwt}]`, jwt); - return url; + // extract & strip existing jwt from search + const jwtFromSearch = resultURL.searchParams.get(DEV_BROWSER_SSO_JWT_PARAMETER); + resultURL.searchParams.delete(DEV_BROWSER_SSO_JWT_PARAMETER); + + // Existing jwt takes precedence + const jwtToSet = jwtFromHash || jwtFromSearch || jwt; + + if (jwtToSet) { + if (asQueryParam) { + resultURL.searchParams.append(DEV_BROWSER_SSO_JWT_PARAMETER, jwtToSet); + } else { + resultURL.hash = resultURL.hash + `${DEV_BROWSER_JWT_MARKER}[${jwtToSet}]`; + } } - const hasHash = (url || '').includes('#'); - return `${url}${hasHash ? '' : '#'}${DEV_BROWSER_JWT_MARKER}[${(jwt || '').trim()}]`; + + return resultURL; } -export function getDevBrowserJWTFromURL(url: string): string { - const jwt = extractDevBrowserJWT(url); - if (!jwt) { - return ''; +// Gets the dev_browser JWT from either the hash or the search +// Side effect: +// Removes dev_browser JWT from the URL as a side effect and updates the browser history +export function getDevBrowserJWTFromURL(url: URL): string { + const resultURL = new URL(url); + + // extract & strip existing jwt from hash + const jwtFromHash = extractDevBrowserJWTFromHash(resultURL.hash); + resultURL.hash = resultURL.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); + if (resultURL.href.endsWith('#')) { + resultURL.hash = ''; } - let newUrl = url.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); + // extract & strip existing jwt from search + const jwtFromSearch = resultURL.searchParams.get(DEV_BROWSER_SSO_JWT_PARAMETER) || ''; + resultURL.searchParams.delete(DEV_BROWSER_SSO_JWT_PARAMETER); - if (newUrl.endsWith('#')) { - newUrl = newUrl.slice(0, -1); - } + const jwt = jwtFromHash || jwtFromSearch; - if (typeof globalThis.history !== undefined) { - globalThis.history.replaceState(null, '', newUrl); + if (jwt && typeof globalThis.history !== undefined) { + globalThis.history.replaceState(null, '', resultURL.href); } return jwt; } + +function extractDevBrowserJWTFromHash(hash: string): string { + const matches = hash.match(DEV_BROWSER_JWT_MARKER_REGEXP); + return matches ? matches[1] : ''; +} diff --git a/packages/clerk-js/src/utils/url.ts b/packages/clerk-js/src/utils/url.ts index 21c97bb7389..be381c01093 100644 --- a/packages/clerk-js/src/utils/url.ts +++ b/packages/clerk-js/src/utils/url.ts @@ -27,26 +27,52 @@ export const DEV_OR_STAGING_SUFFIXES = [ 'accounts.dev', ]; +export const LEGACY_DEV_SUFFIXES = ['.lcl.dev', '.lclstage.dev', '.lclclerk.com']; +export const CURRENT_DEV_SUFFIXES = ['.accounts.dev', '.accountsstage.dev', '.accounts.lclclerk.com']; + const BANNED_URI_PROTOCOLS = ['javascript:'] as const; const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); export { isDevOrStagingUrl }; -const accountsCache = new Map(); +const accountPortalCache = new Map(); -export function isAccountsHostedPages(url: string | URL = window.location.hostname): boolean { - if (!url) { +export function isDevAccountPortalOrigin(hostname: string = window.location.hostname): boolean { + if (!hostname) { return false; } - const hostname = typeof url === 'string' ? url : url.hostname; - let res = accountsCache.get(hostname); + let res = accountPortalCache.get(hostname); + if (res === undefined) { - res = DEV_OR_STAGING_SUFFIXES.some(s => /^(https?:\/\/)?accounts\./.test(hostname) && hostname.endsWith(s)); - accountsCache.set(hostname, res); + res = isLegacyDevAccountPortalOrigin(hostname) || isCurrentDevAccountPortalOrigin(hostname); + accountPortalCache.set(hostname, res); } + return res; } +// Returns true for hosts such as: +// * accounts.foo.bar-13.lcl.dev +// * accounts.foo.bar-13.lclstage.dev +// * accounts.foo.bar-13.dev.lclclerk.com +function isLegacyDevAccountPortalOrigin(host: string): boolean { + return LEGACY_DEV_SUFFIXES.some(legacyDevSuffix => { + return host.startsWith('accounts.') && host.endsWith(legacyDevSuffix); + }); +} + +// Returns true for hosts such as: +// * foo-bar-13.accounts.dev +// * foo-bar-13.accountsstage.dev +// * foo-bar-13.accounts.lclclerk.com +// But false for: +// * foo-bar-13.clerk.accounts.lclclerk.com +function isCurrentDevAccountPortalOrigin(host: string): boolean { + return CURRENT_DEV_SUFFIXES.some(currentDevSuffix => { + return host.endsWith(currentDevSuffix) && !host.endsWith('.clerk' + currentDevSuffix); + }); +} + export function getETLDPlusOneFromFrontendApi(frontendApi: string): string { return frontendApi.replace('clerk.', ''); } diff --git a/packages/nextjs/src/server/authMiddleware.test.ts b/packages/nextjs/src/server/authMiddleware.test.ts index 5fb29a33237..72715e23709 100644 --- a/packages/nextjs/src/server/authMiddleware.test.ts +++ b/packages/nextjs/src/server/authMiddleware.test.ts @@ -456,14 +456,14 @@ describe('Dev Browser JWT when redirecting to cross origin', function () { expect(authenticateRequest).toBeCalled(); }); - it('appends the Dev Browser JWT on the URL when cookie __clerk_db_jwt exists', async () => { + it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { const resp = await authMiddleware({ beforeAuth: () => NextResponse.next(), })(mockRequest({ url: '/protected', appendDevBrowserCookie: true }), {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected#__clerk_db_jwt[test_jwt]', + 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__dev_session=test_jwt', ); expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); expect(authenticateRequest).toBeCalled(); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index 024ced0ec0a..edc13fb837c 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -12,6 +12,7 @@ import { DEV_BROWSER_JWT_MARKER, setDevBrowserJWTInURL } from './devBrowser'; import { infiniteRedirectLoopDetected, informAboutProtectedRouteInfo, receivedRequestForIgnoredRoute } from './errors'; import { redirectToSignIn } from './redirect'; import type { NextMiddlewareResult, WithAuthOptions } from './types'; +import { isDevAccountPortalOrigin } from './url'; import { apiEndpointUnauthorizedNextResponse, decorateRequest, @@ -282,16 +283,26 @@ const withDefaultPublicRoutes = (publicRoutes: RouteMatcherParam | undefined) => // Middleware runs on the server side, before clerk-js is loaded, that's why we need Cookies. const appendDevBrowserOnCrossOrigin = (req: WithClerkUrl, res: Response, opts: AuthMiddlewareParams) => { const location = res.headers.get('location'); + const shouldAppendDevBrowser = res.headers.get(constants.Headers.ClerkRedirectTo) === 'true'; + if ( shouldAppendDevBrowser && !!location && isDevelopmentFromApiKey(opts.secretKey || SECRET_KEY) && isCrossOrigin(req.experimental_clerkUrl, location) ) { - const dbJwt = req.cookies.get(DEV_BROWSER_JWT_MARKER)?.value; - const urlWithDevBrowser = setDevBrowserJWTInURL(location, dbJwt); - return NextResponse.redirect(urlWithDevBrowser, res); + const dbJwt = req.cookies.get(DEV_BROWSER_JWT_MARKER)?.value || ''; + + // Next.js 12.1+ allows redirects only to absolute URLs + const url = new URL(location); + + // Use query param for Account Portal pages so that SSR can access the dev_browser JWT + const asQueryParam = isDevAccountPortalOrigin(url.hostname); + + const urlWithDevBrowser = setDevBrowserJWTInURL(url, dbJwt, asQueryParam); + + return NextResponse.redirect(urlWithDevBrowser.href, res); } return res; }; diff --git a/packages/nextjs/src/server/devBrowser.ts b/packages/nextjs/src/server/devBrowser.ts index 668760a105d..5a7074f8f7a 100644 --- a/packages/nextjs/src/server/devBrowser.ts +++ b/packages/nextjs/src/server/devBrowser.ts @@ -1,23 +1,45 @@ -// TODO: This is a duplicate of part of packages/clerk-js/src/utils/devBrowser.ts +// TODO: This is a partial duplicate of part of packages/clerk-js/src/utils/devBrowser.ts // TODO: To be removed when we can extract this utility to @clerk/shared + +export const DEV_BROWSER_SSO_JWT_PARAMETER = '__dev_session'; + +// +// Below this line should be identical to clerk-js version +// + export const DEV_BROWSER_JWT_MARKER = '__clerk_db_jwt'; const DEV_BROWSER_JWT_MARKER_REGEXP = /__clerk_db_jwt\[(.*)\]/; -function extractDevBrowserJWT(url: string): string { - const matches = url.match(DEV_BROWSER_JWT_MARKER_REGEXP); - return matches ? matches[1] : ''; -} +// Sets the dev_browser JWT in the hash or the search +export function setDevBrowserJWTInURL(url: URL, jwt: string, asQueryParam: boolean): URL { + const resultURL = new URL(url); -export function setDevBrowserJWTInURL(url: string, jwt?: string): string { - if (!jwt) { - return url; + // extract & strip existing jwt from hash + const jwtFromHash = extractDevBrowserJWTFromHash(resultURL.hash); + resultURL.hash = resultURL.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); + if (resultURL.href.endsWith('#')) { + resultURL.hash = ''; } - const dbJwt = extractDevBrowserJWT(url); - if (dbJwt) { - url.replace(`${DEV_BROWSER_JWT_MARKER}[${dbJwt}]`, jwt); - return url; + // extract & strip existing jwt from search + const jwtFromSearch = resultURL.searchParams.get(DEV_BROWSER_SSO_JWT_PARAMETER); + resultURL.searchParams.delete(DEV_BROWSER_SSO_JWT_PARAMETER); + + // Existing jwt takes precedence + const jwtToSet = jwtFromHash || jwtFromSearch || jwt; + + if (jwtToSet) { + if (asQueryParam) { + resultURL.searchParams.append(DEV_BROWSER_SSO_JWT_PARAMETER, jwtToSet); + } else { + resultURL.hash = resultURL.hash + `${DEV_BROWSER_JWT_MARKER}[${jwtToSet}]`; + } } - const hasHash = (url || '').includes('#'); - return `${url}${hasHash ? '' : '#'}${DEV_BROWSER_JWT_MARKER}[${(jwt || '').trim()}]`; + + return resultURL; +} + +function extractDevBrowserJWTFromHash(hash: string): string { + const matches = hash.match(DEV_BROWSER_JWT_MARKER_REGEXP); + return matches ? matches[1] : ''; } diff --git a/packages/nextjs/src/server/url.ts b/packages/nextjs/src/server/url.ts new file mode 100644 index 00000000000..a659c3fbcd8 --- /dev/null +++ b/packages/nextjs/src/server/url.ts @@ -0,0 +1,44 @@ +// TODO: This is a partial duplicate of part of packages/clerk-js/src/utils/url.ts +// TODO: To be removed when we can extract this utility to @clerk/shared + +export const LEGACY_DEV_SUFFIXES = ['.lcl.dev', '.lclstage.dev', '.lclclerk.com']; +export const CURRENT_DEV_SUFFIXES = ['.accounts.dev', '.accountsstage.dev', '.accounts.lclclerk.com']; + +const accountPortalCache = new Map(); + +export function isDevAccountPortalOrigin(hostname: string): boolean { + if (!hostname) { + return false; + } + + let res = accountPortalCache.get(hostname); + + if (res === undefined) { + res = isLegacyDevAccountPortalOrigin(hostname) || isCurrentDevAccountPortalOrigin(hostname); + accountPortalCache.set(hostname, res); + } + + return res; +} + +// Returns true for hosts such as: +// * accounts.foo.bar-13.lcl.dev +// * accounts.foo.bar-13.lclstage.dev +// * accounts.foo.bar-13.dev.lclclerk.com +function isLegacyDevAccountPortalOrigin(host: string): boolean { + return LEGACY_DEV_SUFFIXES.some(legacyDevSuffix => { + return host.startsWith('accounts.') && host.endsWith(legacyDevSuffix); + }); +} + +// Returns true for hosts such as: +// * foo-bar-13.accounts.dev +// * foo-bar-13.accountsstage.dev +// * foo-bar-13.accounts.lclclerk.com +// But false for: +// * foo-bar-13.clerk.accounts.lclclerk.com +function isCurrentDevAccountPortalOrigin(host: string): boolean { + return CURRENT_DEV_SUFFIXES.some(currentDevSuffix => { + return host.endsWith(currentDevSuffix) && !host.endsWith('.clerk' + currentDevSuffix); + }); +}