Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/astro-keyless-cli-init-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/astro': minor
'@clerk/shared': patch
---

In development, missing Clerk keys no longer activate keyless mode. When `PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` are not set, the SDK now fails with an error directing you to run `npx clerk@latest init`, which provisions a Clerk application and writes the keys to `.env`. Existing apps with configured or claimed keys are unaffected.
7 changes: 7 additions & 0 deletions .changeset/keyless-leftover-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/shared': minor
'@clerk/nextjs': patch
'@clerk/backend': minor
---

Remove leftover keyless-mode creation code now that no SDK mints keyless applications. `@clerk/shared/keyless` drops `resolveKeysWithKeylessFallback`, `getOrCreateKeys`, and related exports (internal APIs consumed only by Clerk SDKs); `@clerk/backend` removes the experimental `createAccountlessApplication` method; `@clerk/nextjs` deletes the unused keyless cookie reader and dead keyless middleware parameters, and logs a pointer to existing `.clerk/.tmp/keyless.json` keys when env keys are missing.
5 changes: 5 additions & 0 deletions .changeset/nuxt-keyless-cli-init-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/nuxt': minor
---

In development, missing Clerk keys no longer activate keyless mode. When `NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `NUXT_CLERK_SECRET_KEY` are not set, the SDK now fails with an error directing you to run `npx clerk@latest init`, which provisions a Clerk application and writes the keys to `.env`. Existing apps with configured or claimed keys are unaffected.
5 changes: 5 additions & 0 deletions .changeset/react-router-keyless-cli-init-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/react-router': minor
---

In development, missing Clerk keys no longer activate keyless mode. When `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` are not set, the SDK now fails with an error directing you to run `npx clerk@latest init`, which provisions a Clerk application and writes the keys to `.env`. Existing apps with configured or claimed keys are unaffected.
5 changes: 5 additions & 0 deletions .changeset/tanstack-keyless-cli-init-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/tanstack-react-start': minor
---

In development, missing Clerk keys no longer activate keyless mode. When `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` are not set, the SDK now fails with an error directing you to run `npx clerk@latest init`, which provisions a Clerk application and writes the keys to `.env`. Existing apps with configured or claimed keys are unaffected.
2 changes: 1 addition & 1 deletion integration/scripts/waitForServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type WaitForServerArgsType = {
acceptAnyResponse?: boolean;
};

// Poll a url until it returns a 200 status code
// Poll a url until it returns 2xx (or any HTTP response when acceptAnyResponse is set)
export const waitForServer = async (url: string, opts: WaitForServerArgsType) => {
const { log, delayInMs = 1000, maxAttempts = 20, shouldExit = () => false, acceptAnyResponse = false } = opts;
let attempts = 0;
Expand Down
100 changes: 1 addition & 99 deletions integration/testUtils/keylessHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type { BrowserContext, Page } from '@playwright/test';
import { expect } from '@playwright/test';

import type { Application } from '../models/application';
import { createTestUtils } from './index';
import type { Page } from '@playwright/test';

/**
* Mocks the environment API call to return a claimed instance.
Expand All @@ -22,97 +18,3 @@ export const mockClaimedInstanceEnvironmentCall = async (page: Page): Promise<vo
await route.fulfill({ response, json: newJson });
});
};

/**
* Tests that the keyless popover can be toggled and the claim link opens the dashboard.
*/
export async function testToggleCollapsePopoverAndClaim({
page,
context,
app,
dashboardUrl,
framework,
}: {
page: Page;
context: BrowserContext;
app: Application;
dashboardUrl: string;
framework: string;
}): Promise<void> {
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.po.keylessPopover.waitForMounted();

const claim = u.po.keylessPopover.promptsToClaim();

const href = await claim.getAttribute('href');
expect(href).toBeTruthy();

const claimUrl = new URL(href!);
expect(claimUrl.origin + '/').toBe(dashboardUrl);
expect(claimUrl.pathname).toBe('/apps/claim');
expect(claimUrl.searchParams.get('framework')).toBe(framework);
expect(claimUrl.searchParams.has('token')).toBe(true);
expect(claimUrl.searchParams.has('return_url')).toBe(true);
}

/**
* Tests that a claimed application with missing explicit keys shows the popover expanded
* with a prompt to get keys from the dashboard.
*/
export async function testClaimedAppWithMissingKeys({
page,
context,
app,
dashboardUrl,
}: {
page: Page;
context: BrowserContext;
app: Application;
dashboardUrl: string;
}): Promise<void> {
await mockClaimedInstanceEnvironmentCall(page);
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();

await u.po.keylessPopover.waitForMounted();
expect(await u.po.keylessPopover.isExpanded()).toBe(true);
await expect(u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible();

const href = await u.po.keylessPopover.promptToUseClaimedKeys().getAttribute('href');
expect(href).toBeTruthy();
expect(href).toContain(dashboardUrl);
}

/**
* Tests that the keyless popover is removed after adding keys to .env and restarting the dev server.
*/
export async function testKeylessRemovedAfterEnvAndRestart({
page,
context,
app,
}: {
page: Page;
context: BrowserContext;
app: Application;
}): Promise<void> {
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();

await u.po.keylessPopover.waitForMounted();

// Copy keys from keyless.json to .env
await app.keylessToEnv();

// Restart the dev server to pick up new env vars (Vite doesn't hot-reload .env)
await app.restart();

await u.page.goToAppHome();

// Keyless popover should no longer be present since we now have explicit keys
await u.po.keylessPopover.waitForUnmounted();
}
59 changes: 39 additions & 20 deletions integration/tests/astro/keyless.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { test } from '@playwright/test';
import * as path from 'node:path';

import { expect, test } from '@playwright/test';

import type { Application } from '../../models/application';
import { appConfigs } from '../../presets';
import {
testClaimedAppWithMissingKeys,
testKeylessRemovedAfterEnvAndRestart,
testToggleCollapsePopoverAndClaim,
} from '../../testUtils/keylessHelpers';
import { fs } from '../../scripts';
import { createTestUtils } from '../../testUtils';

const commonSetup = appConfigs.astro.node.clone();

Expand All @@ -21,34 +20,54 @@ test.describe('Keyless mode @astro', () => {
});

let app: Application;
let dashboardUrl = 'https://dashboard.clerk.com/';

test.beforeAll(async () => {
app = await commonSetup.commit();
await app.setup();
await app.withEnv(appConfigs.envs.withKeyless);
if (appConfigs.envs.withKeyless.privateVariables.get('CLERK_API_URL')?.includes('clerkstage')) {
dashboardUrl = 'https://dashboard.clerkstage.dev/';
}
await app.dev();
// Without keys the app 500s on every request, so readiness can't wait for a 2xx
await app.dev({ acceptAnyResponse: true });
});

test.afterAll(async () => {
await app?.teardown();
});

test('Toggle collapse popover and claim.', async ({ page, context }) => {
await testToggleCollapsePopoverAndClaim({ page, context, app, dashboardUrl, framework: 'astro' });
});

test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({
test('Without keys, requests fail with the missing env vars error instead of keyless bootstrap.', async ({
page,
context,
}) => {
await testClaimedAppWithMissingKeys({ page, context, app, dashboardUrl });
const response = await page.goto(`${app.serverUrl}/`);
expect(response?.status()).toBe(500);
// The Astro dev error overlay renders inside shadow DOM, which page.content() does not
// include — locators pierce open shadow roots.
await expect(page.getByText('Publishable key is missing').first()).toBeVisible();
await expect(page.getByText('npx clerk@latest init').first()).toBeVisible();
});

test('Keyless popover is removed after adding keys to .env and restarting.', async ({ page, context }) => {
await testKeylessRemovedAfterEnvAndRestart({ page, context, app });
test('Claimed application with keys inside .env boots and serves the app.', async ({ page, context }) => {
/**
* Seed claimed keyless state directly: the SDK no longer mints keys, so write the
* keys fixture to `.clerk/.tmp/keyless.json` and configure the matching environment
* (keys AND api url, so the server-side onboarding-completion call targets the right
* instance). The completion request itself is BAPI-bound from the server, invisible
* to Playwright — its logic is covered by packages/astro keyless unit tests.
*/
const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY');
await fs.ensureDir(path.join(app.appDir, '.clerk', '.tmp'));
await fs.writeJSON(path.join(app.appDir, '.clerk', '.tmp', 'keyless.json'), {
publishableKey,
secretKey,
claimUrl: 'https://dashboard.clerk.com/apps/claim',
apiKeysUrl: 'https://dashboard.clerk.com/last-active?path=api-keys',
});
await app.withEnv(appConfigs.envs.withEmailCodes);
// Restart the dev server to pick up new env vars (Vite doesn't hot-reload .env)
await app.restart();

const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();
});
});
58 changes: 37 additions & 21 deletions integration/tests/nuxt/keyless.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { test } from '@playwright/test';
import * as path from 'node:path';

import { expect, test } from '@playwright/test';

import type { Application } from '../../models/application';
import { appConfigs } from '../../presets';
import {
testClaimedAppWithMissingKeys,
testKeylessRemovedAfterEnvAndRestart,
testToggleCollapsePopoverAndClaim,
} from '../../testUtils/keylessHelpers';
import { fs } from '../../scripts';
import { createTestUtils } from '../../testUtils';

const commonSetup = appConfigs.nuxt.node.clone();

Expand All @@ -21,35 +20,52 @@ test.describe('Keyless mode @nuxt', () => {
});

let app: Application;
let dashboardUrl = 'https://dashboard.clerk.com/';

test.beforeAll(async () => {
app = await commonSetup.commit();
await app.setup();
await app.withEnv(appConfigs.envs.withKeyless);
if (appConfigs.envs.withKeyless.privateVariables.get('CLERK_API_URL')?.includes('clerkstage')) {
dashboardUrl = 'https://dashboard.clerkstage.dev/';
}
await app.dev();
// Without keys the app 500s on every request, so readiness can't wait for a 2xx
await app.dev({ acceptAnyResponse: true });
});

test.afterAll(async () => {
// Keep files for debugging
await app?.teardown();
});

test('Toggle collapse popover and claim.', async ({ page, context }) => {
await testToggleCollapsePopoverAndClaim({ page, context, app, dashboardUrl, framework: 'nuxt' });
});

test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({
test('Without keys, requests fail with the missing env vars error instead of keyless bootstrap.', async ({
page,
context,
}) => {
await testClaimedAppWithMissingKeys({ page, context, app, dashboardUrl });
const response = await page.goto(`${app.serverUrl}/`);
expect(response?.status()).toBe(500);
await expect(page.getByText('Publishable key is missing').first()).toBeVisible();
await expect(page.getByText('npx clerk@latest init').first()).toBeVisible();
});

test('Keyless popover is removed after adding keys to .env and restarting.', async ({ page, context }) => {
await testKeylessRemovedAfterEnvAndRestart({ page, context, app });
test('Claimed application with keys inside .env boots and serves the app.', async ({ page, context }) => {
/**
* Seed claimed keyless state directly: the SDK no longer mints keys, so write the
* keys fixture to `.clerk/.tmp/keyless.json` and configure the matching environment
* (keys AND api url, so the server-side onboarding-completion call targets the right
* instance). The completion request itself is BAPI-bound from the server, invisible
* to Playwright — its logic is covered by packages/nuxt keyless unit tests.
*/
const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY');
await fs.ensureDir(path.join(app.appDir, '.clerk', '.tmp'));
await fs.writeJSON(path.join(app.appDir, '.clerk', '.tmp', 'keyless.json'), {
publishableKey,
secretKey,
claimUrl: 'https://dashboard.clerk.com/apps/claim',
apiKeysUrl: 'https://dashboard.clerk.com/last-active?path=api-keys',
});
await app.withEnv(appConfigs.envs.withEmailCodes);
// Restart the dev server to pick up new env vars
await app.restart();

const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();
});
});
Loading
Loading