diff --git a/.changeset/perfect-monkeys-beg.md b/.changeset/perfect-monkeys-beg.md new file mode 100644 index 00000000000..98b35296446 --- /dev/null +++ b/.changeset/perfect-monkeys-beg.md @@ -0,0 +1,40 @@ +--- +'@clerk/chrome-extension': major +'@clerk/shared': patch +--- + +Expand the ability for `@clerk/chrome-extension` WebSSO to sync with host applications which use URL-based session syncing. + +### How to Update + +**WebSSO Local Host Permissions:** + +Add `*://localhost:*/*` to the `host_permissions` array in your `manifest.json` file: +```json +{ + "host_permissions": ["*://localhost:*/*"] +} +``` + +If you're using a local domain other than `localhost`, you'll want replace that entry with your domain: `*:///*` + +```json +{ + "host_permissions": ["*:///*"] +} +``` + +**WebSSO Provider settings:** + +```tsx + navigate(to)} + routerReplace={to => navigate(to, { replace: true })} + syncSessionWithTab + + // [Development Only]: If the host application isn't on + // `http://localhost`, you can provide it here: + syncSessionHost="http://" +> +``` diff --git a/package-lock.json b/package-lock.json index b95d0b128f6..ef49ffdb0e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8187,9 +8187,10 @@ "license": "MIT" }, "node_modules/@types/chrome": { - "version": "0.0.237", + "version": "0.0.253", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.253.tgz", + "integrity": "sha512-ZnBlbeoje0XaBrJbFCXI8DsDfqvqdoWQO5NSGecMCHFC8W8z/rb/n7lI1FHob+TFKKLR4L2c3QJJSFLwtVc9TA==", "dev": true, - "license": "MIT", "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" @@ -33280,16 +33281,16 @@ }, "packages/backend": { "name": "@clerk/backend", - "version": "1.0.0-alpha-v5.4", + "version": "1.0.0-alpha-v5.5", "license": "MIT", "dependencies": { - "@clerk/shared": "2.0.0-alpha-v5.3", + "@clerk/shared": "2.0.0-alpha-v5.4", "cookie": "0.5.0", "snakecase-keys": "5.4.4", "tslib": "2.4.1" }, "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@cloudflare/workers-types": "^3.18.0", "@types/chai": "^4.3.3", "@types/cookie": "^0.5.1", @@ -33319,14 +33320,15 @@ }, "packages/chrome-extension": { "name": "@clerk/chrome-extension", - "version": "1.0.0-alpha-v5.6", + "version": "1.0.0-alpha-v5.7", "license": "MIT", "dependencies": { - "@clerk/clerk-js": "5.0.0-alpha-v5.6", - "@clerk/clerk-react": "5.0.0-alpha-v5.6" + "@clerk/clerk-js": "*", + "@clerk/clerk-react": "*", + "@clerk/shared": "*" }, "devDependencies": { - "@types/chrome": "*", + "@types/chrome": "latest", "@types/node": "^18.17.0", "@types/react": "*", "@types/react-dom": "*", @@ -33344,12 +33346,12 @@ }, "packages/clerk-js": { "name": "@clerk/clerk-js", - "version": "5.0.0-alpha-v5.6", + "version": "5.0.0-alpha-v5.7", "license": "MIT", "dependencies": { "@clerk/localizations": "2.0.0-alpha-v5.5", - "@clerk/shared": "2.0.0-alpha-v5.3", - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/shared": "2.0.0-alpha-v5.4", + "@clerk/types": "4.0.0-alpha-v5.7", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", "@floating-ui/react": "0.25.4", @@ -33684,17 +33686,17 @@ }, "packages/expo": { "name": "@clerk/clerk-expo", - "version": "1.0.0-alpha-v5.6", + "version": "1.0.0-alpha-v5.7", "license": "MIT", "dependencies": { - "@clerk/clerk-js": "5.0.0-alpha-v5.6", - "@clerk/clerk-react": "5.0.0-alpha-v5.6", - "@clerk/shared": "2.0.0-alpha-v5.3", + "@clerk/clerk-js": "5.0.0-alpha-v5.7", + "@clerk/clerk-react": "5.0.0-alpha-v5.7", + "@clerk/shared": "2.0.0-alpha-v5.4", "base-64": "^1.0.0", "react-native-url-polyfill": "2.0.0" }, "devDependencies": { - "@clerk/types": "^4.0.0-alpha-v5.6", + "@clerk/types": "^4.0.0-alpha-v5.7", "@types/base-64": "^1.0.2", "@types/node": "^18.17.0", "@types/react": "*", @@ -33717,12 +33719,12 @@ }, "packages/fastify": { "name": "@clerk/fastify", - "version": "1.0.0-alpha-v5.6", + "version": "1.0.0-alpha-v5.7", "license": "MIT", "dependencies": { - "@clerk/backend": "1.0.0-alpha-v5.4", - "@clerk/shared": "2.0.0-alpha-v5.3", - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/backend": "1.0.0-alpha-v5.5", + "@clerk/shared": "2.0.0-alpha-v5.4", + "@clerk/types": "4.0.0-alpha-v5.7", "cookies": "0.8.0" }, "devDependencies": { @@ -33740,17 +33742,17 @@ } }, "packages/gatsby-plugin-clerk": { - "version": "5.0.0-alpha-v5.6", + "version": "5.0.0-alpha-v5.7", "license": "MIT", "dependencies": { - "@clerk/backend": "1.0.0-alpha-v5.4", - "@clerk/clerk-react": "5.0.0-alpha-v5.6", - "@clerk/clerk-sdk-node": "5.0.0-alpha-v5.4", + "@clerk/backend": "1.0.0-alpha-v5.5", + "@clerk/clerk-react": "5.0.0-alpha-v5.7", + "@clerk/clerk-sdk-node": "5.0.0-alpha-v5.5", "cookie": "0.5.0", "tslib": "2.4.1" }, "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@types/cookie": "^0.5.0", "@types/node": "^18.17.0", "eslint-config-custom": "*", @@ -33773,7 +33775,7 @@ "version": "2.0.0-alpha-v5.5", "license": "MIT", "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@types/node": "^18.17.0", "eslint-config-custom": "*", "tsup": "*", @@ -33789,16 +33791,16 @@ }, "packages/nextjs": { "name": "@clerk/nextjs", - "version": "5.0.0-alpha-v5.6", + "version": "5.0.0-alpha-v5.7", "license": "MIT", "dependencies": { - "@clerk/backend": "1.0.0-alpha-v5.4", - "@clerk/clerk-react": "5.0.0-alpha-v5.6", - "@clerk/shared": "2.0.0-alpha-v5.3", + "@clerk/backend": "1.0.0-alpha-v5.5", + "@clerk/clerk-react": "5.0.0-alpha-v5.7", + "@clerk/shared": "2.0.0-alpha-v5.4", "path-to-regexp": "6.2.1" }, "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@types/node": "^18.17.0", "@types/react": "*", "@types/react-dom": "*", @@ -33823,11 +33825,11 @@ }, "packages/react": { "name": "@clerk/clerk-react", - "version": "5.0.0-alpha-v5.6", + "version": "5.0.0-alpha-v5.7", "license": "MIT", "dependencies": { - "@clerk/shared": "2.0.0-alpha-v5.3", - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/shared": "2.0.0-alpha-v5.4", + "@clerk/types": "4.0.0-alpha-v5.7", "eslint-config-custom": "*", "semver": "^7.5.4", "tslib": "2.4.1" @@ -33855,17 +33857,17 @@ }, "packages/remix": { "name": "@clerk/remix", - "version": "4.0.0-alpha-v5.6", + "version": "4.0.0-alpha-v5.7", "license": "MIT", "dependencies": { - "@clerk/backend": "1.0.0-alpha-v5.4", - "@clerk/clerk-react": "5.0.0-alpha-v5.6", - "@clerk/shared": "2.0.0-alpha-v5.3", + "@clerk/backend": "1.0.0-alpha-v5.5", + "@clerk/clerk-react": "5.0.0-alpha-v5.7", + "@clerk/shared": "2.0.0-alpha-v5.4", "cookie": "0.5.0", "tslib": "2.4.1" }, "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@remix-run/react": "^2.0.0", "@remix-run/server-runtime": "^2.0.0", "@types/cookie": "^0.5.0", @@ -33891,16 +33893,16 @@ }, "packages/sdk-node": { "name": "@clerk/clerk-sdk-node", - "version": "5.0.0-alpha-v5.4", + "version": "5.0.0-alpha-v5.5", "license": "MIT", "dependencies": { - "@clerk/backend": "1.0.0-alpha-v5.4", - "@clerk/shared": "2.0.0-alpha-v5.3", + "@clerk/backend": "1.0.0-alpha-v5.5", + "@clerk/shared": "2.0.0-alpha-v5.4", "camelcase-keys": "6.2.2", "snakecase-keys": "3.2.1" }, "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@types/express": "4.17.14", "@types/node": "^18.17.0", "eslint-config-custom": "*", @@ -33933,7 +33935,7 @@ }, "packages/shared": { "name": "@clerk/shared", - "version": "2.0.0-alpha-v5.3", + "version": "2.0.0-alpha-v5.4", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -33942,7 +33944,7 @@ "swr": "2.2.0" }, "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@types/glob-to-regexp": "0.4.1", "@types/js-cookie": "3.0.2", "@types/node": "^18.17.0", @@ -33978,7 +33980,7 @@ "version": "2.0.0-alpha-v5.2", "license": "MIT", "devDependencies": { - "@clerk/types": "4.0.0-alpha-v5.6", + "@clerk/types": "4.0.0-alpha-v5.7", "@types/node": "^18.17.0", "eslint-config-custom": "*", "typescript": "*" @@ -33993,7 +33995,7 @@ }, "packages/types": { "name": "@clerk/types", - "version": "4.0.0-alpha-v5.6", + "version": "4.0.0-alpha-v5.7", "license": "MIT", "dependencies": { "csstype": "3.1.1" diff --git a/packages/chrome-extension/README.md b/packages/chrome-extension/README.md index bae26037817..5ed78923bc4 100644 --- a/packages/chrome-extension/README.md +++ b/packages/chrome-extension/README.md @@ -134,19 +134,34 @@ The 2 supported cases (links to different branches of the same repository): ## WebSSO required settings -### Permissions (in manifest.json) +### Extension Manifest (`manifest.json`) -- "cookies" for more info see (here)[https://developer.chrome.com/docs/extensions/reference/cookies/] -- "storage" for more info see (here)[https://developer.chrome.com/docs/extensions/reference/storage/] +#### Permissions -### Host permissions (in manifest.json) - -You will need your Frontend API URL, which can be found in your `Dashboard > API Keys > Advanced > Clerk API URLs`. +You must enable the following permissions in your `manifest.json` file: ``` -"host_permissions": ["*://YOUR_CLERK_FRONTEND_API_GOES_HERE/"], +"permissions": ["cookies", "storage"] ``` +- For more info on the "cookies" permission: (Google Developer Cookies Reference)[https://developer.chrome.com/docs/extensions/reference/cookies/] +- For more info on the "storage" permission: (Google Developer Storage Reference)[https://developer.chrome.com/docs/extensions/reference/storage/] + +#### Host Permissions + +You must enable the following host permissions in your `manifest.json` file: + +- **Development:** `"host_permissions": ["*://localhost:*/*"]` + - If you're using a domain other than `localhost`, you'll want replace that entry with your domain: `*:///*` +- **Production:** `"host_permissions": ["*:///"]` + - Your Frontend API URL can be found in `Clerk Dashboard > API Keys > Advanced > Clerk API URLs`. + +For more info on host permissions: (Google Developer `host_permissions` Reference)[https://developer.chrome.com/docs/extensions/mv3/declare_permissions/#host-permissions] + +#### ClerkProvider + +If your plan to sync sessions with a host application not on `localhost`, you'll need to use the `syncSessionHost` prop on `ClerkProvider` to specify the domain you're using, inclusive of the protocol. (eg: `https://`) + ### Clerk settings @@ -160,10 +175,6 @@ curl -X PATCH https://api.clerk.com/v1/instance \ -d '{"allowed_origins": ["chrome-extension://extension_id_goes_here"]}' ``` -## Development - -The `Enable URL-based session syncing` should be `DISABLED` from the `Clerk Dashboard > Setting` for a development instance to support @clerk/chrome-extension functionality. - ## Deploy to Production Setting the `allowed_origins` (check [Clerk Settings](#clerk-settings)) is **REQUIRED** for both **Development** and **Production** instances when using the WebSSO use case. diff --git a/packages/chrome-extension/jest.config.js b/packages/chrome-extension/jest.config.js index 82554df0854..4935c8f83e4 100644 --- a/packages/chrome-extension/jest.config.js +++ b/packages/chrome-extension/jest.config.js @@ -6,7 +6,7 @@ module.exports = { roots: ['/src'], testEnvironment: 'jsdom', - setupFilesAfterEnv: ['../../jest.setup-after-env.ts'], + setupFilesAfterEnv: ['../../jest.setup-after-env.ts', '/jest.setup.ts'], moduleDirectories: ['node_modules', '/src'], transform: { diff --git a/packages/chrome-extension/jest.setup.ts b/packages/chrome-extension/jest.setup.ts new file mode 100644 index 00000000000..d377f09bd5f --- /dev/null +++ b/packages/chrome-extension/jest.setup.ts @@ -0,0 +1,21 @@ +globalThis.chrome = { + cookies: { + // @ts-expect-error - Mock implementation + get: jest.fn(({ url, name }) => `cookies.get:${url}:${name}`), + }, + runtime: { + // @ts-expect-error - Mock implementation + getManifest: jest.fn(() => ({ + permissions: ['cookies', 'storage'], + host_permissions: ['https://*/*'], + })), + }, + storage: { + // @ts-expect-error - Mock implementation + local: { + // @ts-expect-error - Mock implementation + get: jest.fn(async k => Promise.resolve({ [k]: `storage.get:${k}` })), + set: jest.fn(async () => Promise.resolve()), + }, + }, +}; diff --git a/packages/chrome-extension/package.json b/packages/chrome-extension/package.json index e93e89b4901..c01aba6e2b4 100644 --- a/packages/chrome-extension/package.json +++ b/packages/chrome-extension/package.json @@ -45,11 +45,12 @@ "test:coverage": "jest --collectCoverage && open coverage/lcov-report/index.html" }, "dependencies": { - "@clerk/clerk-js": "5.0.0-alpha-v5.7", - "@clerk/clerk-react": "5.0.0-alpha-v5.7" + "@clerk/clerk-js": "*", + "@clerk/clerk-react": "*", + "@clerk/shared": "*" }, "devDependencies": { - "@types/chrome": "*", + "@types/chrome": "latest", "@types/node": "^18.17.0", "@types/react": "*", "@types/react-dom": "*", diff --git a/packages/chrome-extension/src/ClerkProvider.tsx b/packages/chrome-extension/src/ClerkProvider.tsx index 52f613f003b..b3b61df3af9 100644 --- a/packages/chrome-extension/src/ClerkProvider.tsx +++ b/packages/chrome-extension/src/ClerkProvider.tsx @@ -18,10 +18,12 @@ __internal__setErrorThrowerOptions({ type WebSSOClerkProviderCustomProps = | { + syncSessionHost?: never; syncSessionWithTab?: false; tokenCache?: never; } | { + syncSessionHost?: string; syncSessionWithTab: true; tokenCache?: TokenCache; }; @@ -29,7 +31,7 @@ type WebSSOClerkProviderCustomProps = type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps; const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => { - const { children, tokenCache: runtimeTokenCache, ...rest } = props; + const { children, tokenCache: runtimeTokenCache, syncSessionWithTab, ...rest } = props; const { publishableKey = '' } = props; const [clerkInstance, setClerkInstance] = React.useState(null); @@ -41,7 +43,7 @@ const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | nul void (async () => { setClerkInstance(await buildClerk({ publishableKey, tokenCache })); })(); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps if (!clerkInstance) { return null; @@ -64,7 +66,7 @@ const StandaloneClerkProvider = (props: ClerkReactProviderProps): JSX.Element => return ( {children} @@ -74,10 +76,11 @@ const StandaloneClerkProvider = (props: ClerkReactProviderProps): JSX.Element => type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps; export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null { - const { tokenCache, syncSessionWithTab, ...rest } = props; + const { syncSessionHost, tokenCache, syncSessionWithTab, ...rest } = props; return syncSessionWithTab ? ( ) : ( diff --git a/packages/chrome-extension/src/cache.ts b/packages/chrome-extension/src/cache.ts index 597368ffa26..a25cb9107d8 100644 --- a/packages/chrome-extension/src/cache.ts +++ b/packages/chrome-extension/src/cache.ts @@ -1,4 +1,4 @@ -import { getFromStorage, setInStorage } from './utils'; +import { getFromStorage, setInStorage } from './chrome/storage'; export interface TokenCache { getToken: (key: string) => Promise; diff --git a/packages/chrome-extension/src/chrome/cookies.test.ts b/packages/chrome-extension/src/chrome/cookies.test.ts new file mode 100644 index 00000000000..53345c5de01 --- /dev/null +++ b/packages/chrome-extension/src/chrome/cookies.test.ts @@ -0,0 +1,16 @@ +import { CLIENT_JWT_KEY } from '@clerk/shared'; + +import { getClientCookie } from './cookies'; + +describe('chrome.cookies', () => { + describe('getClientCookie(url, name)', () => { + afterEach(() => jest.clearAllMocks()); + + test('returns cookie value from chrome.cookies if is set for url', async () => { + globalThis.chrome; + const url = 'http://localhost:3000'; + + await expect(getClientCookie(url, CLIENT_JWT_KEY)).resolves.toEqual(`cookies.get:${url}:${CLIENT_JWT_KEY}`); + }); + }); +}); diff --git a/packages/chrome-extension/src/chrome/cookies.ts b/packages/chrome-extension/src/chrome/cookies.ts new file mode 100644 index 00000000000..6a45d329ac8 --- /dev/null +++ b/packages/chrome-extension/src/chrome/cookies.ts @@ -0,0 +1,3 @@ +export async function getClientCookie(url: string, key: string) { + return chrome.cookies.get({ url, name: key }); +} diff --git a/packages/chrome-extension/src/chrome/runtime.test.ts b/packages/chrome-extension/src/chrome/runtime.test.ts new file mode 100644 index 00000000000..3c913d8e112 --- /dev/null +++ b/packages/chrome-extension/src/chrome/runtime.test.ts @@ -0,0 +1,51 @@ +import { validateManifest } from './runtime'; + +describe('chrome.runtime', () => { + describe('validateManifest(manifest)', () => { + afterEach(() => jest.clearAllMocks()); + + test('valid configuration', async () => { + expect(() => validateManifest(chrome.runtime.getManifest())).not.toThrowError(); + }); + + describe('invalid configuration', () => { + describe('permissions', () => { + test('missing root key', async () => { + const manifest = { + host_permissions: ['https://*/*'], + } as chrome.runtime.Manifest; + + expect(() => validateManifest(manifest)).toThrowError(/^Missing `permissions`/); + }); + + test('missing cookies', async () => { + const manifest = { + permissions: ['storage'], + host_permissions: ['https://*/*'], + } as chrome.runtime.Manifest; + + expect(() => validateManifest(manifest)).toThrowError(/^Missing `cookies`/); + }); + + test('missing storage', async () => { + const manifest = { + permissions: ['cookies'], + host_permissions: ['https://*/*'], + } as chrome.runtime.Manifest; + + expect(() => validateManifest(manifest)).toThrowError(/^Missing `storage`/); + }); + }); + + describe('host_permissions', () => { + test('should work', async () => { + const manifest = { + permissions: ['cookies', 'storage'], + } as chrome.runtime.Manifest; + + expect(() => validateManifest(manifest)).toThrowError(/^Missing `host_permissions`/); + }); + }); + }); + }); +}); diff --git a/packages/chrome-extension/src/chrome/runtime.ts b/packages/chrome-extension/src/chrome/runtime.ts new file mode 100644 index 00000000000..0c93c764f1a --- /dev/null +++ b/packages/chrome-extension/src/chrome/runtime.ts @@ -0,0 +1,19 @@ +export function validateManifest(manifest: chrome.runtime.Manifest): void { + if (!manifest.permissions) { + throw new Error('Missing `permissions` key in manifest.json'); + } + + if (!manifest.host_permissions) { + throw new Error('Missing `host_permissions` key in manifest.json'); + } + + if (!manifest.permissions.includes('cookies')) { + throw new Error('Missing `cookies` in the `permissions` key in manifest.json'); + } + + if (!manifest.permissions.includes('storage')) { + throw new Error('Missing `storage` in the `permissions` key in manifest.json'); + } + + // TODO: Validate hosts +} diff --git a/packages/chrome-extension/src/chrome/storage.test.ts b/packages/chrome-extension/src/chrome/storage.test.ts new file mode 100644 index 00000000000..5a784ad43de --- /dev/null +++ b/packages/chrome-extension/src/chrome/storage.test.ts @@ -0,0 +1,20 @@ +import { getFromStorage, setInStorage } from './storage'; + +describe('chrome/storage', () => { + afterEach(() => jest.clearAllMocks()); + + describe('setInStorage(key, value)', () => { + test('sets value in chrome.storage', () => { + setInStorage('key', 'value'); + + expect(globalThis.chrome.storage.local.set).toBeCalledTimes(1); + expect(globalThis.chrome.storage.local.set).toBeCalledWith({ key: 'value' }); + }); + }); + + describe('getFromStorage(key)', () => { + test('gets value from chrome.storage', async () => { + await expect(getFromStorage('key')).resolves.toEqual(`storage.get:key`); + }); + }); +}); diff --git a/packages/chrome-extension/src/chrome/storage.ts b/packages/chrome-extension/src/chrome/storage.ts new file mode 100644 index 00000000000..a8f3551ed54 --- /dev/null +++ b/packages/chrome-extension/src/chrome/storage.ts @@ -0,0 +1,9 @@ +export function setInStorage(key: string, value: string) { + return chrome.storage.local.set({ [key]: value }); +} + +export function getFromStorage(key: string) { + return chrome.storage.local.get(key).then(result => { + return result[key]; + }); +} diff --git a/packages/chrome-extension/src/singleton.ts b/packages/chrome-extension/src/singleton.ts index 21caa55bd05..0014df3169b 100644 --- a/packages/chrome-extension/src/singleton.ts +++ b/packages/chrome-extension/src/singleton.ts @@ -1,8 +1,10 @@ import { Clerk } from '@clerk/clerk-js'; import type { ClerkProp } from '@clerk/clerk-react'; +import { CLIENT_JWT_KEY, DEV_BROWSER_JWT_MARKER, parsePublishableKey } from '@clerk/shared'; import type { TokenCache } from './cache'; -import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils'; +import { getClientCookie } from './chrome/cookies'; +import { validateManifest } from './chrome/runtime'; const KEY = '__clerk_client_jwt'; @@ -10,44 +12,63 @@ export let clerk: ClerkProp; type BuildClerkOptions = { publishableKey: string; + syncSessionHost?: string; tokenCache: TokenCache; }; // error handler that logs the error (used in cookie retrieval and token saving) const logErrorHandler = (err: Error) => console.error(err); -export async function buildClerk({ publishableKey, tokenCache }: BuildClerkOptions): Promise { - if (!clerk) { - const clerkFrontendAPIOrigin = convertPublishableKeyToFrontendAPIOrigin(publishableKey); - - const clientCookie = await getClientCookie(clerkFrontendAPIOrigin).catch(logErrorHandler); +export async function buildClerk({ + publishableKey, + tokenCache, + syncSessionHost = 'http://localhost', +}: BuildClerkOptions): Promise { + if (clerk) { + // Return an existing clerk instance + return clerk; + } - // TODO: Listen to client cookie changes and sync updates - // https://developer.chrome.com/docs/extensions/reference/cookies/#event-onChanged + // Will throw if manifest is invalid + validateManifest(chrome.runtime.getManifest()); - if (clientCookie) { - await tokenCache.saveToken(KEY, clientCookie.value).catch(logErrorHandler); - } + const key = parsePublishableKey(publishableKey); - clerk = new Clerk(publishableKey); + if (!key) { + logErrorHandler(new Error('Invalid publishable key')); + return null; + } - // @ts-expect-error - clerk.__unstable__onBeforeRequest(async requestInit => { - requestInit.credentials = 'omit'; - requestInit.url?.searchParams.append('_is_native', '1'); + const clientCookie = await (key.instanceType === 'production' + ? getClientCookie(key.frontendApi, CLIENT_JWT_KEY) + : getClientCookie(syncSessionHost, DEV_BROWSER_JWT_MARKER) + ).catch(logErrorHandler); - const jwt = await tokenCache.getToken(KEY); - (requestInit.headers as Headers).set('authorization', jwt || ''); - }); + // TODO: Listen to client cookie changes and sync updates + // https://developer.chrome.com/docs/extensions/reference/cookies/#event-onChanged - // @ts-expect-error - clerk.__unstable__onAfterResponse(async (_, response) => { - const authHeader = response.headers.get('authorization'); - if (authHeader) { - await tokenCache.saveToken(KEY, authHeader); - } - }); + if (clientCookie) { + await tokenCache.saveToken(KEY, clientCookie.value).catch(logErrorHandler); } + clerk = new Clerk(publishableKey) as ClerkProp; + + // @ts-expect-error - Clerk doesn't expose this unstable method + clerk.__unstable__onBeforeRequest(async requestInit => { + requestInit.credentials = 'omit'; + requestInit.url?.searchParams.append('_is_native', '1'); + + const jwt = await tokenCache.getToken(KEY); + (requestInit.headers as Headers).set('authorization', jwt || ''); + }); + + // @ts-expect-error - Clerk doesn't expose this unstable method + clerk.__unstable__onAfterResponse(async (_, response) => { + const authHeader = response?.headers.get('authorization'); + if (authHeader) { + await tokenCache.saveToken(KEY, authHeader); + } + }); + return clerk; } diff --git a/packages/chrome-extension/src/utils.test.ts b/packages/chrome-extension/src/utils.test.ts deleted file mode 100644 index d0c1ff69e1a..00000000000 --- a/packages/chrome-extension/src/utils.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie, getFromStorage, setInStorage } from './utils'; - -describe('utils', () => { - const _chrome = globalThis.chrome; - - beforeAll(() => { - globalThis.chrome = { - storage: { - // @ts-ignore - local: { set: jest.fn(), get: jest.fn(k => Promise.resolve({ [k]: `storage.get:${k}` })) }, - }, - // @ts-ignore - cookies: { get: jest.fn(({ url, name }) => `cookies.get:${url}:${name}`) }, - }; - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - globalThis.chrome = _chrome; - }); - - describe('convertPublishableKeyToFrontendAPIOrigin(key)', () => { - test('returns FAPI domain for production', () => { - const livePk = 'pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk'; - - expect(convertPublishableKeyToFrontendAPIOrigin(livePk)).toEqual('https://example.clerk.accounts.dev'); - }); - - test('returns FAPI domain for development', () => { - const devPk = 'pk_test_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk'; - - expect(convertPublishableKeyToFrontendAPIOrigin(devPk)).toEqual('https://example.clerk.accounts.dev'); - }); - - test('returns FAPI domain for invalid key', () => { - const invalidPk = 'pk_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk'; - - const errMsg = 'The string to be decoded contains invalid characters.'; - expect(() => convertPublishableKeyToFrontendAPIOrigin(invalidPk)).toThrowError(errMsg); - }); - }); - - describe('getClientCookie(url)', () => { - test('returns cookie value from chrome.cookies if is set for url', async () => { - const url = 'http://localhost:3000'; - await expect(getClientCookie(url)).resolves.toEqual(`cookies.get:${url}:__client`); - }); - }); - - describe('setInStorage(key, value)', () => { - test('sets value in chrome.storage', async () => { - await setInStorage('key', 'value'); - - expect(globalThis.chrome.storage.local.set).toBeCalledTimes(1); - expect(globalThis.chrome.storage.local.set).toBeCalledWith({ key: 'value' }); - }); - }); - - describe('getFromStorage(key)', () => { - test('gets value from chrome.storage', async () => { - await expect(getFromStorage('key')).resolves.toEqual(`storage.get:key`); - }); - }); -}); diff --git a/packages/chrome-extension/src/utils.ts b/packages/chrome-extension/src/utils.ts deleted file mode 100644 index c627450b5c7..00000000000 --- a/packages/chrome-extension/src/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function convertPublishableKeyToFrontendAPIOrigin(key = '') { - return `https://${atob(key.replace(/pk_(test|live)_/, '')).slice(0, -1)}`; -} - -export async function getClientCookie(url: string) { - return chrome.cookies.get({ url, name: '__client' }); -} - -export function setInStorage(key: string, value: string) { - return chrome.storage.local.set({ [key]: value }); -} - -export function getFromStorage(key: string) { - return chrome.storage.local.get(key).then(result => { - return result[key]; - }); -} diff --git a/packages/chrome-extension/tsconfig.lint.json b/packages/chrome-extension/tsconfig.lint.json new file mode 100644 index 00000000000..41f511c28c8 --- /dev/null +++ b/packages/chrome-extension/tsconfig.lint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "./*.ts"] +} diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index bf3fc978401..9febf9a924f 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -16,3 +16,4 @@ export const STAGING_ENV_SUFFIXES = ['.accountsstage.dev']; export const LOCAL_API_URL = 'https://api.lclclerk.com'; export const STAGING_API_URL = 'https://api.clerkstage.dev'; export const PROD_API_URL = 'https://api.clerk.com'; +export const CLIENT_JWT_KEY = '__client';