diff --git a/.changeset/dull-forks-agree.md b/.changeset/dull-forks-agree.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/dull-forks-agree.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/integration/templates/tanstack-react-start/package.json b/integration/templates/tanstack-react-start/package.json index 7e86c3cb0ab..bf45f3efb8d 100644 --- a/integration/templates/tanstack-react-start/package.json +++ b/integration/templates/tanstack-react-start/package.json @@ -13,7 +13,6 @@ "@tanstack/react-start": "1.132.51", "react": "18.3.1", "react-dom": "18.3.1", - "srvx": "0.8.15", "tailwind-merge": "^2.5.4" }, "devDependencies": { diff --git a/packages/backend/src/tokens/__tests__/clerkRequest.test.ts b/packages/backend/src/tokens/__tests__/clerkRequest.test.ts index 18c5c72ece0..35a5625afe0 100644 --- a/packages/backend/src/tokens/__tests__/clerkRequest.test.ts +++ b/packages/backend/src/tokens/__tests__/clerkRequest.test.ts @@ -204,4 +204,39 @@ describe('createClerkRequest', () => { expect(json.cookies).toBe('{}'); }); }); + + describe('duck typing detection (instanceof workaround)', () => { + it('should create a new ClerkRequest from a regular Request', () => { + const regularRequest = new Request('http://localhost:3000'); + const clerkRequest = createClerkRequest(regularRequest); + + expect(clerkRequest).not.toBe(regularRequest); + expect(clerkRequest.clerkUrl).toBeDefined(); + expect(clerkRequest.cookies).toBeDefined(); + }); + + it('should return an existing ClerkRequest instance unchanged', () => { + const firstClerkRequest = createClerkRequest(new Request('http://localhost:3000')); + const secondClerkRequest = createClerkRequest(firstClerkRequest); + + expect(secondClerkRequest).toBe(firstClerkRequest); + }); + + it('should work correctly with bundler-scoped Request classes', () => { + // Simulate bundler creating a scoped Request class (like Request$1) + class RequestScoped extends Request { + constructor(input: RequestInfo | URL, init?: RequestInit) { + super(input, init); + } + } + + const scopedRequest = new RequestScoped('http://localhost:3000'); + const clerkRequest = createClerkRequest(scopedRequest); + + // Should create a new ClerkRequest even though scopedRequest is a different Request class + expect(clerkRequest).not.toBe(scopedRequest); + expect(clerkRequest.clerkUrl).toBeDefined(); + expect(clerkRequest.cookies).toBeDefined(); + }); + }); }); diff --git a/packages/backend/src/tokens/clerkRequest.ts b/packages/backend/src/tokens/clerkRequest.ts index f35b079d779..89ab5e6bc6d 100644 --- a/packages/backend/src/tokens/clerkRequest.ts +++ b/packages/backend/src/tokens/clerkRequest.ts @@ -82,7 +82,12 @@ class ClerkRequest extends Request { } export const createClerkRequest = (...args: ConstructorParameters): ClerkRequest => { - return args[0] instanceof ClerkRequest ? args[0] : new ClerkRequest(...args); + // Use duck typing instead of instanceof to avoid issues with polyfilled Request classes + // (e.g., in TanStack Start or other environments with multiple Request class instances) + // ClerkRequest has unique properties 'clerkUrl' and 'cookies' that distinguish it from Request + const isClerkRequest = args[0] && typeof args[0] === 'object' && 'clerkUrl' in args[0] && 'cookies' in args[0]; + + return isClerkRequest ? (args[0] as ClerkRequest) : new ClerkRequest(...args); }; export type { ClerkRequest }; diff --git a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts index 48cc8cb896e..7be75419d7a 100644 --- a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts +++ b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts @@ -166,7 +166,7 @@ describe('keyless-custom-headers', () => { }); describe('formatMetadataHeaders', () => { - it('should format complete metadata object with all fields present', () => { + it('should format complete metadata object with all fields present', async () => { const metadata = { nodeVersion: 'v18.17.0', nextVersion: 'next-server (v15.4.5)', @@ -181,7 +181,7 @@ describe('keyless-custom-headers', () => { isCI: false, }; - const result = formatMetadataHeaders(metadata); + const result = await formatMetadataHeaders(metadata); // Test exact header casing and values expect(result.get('Clerk-Node-Version')).toBe('v18.17.0'); @@ -196,7 +196,7 @@ describe('keyless-custom-headers', () => { expect(result.get('Clerk-Auth-Status')).toBe('signed-out'); }); - it('should handle missing optional fields gracefully', () => { + it('should handle missing optional fields gracefully', async () => { const metadata = { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', host: 'localhost:3000', @@ -208,7 +208,7 @@ describe('keyless-custom-headers', () => { // Missing: nodeVersion, nextVersion, npmConfigUserAgent, port }; - const result = formatMetadataHeaders(metadata); + const result = await formatMetadataHeaders(metadata); // Test that only present fields are set expect(result.get('Clerk-Client-User-Agent')).toBe('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)'); @@ -225,7 +225,7 @@ describe('keyless-custom-headers', () => { expect(result.get('Clerk-Node-Port')).toBeNull(); }); - it('should handle undefined values for optional fields', () => { + it('should handle undefined values for optional fields', async () => { const metadata = { nodeVersion: undefined, nextVersion: undefined, @@ -240,7 +240,7 @@ describe('keyless-custom-headers', () => { isCI: false, }; - const result = formatMetadataHeaders(metadata); + const result = await formatMetadataHeaders(metadata); // Test that undefined fields are not set expect(result.get('Clerk-Node-Version')).toBeNull(); @@ -257,7 +257,7 @@ describe('keyless-custom-headers', () => { expect(result.get('Clerk-Auth-Status')).toBe('test-auth-status'); }); - it('should handle empty string values', () => { + it('should handle empty string values', async () => { const metadata = { nodeVersion: '', nextVersion: '', @@ -272,7 +272,7 @@ describe('keyless-custom-headers', () => { isCI: false, }; - const result = formatMetadataHeaders(metadata); + const result = await formatMetadataHeaders(metadata); // Empty strings should not be set as headers expect(result.get('Clerk-Node-Version')).toBeNull(); @@ -513,7 +513,7 @@ describe('keyless-custom-headers', () => { // Collect metadata and format headers const metadata = await collectKeylessMetadata(); - const headers = formatMetadataHeaders(metadata); + const headers = await formatMetadataHeaders(metadata); // Verify the full pipeline works correctly expect(headers.get('Clerk-Client-User-Agent')).toBe('Integration-Test-Agent'); diff --git a/packages/nextjs/src/server/keyless-custom-headers.ts b/packages/nextjs/src/server/keyless-custom-headers.ts index ff016d028e7..73ca03837b6 100644 --- a/packages/nextjs/src/server/keyless-custom-headers.ts +++ b/packages/nextjs/src/server/keyless-custom-headers.ts @@ -99,7 +99,7 @@ function getNextVersion(): string | undefined { /** * Converts metadata to HTTP headers */ -export function formatMetadataHeaders(metadata: MetadataHeaders): Headers { +export async function formatMetadataHeaders(metadata: MetadataHeaders): Promise { const headers = new Headers(); if (metadata.nodeVersion) {