Skip to content

Commit d8697a7

Browse files
committed
feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836]
1 parent 77a61bc commit d8697a7

File tree

20 files changed

+384
-202
lines changed

20 files changed

+384
-202
lines changed

.changeset/shiny-glasses-switch.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@clerk/chrome-extension': major
3+
'@clerk/clerk-js': minor
4+
'@clerk/shared': minor
5+
---
6+
7+
Expand the ability for `@clerk/chrome-extension` WebSSO to sync with host applications which use URL-based session syncing.
8+
9+
### How to Update
10+
11+
**WebSSO Local Host Permissions:**
12+
13+
Add the following to the top-level `content_scripts` array key in your `manifest.json` file:
14+
```json
15+
{
16+
"matches": ["*://localhost/*"], // URL of your host application
17+
"js": ["src/content.tsx"] // Path to your content script
18+
}
19+
```
20+
21+
**Content Script:**
22+
23+
In order to sync with your host application, you must add the following to your content script to the path specified in the `manifest.json` file above:
24+
25+
```ts
26+
import { ContentScript } from '@clerk/chrome-extension';
27+
28+
ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
29+
```

packages/chrome-extension/src/ClerkProvider.tsx

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
33
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
44
import React from 'react';
55

6-
import type { TokenCache } from './cache';
7-
import { ChromeStorageCache } from './cache';
86
import { buildClerk } from './singleton';
97

108
Clerk.sdkMetadata = {
@@ -16,30 +14,21 @@ __internal__setErrorThrowerOptions({
1614
packageName: '@clerk/chrome-extension',
1715
});
1816

19-
type WebSSOClerkProviderCustomProps =
20-
| {
21-
syncSessionWithTab?: false;
22-
tokenCache?: never;
23-
}
24-
| {
25-
syncSessionWithTab: true;
26-
tokenCache?: TokenCache;
27-
};
17+
type WebSSOClerkProviderCustomProps = {
18+
syncSessionWithTab?: boolean;
19+
};
2820

2921
type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;
3022

3123
const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
32-
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
24+
const { children, ...rest } = props;
3325
const { publishableKey = '' } = props;
3426

3527
const [clerkInstance, setClerkInstance] = React.useState<ClerkProp>(null);
3628

37-
// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
38-
const tokenCache = runtimeTokenCache || ChromeStorageCache;
39-
4029
React.useEffect(() => {
4130
void (async () => {
42-
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
31+
setClerkInstance(await buildClerk({ publishableKey }));
4332
})();
4433
}, []);
4534

@@ -73,14 +62,6 @@ const StandaloneClerkProvider = (props: ClerkReactProviderProps): JSX.Element =>
7362

7463
type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;
7564

76-
export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
77-
const { tokenCache, syncSessionWithTab, ...rest } = props;
78-
return syncSessionWithTab ? (
79-
<WebSSOClerkProvider
80-
{...props}
81-
tokenCache={tokenCache}
82-
/>
83-
) : (
84-
<StandaloneClerkProvider {...rest} />
85-
);
65+
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
66+
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
8667
}

packages/chrome-extension/src/__snapshots__/exports.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
66
"ClerkLoaded",
77
"ClerkLoading",
88
"ClerkProvider",
9+
"ContentScript",
910
"CreateOrganization",
1011
"EmailLinkErrorCode",
1112
"Experimental__Gate",

packages/chrome-extension/src/cache.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { parsePublishableKey } from '@clerk/shared';
2+
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';
3+
4+
import { STORAGE_KEY_CLIENT_JWT } from './constants';
5+
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
6+
import { ChromeStorageCache } from './utils/storage';
7+
8+
export const ContentScript = {
9+
init(publishableKey: string) {
10+
try {
11+
// Ensure we have a publishable key
12+
if (!publishableKey) {
13+
throw new ClerkChromeExtensionError('Missing publishable key.');
14+
}
15+
16+
// Parse the publishable key
17+
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};
18+
19+
// Ensure we have a valid publishable key
20+
if (!frontendApi || !instanceType) {
21+
throw new ClerkChromeExtensionError('Invalid publishable key.');
22+
}
23+
24+
// Ensure we're in a development environment
25+
if (instanceType !== 'development') {
26+
throw new ClerkChromeExtensionError(`
27+
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
28+
Please update your manifest.json to exclude production URLs in content_scripts.
29+
`);
30+
}
31+
32+
// Create an extension sync manager
33+
const extensionSyncManager = createExtensionSyncManager();
34+
35+
// Listen for token update events from other Clerk hosts
36+
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
37+
// Ignore events from other Clerk hosts
38+
if (data.frontendApi !== frontendApi) {
39+
console.log('Received a token update event for a different Clerk host. Ignoring.');
40+
return;
41+
}
42+
43+
const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);
44+
45+
if (data.action === 'set') {
46+
void ChromeStorageCache.set(KEY, data.token);
47+
} else if (data.action === 'remove') {
48+
void ChromeStorageCache.remove(KEY);
49+
}
50+
});
51+
} catch (e) {
52+
logErrorHandler(e as Error);
53+
}
54+
},
55+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// error handler that logs the error (used in cookie retrieval and token saving)
2+
export const logErrorHandler = (err: Error) => console.error(err);
3+
4+
export class ClerkChromeExtensionError extends Error {
5+
clerk: boolean = true;
6+
7+
constructor(message: string) {
8+
super(`[Clerk: Chrome Extension]: ${message}`);
9+
}
10+
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// eslint-disable-next-line import/export
21
export * from '@clerk/clerk-react';
2+
export { ContentScript } from './content';
33

44
// order matters since we want override @clerk/clerk-react ClerkProvider
5-
// eslint-disable-next-line import/export
65
export { ClerkProvider } from './ClerkProvider';
Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
11
import { Clerk } from '@clerk/clerk-js';
22
import type { ClerkProp } from '@clerk/clerk-react';
3+
import { parsePublishableKey } from '@clerk/shared';
34

4-
import type { TokenCache } from './cache';
5-
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';
6-
7-
const KEY = '__clerk_client_jwt';
5+
import { STORAGE_KEY_CLIENT_JWT } from './constants';
6+
import { logErrorHandler } from './errors';
7+
import { getClientCookie } from './utils/cookies';
8+
import { ChromeStorageCache } from './utils/storage';
89

910
export let clerk: ClerkProp;
1011

1112
type BuildClerkOptions = {
1213
publishableKey: string;
13-
tokenCache: TokenCache;
1414
};
1515

16-
// error handler that logs the error (used in cookie retrieval and token saving)
17-
const logErrorHandler = (err: Error) => console.error(err);
16+
export async function buildClerk({ publishableKey }: BuildClerkOptions): Promise<ClerkProp> {
17+
if (clerk) {
18+
return clerk;
19+
}
1820

19-
export async function buildClerk({ publishableKey, tokenCache }: BuildClerkOptions): Promise<ClerkProp> {
20-
if (!clerk) {
21-
const clerkFrontendAPIOrigin = convertPublishableKeyToFrontendAPIOrigin(publishableKey);
21+
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};
2222

23-
const clientCookie = await getClientCookie(clerkFrontendAPIOrigin).catch(logErrorHandler);
23+
if (!frontendApi || !instanceType) {
24+
throw new Error('Invalid publishable key.');
25+
}
2426

25-
// TODO: Listen to client cookie changes and sync updates
26-
// https://developer.chrome.com/docs/extensions/reference/cookies/#event-onChanged
27+
const clientCookie = await getClientCookie(frontendApi).catch(logErrorHandler);
2728

28-
if (clientCookie) {
29-
await tokenCache.saveToken(KEY, clientCookie.value).catch(logErrorHandler);
30-
}
29+
// TODO: Listen to client cookie changes and sync updates
30+
// https://developer.chrome.com/docs/extensions/reference/cookies/#event-onChanged
3131

32-
clerk = new Clerk(publishableKey);
32+
const KEY = ChromeStorageCache.createKey(frontendApi, STORAGE_KEY_CLIENT_JWT);
3333

34-
// @ts-expect-error
35-
clerk.__unstable__onBeforeRequest(async requestInit => {
36-
requestInit.credentials = 'omit';
37-
requestInit.url?.searchParams.append('_is_native', '1');
34+
if (clientCookie) {
35+
await ChromeStorageCache.set(KEY, clientCookie.value).catch(logErrorHandler);
36+
}
3837

39-
const jwt = await tokenCache.getToken(KEY);
40-
(requestInit.headers as Headers).set('authorization', jwt || '');
41-
});
38+
clerk = new Clerk(publishableKey);
4239

43-
// @ts-expect-error
44-
clerk.__unstable__onAfterResponse(async (_, response) => {
45-
const authHeader = response.headers.get('authorization');
46-
if (authHeader) {
47-
await tokenCache.saveToken(KEY, authHeader);
48-
}
49-
});
50-
}
40+
// @ts-expect-error - Clerk doesn't expose this unstable method
41+
clerk.__unstable__onBeforeRequest(async requestInit => {
42+
requestInit.credentials = 'omit';
43+
requestInit.url?.searchParams.append('_is_native', '1');
44+
45+
const jwt = await ChromeStorageCache.get(KEY);
46+
(requestInit.headers as Headers).set('authorization', jwt || '');
47+
});
48+
49+
// @ts-expect-error - Clerk doesn't expose this unstable method
50+
clerk.__unstable__onAfterResponse(async (_, response) => {
51+
const authHeader = response.headers.get('authorization');
52+
if (authHeader) {
53+
await ChromeStorageCache.set(KEY, authHeader);
54+
}
55+
});
5156

5257
return clerk;
5358
}

packages/chrome-extension/src/utils.test.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

packages/chrome-extension/src/utils.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)