Skip to content

Commit

Permalink
Merge branch 'main' into rob/eco-276-re-export-vue-sdk-components-and…
Browse files Browse the repository at this point in the history
…-composables-in-the-nuxt-sdk
  • Loading branch information
wobsoriano authored Dec 11, 2024
2 parents d84435d + 12f92e9 commit f7b0566
Show file tree
Hide file tree
Showing 20 changed files with 105 additions and 8 deletions.
7 changes: 7 additions & 0 deletions .changeset/gold-monkeys-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/nextjs': patch
'@clerk/types': patch
---

Introduce `__internal_copyInstanceKeysUrl` as property in `ClerkOptions`. It is intented for internall usage from other Clerk SDKs and will be used in Keyless mode.
5 changes: 5 additions & 0 deletions .changeset/hungry-fishes-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/astro": patch
---

Fix handshake redirect loop in Netlify deployments
6 changes: 6 additions & 0 deletions .changeset/slow-cars-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/types': patch
---

Add `claimedAt` proprty inside AuthConfig for the environment. It describes when a instance that was created from the Keyless mode was finally claimed.
5 changes: 5 additions & 0 deletions .changeset/twelve-spiders-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': patch
---

Add property `api_keys_url` in the `AccountlessApplication` class
5 changes: 4 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
'page',
`
${command === 'dev' ? `console.log("${packageName}","Initialize Clerk: page")` : ''}
import { runInjectionScript, swapDocument } from "${buildImportPath}";
import { removeNetlifyCacheBustParam, runInjectionScript, swapDocument } from "${buildImportPath}";
// Fix an issue with infinite redirect in Netlify and Clerk dev instance
removeNetlifyCacheBustParam();
// Taken from https://github.com/withastro/astro/blob/e10b03e88c22592fbb42d7245b65c4f486ab736d/packages/astro/src/transitions/router.ts#L39.
// Importing it directly from astro:transitions/client breaks custom client-side routing
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export { runInjectionScript };

export { generateSafeId } from './utils/generateSafeId';
export { swapDocument } from './swap-document';
export { NETLIFY_CACHE_BUST_PARAM, removeNetlifyCacheBustParam } from './remove-query-param';
13 changes: 13 additions & 0 deletions packages/astro/src/internal/remove-query-param.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const NETLIFY_CACHE_BUST_PARAM = '__netlify_clerk_cache_bust';

/**
* Removes the temporary cache bust parameter that prevents infinite redirects
* in Netlify and Clerk's dev instance.
*/
export function removeNetlifyCacheBustParam() {
const url = new URL(window.location.href);
if (url.searchParams.has(NETLIFY_CACHE_BUST_PARAM)) {
url.searchParams.delete(NETLIFY_CACHE_BUST_PARAM);
window.history.replaceState(window.history.state, '', url);
}
}
24 changes: 23 additions & 1 deletion packages/astro/src/server/clerk-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AuthObject, ClerkClient } from '@clerk/backend';
import type { AuthenticateRequestOptions, ClerkRequest, RedirectFun, RequestState } from '@clerk/backend/internal';
import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isDevelopmentFromPublishableKey, isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps } from '@clerk/shared/proxy';
import { eventMethodCalled } from '@clerk/shared/telemetry';
import { handleValueOrFn } from '@clerk/shared/utils';
Expand All @@ -10,6 +10,7 @@ import type { APIContext } from 'astro';
// @ts-ignore
import { authAsyncStorage } from '#async-local-storage';

import { NETLIFY_CACHE_BUST_PARAM } from '../internal';
import { buildClerkHotloadScript } from './build-clerk-hotload-script';
import { clerkClient } from './clerk-client';
import { createCurrentUser } from './current-user';
Expand Down Expand Up @@ -83,6 +84,8 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {

const locationHeader = requestState.headers.get(constants.Headers.Location);
if (locationHeader) {
handleNetlifyCacheInDevInstance(locationHeader, requestState);

const res = new Response(null, { status: 307, headers: requestState.headers });
return decorateResponseWithObservabilityHeaders(res, requestState);
} else if (requestState.status === AuthStatus.Handshake) {
Expand Down Expand Up @@ -234,6 +237,25 @@ Check if signInUrl is missing from your configuration or if it is not an absolut
PUBLIC_CLERK_SIGN_IN_URL='SOME_URL'
PUBLIC_CLERK_IS_SATELLITE='true'`;

/**
* Prevents infinite redirects in Netlify's functions
* by adding a cache bust parameter to the original redirect URL. This ensures Netlify
* doesn't serve a cached response during the authentication flow.
*/
function handleNetlifyCacheInDevInstance(locationHeader: string, requestState: RequestState) {
// Only run on Netlify environment and Clerk development instance
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (import.meta.env.NETLIFY && isDevelopmentFromPublishableKey(requestState.publishableKey)) {
const hasHandshakeQueryParam = locationHeader.includes('__clerk_handshake');
// If location header is the original URL before the handshake redirects, add cache bust param
if (!hasHandshakeQueryParam) {
const url = new URL(locationHeader);
url.searchParams.append(NETLIFY_CACHE_BUST_PARAM, Date.now().toString());
requestState.headers.set('Location', url.toString());
}
}
}

function decorateAstroLocal(clerkRequest: ClerkRequest, context: APIContext, requestState: RequestState) {
const { reason, message, status, token } = requestState;
context.locals.authToken = token;
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/api/resources/AccountlessApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ export class AccountlessApplication {
readonly publishableKey: string,
readonly secretKey: string,
readonly claimUrl: string,
readonly apiKeysUrl: string,
) {}

static fromJSON(data: AccountlessApplicationJSON): AccountlessApplication {
return new AccountlessApplication(data.publishable_key, data.secret_key, data.claim_url);
return new AccountlessApplication(data.publishable_key, data.secret_key, data.claim_url, data.api_keys_url);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface AccountlessApplicationJSON extends ClerkResourceJSON {
publishable_key: string;
secret_key: string;
claim_url: string;
api_keys_url: string;
}

export interface AllowlistIdentifierJSON extends ClerkResourceJSON {
Expand Down
5 changes: 4 additions & 1 deletion packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2052,7 +2052,10 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => {
if (this.#options.__internal_claimKeylessApplicationUrl) {
controls.updateProps({
options: { __internal_claimKeylessApplicationUrl: this.#options.__internal_claimKeylessApplicationUrl },
options: {
__internal_claimKeylessApplicationUrl: this.#options.__internal_claimKeylessApplicationUrl,
__internal_copyInstanceKeysUrl: this.#options.__internal_copyInstanceKeysUrl,
},
});
}
});
Expand Down
3 changes: 3 additions & 0 deletions packages/clerk-js/src/core/resources/AuthConfig.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { AuthConfigJSON, AuthConfigResource } from '@clerk/types';

import { unixEpochToDate } from '../../utils/date';
import { BaseResource } from './internal';

export class AuthConfig extends BaseResource implements AuthConfigResource {
singleSessionMode!: boolean;
claimedAt: Date | null = null;

public constructor(data: AuthConfigJSON) {
super();
Expand All @@ -12,6 +14,7 @@ export class AuthConfig extends BaseResource implements AuthConfigResource {

protected fromJSON(data: AuthConfigJSON | null): this {
this.singleSessionMode = data ? data.single_session_mode : true;
this.claimedAt = data?.claimed_at ? unixEpochToDate(data.claimed_at) : null;
return this;
}
}
8 changes: 6 additions & 2 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -518,9 +518,13 @@ const Components = (props: ComponentsProps) => {
)}

{__BUILD_FLAG_KEYLESS_UI__
? state.options?.__internal_claimKeylessApplicationUrl && (
? state.options?.__internal_claimKeylessApplicationUrl &&
state.options?.__internal_copyInstanceKeysUrl && (
<LazyImpersonationFabProvider globalAppearance={state.appearance}>
<KeylessPrompt url={state.options.__internal_claimKeylessApplicationUrl} />
<KeylessPrompt
claimUrl={state.options.__internal_claimKeylessApplicationUrl}
copyKeysUrl={state.options.__internal_copyInstanceKeysUrl}
/>
</LazyImpersonationFabProvider>
)
: null}
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useClerk } from '@clerk/shared/react';
// eslint-disable-next-line no-restricted-imports
import { css } from '@emotion/react';
import { useState } from 'react';
Expand All @@ -9,12 +10,14 @@ import { ClerkLogoIcon } from './ClerkLogoIcon';
import { KeySlashIcon } from './KeySlashIcon';

type KeylessPromptProps = {
url?: string;
claimUrl: string;
copyKeysUrl: string;
};

const _KeylessPrompt = (_props: KeylessPromptProps) => {
const [isExpanded, setIsExpanded] = useState(true);
const handleFocus = () => setIsExpanded(true);
const clerk = useClerk();

return (
<Portal>
Expand Down Expand Up @@ -310,6 +313,9 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
type='button'
onFocus={handleFocus}
data-expanded={isExpanded}
onClick={() => {
void clerk.navigate(_props.claimUrl);
}}
css={css`
position: absolute;
right: 0.375rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const KeylessCreatorOrReader = (props: NextClerkProviderProps) => {
key: state?.publishableKey,
publishableKey: state?.publishableKey,
__internal_claimKeylessApplicationUrl: state?.claimUrl,
__internal_copyInstanceKeysUrl: state?.apiKeysUrl,
__internal_bypassMissingPublishableKey: true,
} as any);
};
3 changes: 2 additions & 1 deletion packages/nextjs/src/app-router/keyless-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function createOrReadKeylessAction(): Promise<null | Omit<Accountle
return null;
}

const { claimUrl, publishableKey, secretKey } = result;
const { claimUrl, publishableKey, secretKey, apiKeysUrl } = result;

void (await cookies()).set(getKeylessCookieName(), JSON.stringify({ claimUrl, publishableKey, secretKey }), {
secure: false,
Expand All @@ -40,5 +40,6 @@ export async function createOrReadKeylessAction(): Promise<null | Omit<Accountle
return {
claimUrl,
publishableKey,
apiKeysUrl,
};
}
1 change: 1 addition & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export async function ClerkProvider(
...rest,
publishableKey: newOrReadKeys.publishableKey,
__internal_claimKeylessApplicationUrl: newOrReadKeys.claimUrl,
__internal_copyInstanceKeysUrl: newOrReadKeys.apiKeysUrl,
})}
nonce={await nonce}
initialState={await statePromise}
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/authConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ export interface AuthConfigResource extends ClerkResource {
* Enabled single session configuration at the instance level.
*/
singleSessionMode: boolean;
/**
* Timestamp of when the instance was claimed. This only applies to applications created with the Keyless mode.
* Defaults to `null`.
*/
claimedAt: Date | null;
}
8 changes: 8 additions & 0 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,8 +753,16 @@ export type ClerkOptions = ClerkOptionsNavigation &
Record<string, any>
>;

/**
* The URL a developer should be redirected to in order to claim an instance created on Keyless mode.
*/
__internal_claimKeylessApplicationUrl?: string;

/**
* After a developer has claimed their instance created by Keyless mode, they can use this URL to find their instance's keys
*/
__internal_copyInstanceKeysUrl?: string;

/**
* [EXPERIMENTAL] Provide the underlying host router, required for the new experimental UI components.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export interface SessionWithActivitiesJSON extends Omit<SessionJSON, 'user'> {
export interface AuthConfigJSON extends ClerkResourceJSON {
single_session_mode: boolean;
url_based_session_syncing: boolean;
claimed_at: number | null;
}

export interface VerificationJSON extends ClerkResourceJSON {
Expand Down

0 comments on commit f7b0566

Please sign in to comment.