Skip to content

Commit

Permalink
feat(nextjs): Support Keyless mode (#4602)
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef authored Dec 5, 2024
1 parent e7abe51 commit 3f64080
Show file tree
Hide file tree
Showing 46 changed files with 673 additions and 64 deletions.
7 changes: 7 additions & 0 deletions .changeset/dry-cats-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/backend': minor
---

New **experimental** API: `AccountlessApplicationAPI`

Inside `clerkClient` you can activate this new API through `__experimental_accountlessApplications`. It allows you to generate an "accountless" application and the API returns the publishable key, secret key, and an URL as a response. The URL allows a user to claim this application with their account. Hence the name "accountless" because in its initial state the application is not attached to any account yet.
10 changes: 10 additions & 0 deletions .changeset/grumpy-camels-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@clerk/clerk-react': minor
---

Various internal changes have been made to support a new feature called "Keyless mode". You'll be able to use this feature with Next.js and `@clerk/nextjs` initially. Read the `@clerk/nextjs` changelog to learn more.

List of changes:
- A new internal prop called `__internal_bypassMissingPublishableKey` has been added. Normally an error is thrown when the publishable key is missing, this disables this behavior.
- Loading of `clerk-js` won't be attempted when a missing key is present
- A new instance of `IsomorphicClerk` (an internal Clerk class) is created for each new publishable key
11 changes: 11 additions & 0 deletions .changeset/itchy-cats-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@clerk/nextjs': minor
---

A new **experimental** feature is available: "Keyless mode"

Normally, in order to start a Clerk + Next.js application you need to provide a publishable key and secret key. With "Keyless mode" activated you no longer need to provide these two keys to start your Clerk application. These keys will be automatically generated and the application can be claimed with your account either through a UI prompt or with a URL in your terminal.

**Requirements**:
- You need to use Next.js `14.2.0` or later
- You need to set the environment variable `NEXT_PUBLIC_CLERK_ENABLE_KEYLESS=true`
6 changes: 6 additions & 0 deletions .changeset/warm-spiders-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Replace `__internal_claimAccountlessKeysUrl` with `__internal_claimKeylessApplicationUrl`.
6 changes: 3 additions & 3 deletions integration/tests/next-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'src/app/nested-provider/page.tsx',
() => `import { ClerkProvider } from '@clerk/nextjs';
import { ClientComponent } from './client';
export default function Page() {
return (
<ClerkProvider dynamic>
Expand All @@ -147,10 +147,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
() => `'use client';
import { useAuth } from '@clerk/nextjs';
export function ClientComponent() {
useAuth();
return <p>I am dynamically rendered</p>;
}
`,
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { AccountlessApplication } from '../resources/AccountlessApplication';
import { AbstractAPI } from './AbstractApi';

const basePath = '/accountless_applications';

export class AccountlessApplicationAPI extends AbstractAPI {
public async createAccountlessApplication() {
return this.request<AccountlessApplication>({
method: 'POST',
path: basePath,
});
}
}
1 change: 1 addition & 0 deletions packages/backend/src/api/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './AccountlessApplicationsAPI';
export * from './AbstractApi';
export * from './AllowlistIdentifierApi';
export * from './ClientApi';
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/api/factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AccountlessApplicationAPI,
AllowlistIdentifierAPI,
ClientAPI,
DomainAPI,
Expand All @@ -23,6 +24,9 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
const request = buildRequest(options);

return {
__experimental_accountlessApplications: new AccountlessApplicationAPI(
buildRequest({ ...options, requireSecretKey: false }),
),
allowlistIdentifiers: new AllowlistIdentifierAPI(request),
clients: new ClientAPI(request),
emailAddresses: new EmailAddressAPI(request),
Expand Down
17 changes: 15 additions & 2 deletions packages/backend/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,26 @@ type BuildRequestOptions = {
apiVersion?: string;
/* Library/SDK name */
userAgent?: string;
/**
* Allow requests without specifying a secret key. In most cases this should be set to `false`.
* Defaults to `true`.
*/
requireSecretKey?: boolean;
};
export function buildRequest(options: BuildRequestOptions) {
const requestFn = async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
const { secretKey, apiUrl = API_URL, apiVersion = API_VERSION, userAgent = USER_AGENT } = options;
const {
secretKey,
requireSecretKey = true,
apiUrl = API_URL,
apiVersion = API_VERSION,
userAgent = USER_AGENT,
} = options;
const { path, method, queryParams, headerParams, bodyParams, formData } = requestOptions;

assertValidSecretKey(secretKey);
if (requireSecretKey) {
assertValidSecretKey(secretKey);
}

const url = joinPaths(apiUrl, apiVersion, path);

Expand Down
13 changes: 13 additions & 0 deletions packages/backend/src/api/resources/AccountlessApplication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { AccountlessApplicationJSON } from './JSON';

export class AccountlessApplication {
constructor(
readonly publishableKey: string,
readonly secretKey: string,
readonly claimUrl: string,
) {}

static fromJSON(data: AccountlessApplicationJSON): AccountlessApplication {
return new AccountlessApplication(data.publishable_key, data.secret_key, data.claim_url);
}
}
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Token,
User,
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import type { PaginatedResponseJSON } from './JSON';
import { ObjectType } from './JSON';

Expand Down Expand Up @@ -65,6 +66,8 @@ function jsonToObject(item: any): any {
}

switch (item.object) {
case ObjectType.AccountlessApplication:
return AccountlessApplication.fromJSON(item);
case ObjectType.AllowlistIdentifier:
return AllowlistIdentifier.fromJSON(item);
case ObjectType.Client:
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
} from './Enums';

export const ObjectType = {
AccountlessApplication: 'accountless_application',
AllowlistIdentifier: 'allowlist_identifier',
Client: 'client',
Email: 'email',
Expand Down Expand Up @@ -48,6 +49,13 @@ export interface TokenJSON {
jwt: string;
}

export interface AccountlessApplicationJSON extends ClerkResourceJSON {
object: typeof ObjectType.AccountlessApplication;
publishable_key: string;
secret_key: string;
claim_url: string;
}

export interface AllowlistIdentifierJSON extends ClerkResourceJSON {
object: typeof ObjectType.AllowlistIdentifier;
identifier: string;
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './AccountlessApplication';
export * from './AllowlistIdentifier';
export * from './Client';
export * from './DeletedObject';
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type { VerifyTokenOptions } from './tokens/verify';
* JSON types
*/
export type {
AccountlessApplicationJSON,
ClerkResourceJSON,
TokenJSON,
AllowlistIdentifierJSON,
Expand Down Expand Up @@ -87,6 +88,7 @@ export type {
* Resources
*/
export type {
AccountlessApplication,
AllowlistIdentifier,
Client,
EmailAddress,
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ if (typeof window !== 'undefined') {

global.__PKG_NAME__ = '';
global.__PKG_VERSION__ = '';
global.__BUILD_FLAG_ACCOUNTLESS_UI__ = '';
global.__BUILD_FLAG_KEYLESS_UI__ = '';
global.BUILD_ENABLE_NEW_COMPONENTS = '';

//@ts-expect-error
Expand Down
7 changes: 5 additions & 2 deletions packages/clerk-js/rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ const common = ({ mode, disableRHC = false }) => {
},
plugins: [
new rspack.DefinePlugin({
__BUILD_DISABLE_RHC__: JSON.stringify(disableRHC),
__DEV__: isDevelopment(mode),
__BUILD_FLAG_ACCOUNTLESS_UI__: isDevelopment(mode),
__PKG_VERSION__: JSON.stringify(packageJSON.version),
__PKG_NAME__: JSON.stringify(packageJSON.name),
/**
* Build time feature flags.
*/
__BUILD_FLAG_KEYLESS_UI__: isDevelopment(mode),
__BUILD_DISABLE_RHC__: JSON.stringify(disableRHC),
BUILD_ENABLE_NEW_COMPONENTS: JSON.stringify(process.env.BUILD_ENABLE_NEW_COMPONENTS),
}),
new rspack.EnvironmentPlugin({
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
Clerk.mountWaitlist(app, componentControls.waitlist.getProps() ?? {});
},
'/accountless': () => {
Clerk.__unstable__updateProps({ options: { __internal_claimAccountlessKeysUrl: '/test-url' } });
Clerk.__unstable__updateProps({ options: { __internal_claimKeylessApplicationUrl: '/test-url' } });
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
Expand Down
12 changes: 6 additions & 6 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1899,8 +1899,8 @@ export class Clerk implements ClerkInterface {
void this.#captchaHeartbeat.start();
this.#clearClerkQueryParams();
this.#handleImpersonationFab();
if (__BUILD_FLAG_ACCOUNTLESS_UI__) {
this.#handleAccountlessPrompt();
if (__BUILD_FLAG_KEYLESS_UI__) {
this.#handleKeylessPrompt();
}
return true;
};
Expand Down Expand Up @@ -2031,12 +2031,12 @@ export class Clerk implements ClerkInterface {
});
};

#handleAccountlessPrompt = () => {
if (__BUILD_FLAG_ACCOUNTLESS_UI__) {
#handleKeylessPrompt = () => {
if (__BUILD_FLAG_KEYLESS_UI__) {
void this.#componentControls?.ensureMounted().then(controls => {
if (this.#options.__internal_claimAccountlessKeysUrl) {
if (this.#options.__internal_claimKeylessApplicationUrl) {
controls.updateProps({
options: { __internal_claimAccountlessKeysUrl: this.#options.__internal_claimAccountlessKeysUrl },
options: { __internal_claimKeylessApplicationUrl: this.#options.__internal_claimKeylessApplicationUrl },
});
}
});
Expand Down
5 changes: 4 additions & 1 deletion packages/clerk-js/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
declare global {
const __DEV__: boolean;
const __BUILD_FLAG_ACCOUNTLESS_UI__: boolean;
const __PKG_NAME__: string;
const __PKG_VERSION__: string;
/**
* Build time feature flags.
*/
const __BUILD_FLAG_KEYLESS_UI__: boolean;
const __BUILD_DISABLE_RHC__: string;

interface Window {
Expand Down
8 changes: 4 additions & 4 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import type { AppearanceCascade } from './customizables/parseAppearance';
import { useClerkModalStateParams } from './hooks/useClerkModalStateParams';
import type { ClerkComponentName } from './lazyModules/components';
import {
AccountlessPrompt,
BlankCaptchaModal,
CreateOrganizationModal,
ImpersonationFab,
KeylessPrompt,
OrganizationProfileModal,
preloadComponent,
SignInModal,
Expand Down Expand Up @@ -517,10 +517,10 @@ const Components = (props: ComponentsProps) => {
</LazyImpersonationFabProvider>
)}

{__BUILD_FLAG_ACCOUNTLESS_UI__
? state.options?.__internal_claimAccountlessKeysUrl && (
{__BUILD_FLAG_KEYLESS_UI__
? state.options?.__internal_claimKeylessApplicationUrl && (
<LazyImpersonationFabProvider globalAppearance={state.appearance}>
<AccountlessPrompt url={state.options.__internal_claimAccountlessKeysUrl} />
<KeylessPrompt url={state.options.__internal_claimKeylessApplicationUrl} />
</LazyImpersonationFabProvider>
)
: null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Col, descriptors, Flex, Link, Text } from '../../customizables';
import { Portal } from '../../elements/Portal';
import { InternalThemeProvider, mqu } from '../../styledSystem';

type AccountlessPromptProps = {
type KeylessPromptProps = {
url?: string;
};

Expand Down Expand Up @@ -50,7 +50,7 @@ const FabContent = ({ title, signOutText, url }: FabContentProps) => {
);
};

export const _AccountlessPrompt = (props: AccountlessPromptProps) => {
const _KeylessPrompt = (props: KeylessPromptProps) => {
// const { parsedInternalTheme } = useAppearance();
const containerRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -150,7 +150,7 @@ export const _AccountlessPrompt = (props: AccountlessPromptProps) => {
},
})}
>
🔓Accountless Mode
🔓Keyless Mode
<Flex
id='cl-impersonationText'
sx={t => ({
Expand All @@ -169,8 +169,8 @@ export const _AccountlessPrompt = (props: AccountlessPromptProps) => {
);
};

export const AccountlessPrompt = (props: AccountlessPromptProps) => (
export const KeylessPrompt = (props: KeylessPromptProps) => (
<InternalThemeProvider>
<_AccountlessPrompt {...props} />
<_KeylessPrompt {...props} />
</InternalThemeProvider>
);
10 changes: 5 additions & 5 deletions packages/clerk-js/src/ui/lazyModules/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const componentImportPaths = {
BlankCaptchaModal: () => import(/* webpackChunkName: "blankcaptcha" */ './../components/BlankCaptchaModal'),
UserVerification: () => import(/* webpackChunkName: "userverification" */ './../components/UserVerification'),
Waitlist: () => import(/* webpackChunkName: "waitlist" */ './../components/Waitlist'),
AccountlessPrompt: __BUILD_FLAG_ACCOUNTLESS_UI__
? () => import(/* webpackChunkName: "accountlessPrompt" */ './../components/AccountlessPrompt')
KeylessPrompt: __BUILD_FLAG_KEYLESS_UI__
? () => import(/* webpackChunkName: "keylessPrompt" */ '../components/KeylessPrompt')
: () => null,
} as const;

Expand Down Expand Up @@ -86,9 +86,9 @@ export const BlankCaptchaModal = lazy(() =>
export const ImpersonationFab = lazy(() =>
componentImportPaths.ImpersonationFab().then(module => ({ default: module.ImpersonationFab })),
);
export const AccountlessPrompt = __BUILD_FLAG_ACCOUNTLESS_UI__
? // @ts-expect-error Types are broken due to __BUILD_FLAG_ACCOUNTLESS_UI__
lazy(() => componentImportPaths.AccountlessPrompt().then(module => ({ default: module.AccountlessPrompt })))
export const KeylessPrompt = __BUILD_FLAG_KEYLESS_UI__
? // @ts-expect-error Types are broken due to __BUILD_FLAG_KEYLESS_UI__
lazy(() => componentImportPaths.KeylessPrompt().then(module => ({ default: module.KeylessPrompt })))
: () => null;

export const preloadComponent = async (component: unknown) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/nextjs/package.cjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"#components": {
"react-server": "./components.server.js",
"default": "./components.client.js"
},
"#safe-node-apis": {
"node": "./runtime/node/safe-node-apis.js",
"default": "./runtime/browser/safe-node-apis.js"
}
}
}
4 changes: 4 additions & 0 deletions packages/nextjs/package.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"#components": {
"react-server": "./components.server.js",
"default": "./components.client.js"
},
"#safe-node-apis": {
"node": "./runtime/node/safe-node-apis.js",
"default": "./runtime/browser/safe-node-apis.js"
}
}
}
Loading

0 comments on commit 3f64080

Please sign in to comment.