Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

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 the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

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:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

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

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

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

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

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

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: 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 }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

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

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

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

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

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

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

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @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 ChromeStorageCache.get(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 ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836] by tmilewski · Pull Request #2249 · clerk/javascript · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

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 the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

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:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

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

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

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

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

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

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: 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 }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

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

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

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

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

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

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

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @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 ChromeStorageCache.get(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 ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836] by tmilewski · Pull Request #2249 · clerk/javascript · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

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 the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

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:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

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

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

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

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

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

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: 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 }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

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

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

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

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

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

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

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @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 ChromeStorageCache.get(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 ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836] by tmilewski · Pull Request #2249 · clerk/javascript · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

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 the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

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:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

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

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

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

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

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

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: 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 }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

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

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

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

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

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

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

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @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 ChromeStorageCache.get(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 ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836] by tmilewski · Pull Request #2249 · clerk/javascript · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

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 the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

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:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

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

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

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

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

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

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: 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 }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

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

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

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

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

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

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

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @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 ChromeStorageCache.get(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 ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836] by tmilewski · Pull Request #2249 · clerk/javascript · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

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 the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

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:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

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

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

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

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

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

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: 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 }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

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

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

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

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

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

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

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @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 ChromeStorageCache.get(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 ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836] by tmilewski · Pull Request #2249 · clerk/javascript · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

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 the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

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:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

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

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

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

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

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

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: 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 }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

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

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

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

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

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

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

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @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 ChromeStorageCache.get(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 ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading