From 563d24a766d6f567c7a923f6846adce24bbac4e9 Mon Sep 17 00:00:00 2001 From: Heat Hamilton Date: Wed, 13 Aug 2025 14:53:55 -0400 Subject: [PATCH 1/7] Include additional headers for Next.js keyless app creation --- .../src/server/keyless-custom-headers.ts | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/keyless-custom-headers.ts b/packages/nextjs/src/server/keyless-custom-headers.ts index 570ab87d649..da0673f0eaf 100644 --- a/packages/nextjs/src/server/keyless-custom-headers.ts +++ b/packages/nextjs/src/server/keyless-custom-headers.ts @@ -1,10 +1,18 @@ +'use server'; + import { headers } from 'next/headers'; interface MetadataHeaders { nodeVersion?: string; nextVersion?: string; npmConfigUserAgent?: string; - userAgent?: string; + userAgent: string; + port?: string; + host: string; + xHost: string; + xPort: string; + xProtocol: string; + xClerkAuthStatus: string; } /** @@ -17,7 +25,13 @@ export async function collectKeylessMetadata(): Promise { nodeVersion: process.version, nextVersion: getNextVersion(), npmConfigUserAgent: process.env.npm_config_user_agent, // eslint-disable-line - userAgent: headerStore.get('User-Agent') ?? undefined, + userAgent: headerStore.get('User-Agent') ?? 'unknown user-agent', + port: process.env.PORT, // eslint-disable-line + host: headerStore.get('host') ?? 'unknown host', + xPort: headerStore.get('x-forwarded-port') ?? 'unknown x-forwarded-port', + xHost: headerStore.get('x-forwarded-host') ?? 'unknown x-forwarded-host', + xProtocol: headerStore.get('x-forwarded-proto') ?? 'unknown x-forwarded-proto', + xClerkAuthStatus: headerStore.get('x-clerk-auth-status') ?? 'unknown x-clerk-auth-status', }; } @@ -54,5 +68,29 @@ export function formatMetadataHeaders(metadata: MetadataHeaders): Headers { headers.set('Clerk-Client-User-Agent', metadata.userAgent); } + if (metadata.port) { + headers.set('Clerk-Node-Port', metadata.port); + } + + if (metadata.host) { + headers.set('Clerk-Client-host', metadata.host); + } + + if (metadata.xPort) { + headers.set('Clerk-X-Port', metadata.xPort); + } + + if (metadata.xHost) { + headers.set('Clerk-X-Host', metadata.xHost); + } + + if (metadata.xProtocol) { + headers.set('Clerk-X-protocol', metadata.xProtocol); + } + + if (metadata.xClerkAuthStatus) { + headers.set('Clerk-Auth-Status', metadata.xClerkAuthStatus); + } + return headers; } From fdc5c71272d9d4909829cb35a2c89039e75483ae Mon Sep 17 00:00:00 2001 From: Heat Hamilton Date: Wed, 13 Aug 2025 15:04:14 -0400 Subject: [PATCH 2/7] Add changeset --- .changeset/chatty-ways-bathe.md | 5 +++++ packages/nextjs/src/server/keyless-custom-headers.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/chatty-ways-bathe.md diff --git a/.changeset/chatty-ways-bathe.md b/.changeset/chatty-ways-bathe.md new file mode 100644 index 00000000000..0211d584702 --- /dev/null +++ b/.changeset/chatty-ways-bathe.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': major +--- + +Forward port, host, x-forwarded-port, x-forwarded-host, x-forwarded-proto, and x-clerk-auth-status with POST requests to /v1/accountless_applications /v1/accountless_applications/complete diff --git a/packages/nextjs/src/server/keyless-custom-headers.ts b/packages/nextjs/src/server/keyless-custom-headers.ts index da0673f0eaf..ab5b4722738 100644 --- a/packages/nextjs/src/server/keyless-custom-headers.ts +++ b/packages/nextjs/src/server/keyless-custom-headers.ts @@ -73,7 +73,7 @@ export function formatMetadataHeaders(metadata: MetadataHeaders): Headers { } if (metadata.host) { - headers.set('Clerk-Client-host', metadata.host); + headers.set('Clerk-Client-Host', metadata.host); } if (metadata.xPort) { @@ -85,7 +85,7 @@ export function formatMetadataHeaders(metadata: MetadataHeaders): Headers { } if (metadata.xProtocol) { - headers.set('Clerk-X-protocol', metadata.xProtocol); + headers.set('Clerk-X-Protocol', metadata.xProtocol); } if (metadata.xClerkAuthStatus) { From 8fdbbf78289e4118fa4b02a3c76cf36e2358cc4f Mon Sep 17 00:00:00 2001 From: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:27:25 -0400 Subject: [PATCH 3/7] Correct version bump to patch from major --- .changeset/chatty-ways-bathe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chatty-ways-bathe.md b/.changeset/chatty-ways-bathe.md index 0211d584702..a5dce0e8cf9 100644 --- a/.changeset/chatty-ways-bathe.md +++ b/.changeset/chatty-ways-bathe.md @@ -1,5 +1,5 @@ --- -'@clerk/nextjs': major +'@clerk/nextjs': patch --- Forward port, host, x-forwarded-port, x-forwarded-host, x-forwarded-proto, and x-clerk-auth-status with POST requests to /v1/accountless_applications /v1/accountless_applications/complete From acabf46971c49fee675eae5cfeeb37bcf4a03082 Mon Sep 17 00:00:00 2001 From: Heat Hamilton Date: Wed, 13 Aug 2025 16:20:51 -0400 Subject: [PATCH 4/7] Add tests for additional telemetry headers --- .../__tests__/keyless-custom-headers.test.ts | 463 ++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 packages/nextjs/src/__tests__/keyless-custom-headers.test.ts diff --git a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts new file mode 100644 index 00000000000..f199605869e --- /dev/null +++ b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts @@ -0,0 +1,463 @@ +import { headers } from 'next/headers'; +import type { MockedFunction } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { collectKeylessMetadata, formatMetadataHeaders } from '../server/keyless-custom-headers'; + +// Default mock headers for keyless-custom-headers.ts +const defaultMockHeaders = new Headers({ + 'User-Agent': 'Mozilla/5.0 (Test Browser)', + host: 'test-host.example.com', + 'x-forwarded-port': '3000', + 'x-forwarded-host': 'forwarded-test-host.example.com', + 'x-forwarded-proto': 'https', + 'x-clerk-auth-status': 'signed-out', +}); + +// Mock Next.js headers +vi.mock('next/headers', () => ({ + headers: vi.fn(() => ({ + get: vi.fn((name: string) => { + // Return mock values for headers used in keyless-custom-headers.ts + return defaultMockHeaders.get(name); + }), + has: vi.fn((name: string) => { + return defaultMockHeaders.has(name); + }), + forEach: vi.fn((callback: (value: string, key: string) => void) => { + defaultMockHeaders.forEach(callback); + }), + entries: function* () { + const entries: [string, string][] = []; + defaultMockHeaders.forEach((value, key) => entries.push([key, value])); + for (const entry of entries) yield entry; + }, + keys: function* () { + const keys: string[] = []; + defaultMockHeaders.forEach((_, key) => keys.push(key)); + for (const key of keys) yield key; + }, + values: function* () { + const values: string[] = []; + defaultMockHeaders.forEach(value => values.push(value)); + for (const value of values) yield value; + }, + })), +})); + +const mockHeaders = headers as unknown as MockedFunction<() => Promise>; + +// Type for mocking Next.js headers +interface MockHeaders { + get(key: string): string | null; + has(key: string): boolean; + forEach(callback: (value: string, key: string) => void): void; + entries(): IterableIterator<[string, string]>; + keys(): IterableIterator; + values(): IterableIterator; +} + +// Helper function to create custom header mocks for specific tests +function createMockHeaders(customHeaders: Record = {}): MockHeaders { + const defaultHeadersObj: Record = {}; + defaultMockHeaders.forEach((value, key) => { + defaultHeadersObj[key] = value; + }); + const allHeaders = { ...defaultHeadersObj, ...customHeaders }; + + return { + get: vi.fn((name: string) => allHeaders[name] || null), + has: vi.fn((name: string) => Object.prototype.hasOwnProperty.call(allHeaders, name) && allHeaders[name] !== null), + forEach: vi.fn((callback: (value: string, key: string) => void) => { + Object.entries(allHeaders).forEach(([key, value]) => { + if (value !== null) callback(value, key); + }); + }), + entries: vi.fn(() => { + const validEntries: [string, string][] = Object.entries(allHeaders).filter(([, value]) => value !== null) as [ + string, + string, + ][]; + return validEntries[Symbol.iterator](); + }), + keys: vi.fn(() => { + const validKeys = Object.keys(allHeaders).filter(key => allHeaders[key] !== null); + return validKeys[Symbol.iterator](); + }), + values: vi.fn(() => { + const validValues = Object.values(allHeaders).filter(value => value !== null); + return validValues[Symbol.iterator](); + }), + }; +} + +describe('keyless-custom-headers', () => { + beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + mockHeaders.mockReset(); + // Default: use the defaultMockHeaders bag + mockHeaders.mockImplementation(async () => createMockHeaders()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + describe('formatMetadataHeaders', () => { + it('should format complete metadata object with all fields present', () => { + const metadata = { + nodeVersion: 'v18.17.0', + nextVersion: 'next-server (v15.4.5)', + npmConfigUserAgent: 'npm/9.8.1 node/v18.17.0 darwin x64 workspaces/false', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + port: '3000', + host: 'localhost:3000', + xHost: 'example.com', + xPort: '3000', + xProtocol: 'https', + xClerkAuthStatus: 'signed-out', + }; + + const result = formatMetadataHeaders(metadata); + + // Test exact header casing and values + expect(result.get('Clerk-Node-Version')).toBe('v18.17.0'); + expect(result.get('Clerk-Next-Version')).toBe('next-server (v15.4.5)'); + expect(result.get('Clerk-NPM-Config-User-Agent')).toBe('npm/9.8.1 node/v18.17.0 darwin x64 workspaces/false'); + expect(result.get('Clerk-Client-User-Agent')).toBe('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)'); + expect(result.get('Clerk-Node-Port')).toBe('3000'); + expect(result.get('Clerk-Client-Host')).toBe('localhost:3000'); + expect(result.get('Clerk-X-Host')).toBe('example.com'); + expect(result.get('Clerk-X-Port')).toBe('3000'); + expect(result.get('Clerk-X-Protocol')).toBe('https'); + expect(result.get('Clerk-Auth-Status')).toBe('signed-out'); + }); + + it('should handle missing optional fields gracefully', () => { + const metadata = { + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + host: 'localhost:3000', + xHost: 'example.com', + xPort: '3000', + xProtocol: 'https', + xClerkAuthStatus: 'signed-out', + // Missing: nodeVersion, nextVersion, npmConfigUserAgent, port + }; + + const result = 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)'); + expect(result.get('Clerk-Client-Host')).toBe('localhost:3000'); + expect(result.get('Clerk-X-Host')).toBe('example.com'); + expect(result.get('Clerk-X-Port')).toBe('3000'); + expect(result.get('Clerk-X-Protocol')).toBe('https'); + expect(result.get('Clerk-Auth-Status')).toBe('signed-out'); + + // Test that missing fields are not set + expect(result.get('Clerk-Node-Version')).toBeNull(); + expect(result.get('Clerk-Next-Version')).toBeNull(); + expect(result.get('Clerk-NPM-Config-User-Agent')).toBeNull(); + expect(result.get('Clerk-Node-Port')).toBeNull(); + }); + + it('should handle undefined values for optional fields', () => { + const metadata = { + nodeVersion: undefined, + nextVersion: undefined, + npmConfigUserAgent: undefined, + userAgent: 'test-user-agent', + port: undefined, + host: 'test-host', + xHost: 'test-x-host', + xPort: 'test-x-port', + xProtocol: 'test-x-protocol', + xClerkAuthStatus: 'test-auth-status', + }; + + const result = formatMetadataHeaders(metadata); + + // Test that undefined fields are not set + expect(result.get('Clerk-Node-Version')).toBeNull(); + expect(result.get('Clerk-Next-Version')).toBeNull(); + expect(result.get('Clerk-NPM-Config-User-Agent')).toBeNull(); + expect(result.get('Clerk-Node-Port')).toBeNull(); + + // Test that defined fields are set + expect(result.get('Clerk-Client-User-Agent')).toBe('test-user-agent'); + expect(result.get('Clerk-Client-Host')).toBe('test-host'); + expect(result.get('Clerk-X-Host')).toBe('test-x-host'); + expect(result.get('Clerk-X-Port')).toBe('test-x-port'); + expect(result.get('Clerk-X-Protocol')).toBe('test-x-protocol'); + expect(result.get('Clerk-Auth-Status')).toBe('test-auth-status'); + }); + + it('should handle empty string values', () => { + const metadata = { + nodeVersion: '', + nextVersion: '', + npmConfigUserAgent: '', + userAgent: '', + port: '', + host: '', + xHost: '', + xPort: '', + xProtocol: '', + xClerkAuthStatus: '', + }; + + const result = formatMetadataHeaders(metadata); + + // Empty strings should not be set as headers + expect(result.get('Clerk-Node-Version')).toBeNull(); + expect(result.get('Clerk-Next-Version')).toBeNull(); + expect(result.get('Clerk-NPM-Config-User-Agent')).toBeNull(); + expect(result.get('Clerk-Client-User-Agent')).toBeNull(); + expect(result.get('Clerk-Node-Port')).toBeNull(); + expect(result.get('Clerk-Client-Host')).toBeNull(); + expect(result.get('Clerk-X-Host')).toBeNull(); + expect(result.get('Clerk-X-Port')).toBeNull(); + expect(result.get('Clerk-X-Protocol')).toBeNull(); + expect(result.get('Clerk-Auth-Status')).toBeNull(); + }); + }); + + describe('collectKeylessMetadata', () => { + it('should use default mock headers when no custom headers are specified', async () => { + // Setup environment variables + vi.stubEnv('PORT', '3000'); + vi.stubEnv('npm_config_user_agent', 'npm/9.8.1 node/v18.17.0 darwin x64'); + + // Mock process.version and process.title + const originalVersion = process.version; + const originalTitle = process.title; + Object.defineProperty(process, 'version', { value: 'v18.17.0', configurable: true }); + Object.defineProperty(process, 'title', { value: 'next-server (v15.4.5)', configurable: true }); + + const result = await collectKeylessMetadata(); + + // Should use the default mock headers + expect(result.userAgent).toBe('Mozilla/5.0 (Test Browser)'); + expect(result.host).toBe('test-host.example.com'); + expect(result.xPort).toBe('3000'); + expect(result.xHost).toBe('forwarded-test-host.example.com'); + expect(result.xProtocol).toBe('https'); + expect(result.xClerkAuthStatus).toBe('signed-out'); + + // Should use environment variables and process info + expect(result.nodeVersion).toBe('v18.17.0'); + expect(result.nextVersion).toBe('next-server (v15.4.5)'); + expect(result.npmConfigUserAgent).toBe('npm/9.8.1 node/v18.17.0 darwin x64'); + expect(result.port).toBe('3000'); + + // Restore original values + Object.defineProperty(process, 'version', { value: originalVersion, configurable: true }); + Object.defineProperty(process, 'title', { value: originalTitle, configurable: true }); + }); + + it('should collect metadata with all fields present', async () => { + // Setup environment variables + vi.stubEnv('PORT', '3000'); + vi.stubEnv('npm_config_user_agent', 'npm/9.8.1 node/v18.17.0 darwin x64'); + + // Mock process.version and process.title + const originalVersion = process.version; + const originalTitle = process.title; + Object.defineProperty(process, 'version', { value: 'v18.17.0', configurable: true }); + Object.defineProperty(process, 'title', { value: 'next-server (v15.4.5)', configurable: true }); + + // Mock headers + const mockHeaderStore = new Headers({ + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + host: 'localhost:3000', + 'x-forwarded-port': '3000', + 'x-forwarded-host': 'example.com', + 'x-forwarded-proto': 'https', + 'x-clerk-auth-status': 'signed-out', + }); + + mockHeaders.mockResolvedValue({ + get: (key: string) => mockHeaderStore.get(key) || null, + has: (key: string) => mockHeaderStore.has(key), + forEach: () => {}, + entries: function* () { + const headerEntries: [string, string][] = []; + mockHeaderStore.forEach((value, key) => headerEntries.push([key, value])); + for (const entry of headerEntries) { + yield entry; + } + }, + keys: function* () { + const headerKeys: string[] = []; + mockHeaderStore.forEach((_, key) => headerKeys.push(key)); + for (const key of headerKeys) { + yield key; + } + }, + values: function* () { + const headerValues: string[] = []; + mockHeaderStore.forEach(value => headerValues.push(value)); + for (const value of headerValues) { + yield value; + } + }, + } as MockHeaders); + + const result = await collectKeylessMetadata(); + + expect(result).toEqual({ + nodeVersion: 'v18.17.0', + nextVersion: 'next-server (v15.4.5)', + npmConfigUserAgent: 'npm/9.8.1 node/v18.17.0 darwin x64', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + port: '3000', + host: 'localhost:3000', + xPort: '3000', + xHost: 'example.com', + xProtocol: 'https', + xClerkAuthStatus: 'signed-out', + }); + + // Restore original values + Object.defineProperty(process, 'version', { value: originalVersion, configurable: true }); + Object.defineProperty(process, 'title', { value: originalTitle, configurable: true }); + }); + + it('should use fallback values when headers are missing', async () => { + // Clear environment variables + vi.stubEnv('PORT', undefined); + vi.stubEnv('npm_config_user_agent', undefined); + + // Mock empty headers using createMockHeaders helper with all null values + mockHeaders.mockResolvedValue( + createMockHeaders({ + 'User-Agent': null, + host: null, + 'x-forwarded-port': null, + 'x-forwarded-host': null, + 'x-forwarded-proto': null, + 'x-clerk-auth-status': null, + }), + ); + + const result = await collectKeylessMetadata(); + + expect(result.userAgent).toBe('unknown user-agent'); + expect(result.host).toBe('unknown host'); + expect(result.xPort).toBe('unknown x-forwarded-port'); + expect(result.xHost).toBe('unknown x-forwarded-host'); + expect(result.xProtocol).toBe('unknown x-forwarded-proto'); + expect(result.xClerkAuthStatus).toBe('unknown x-clerk-auth-status'); + expect(result.port).toBeUndefined(); + expect(result.npmConfigUserAgent).toBeUndefined(); + }); + + it('should handle process.title extraction errors gracefully', async () => { + // Mock process.title to throw an error + const originalTitle = process.title; + Object.defineProperty(process, 'title', { + get: () => { + throw new Error('Process title access error'); + }, + configurable: true, + }); + + mockHeaders.mockResolvedValue({ + get: () => null, + has: () => false, + forEach: () => {}, + entries: function* () {}, + keys: function* () {}, + values: function* () {}, + } as MockHeaders); + + const result = await collectKeylessMetadata(); + + expect(result.nextVersion).toBeUndefined(); + + // Restore original value + Object.defineProperty(process, 'title', { value: originalTitle, configurable: true }); + }); + + it('should demonstrate partial header overrides with createMockHeaders', async () => { + // Only override specific headers, keeping defaults for others + mockHeaders.mockResolvedValue( + createMockHeaders({ + 'User-Agent': 'Partial-Override-Agent/2.0', + 'x-clerk-auth-status': 'signed-out', + // Other headers will use default values from defaultMockHeaders + }), + ); + + const result = await collectKeylessMetadata(); + + // Overridden headers + expect(result.userAgent).toBe('Partial-Override-Agent/2.0'); + expect(result.xClerkAuthStatus).toBe('signed-out'); + + // Default headers (unchanged) + expect(result.host).toBe('test-host.example.com'); + expect(result.xPort).toBe('3000'); + expect(result.xHost).toBe('forwarded-test-host.example.com'); + expect(result.xProtocol).toBe('https'); + }); + }); + + it('should format metadata collected from collectKeylessMetadata correctly', async () => { + // Setup environment + vi.stubEnv('PORT', '4000'); + vi.stubEnv('npm_config_user_agent', 'test-npm-agent'); + + const mockHeaderStore = new Headers({ + 'User-Agent': 'Integration-Test-Agent', + host: 'localhost:4000', + 'x-forwarded-port': '4000', + 'x-forwarded-host': 'integration-forwarded-host', + 'x-forwarded-proto': 'https', + 'x-clerk-auth-status': 'integration-status', + }); + + mockHeaders.mockResolvedValue({ + get: (key: string) => mockHeaderStore.get(key) || null, + has: (key: string) => mockHeaderStore.has(key), + forEach: () => {}, + entries: function* () { + const headerEntries: [string, string][] = []; + mockHeaderStore.forEach((value, key) => headerEntries.push([key, value])); + for (const entry of headerEntries) { + yield entry; + } + }, + keys: function* () { + const headerKeys: string[] = []; + mockHeaderStore.forEach((_, key) => headerKeys.push(key)); + for (const key of headerKeys) { + yield key; + } + }, + values: function* () { + const headerValues: string[] = []; + mockHeaderStore.forEach(value => headerValues.push(value)); + for (const value of headerValues) { + yield value; + } + }, + } as MockHeaders); + + // Collect metadata and format headers + const metadata = await collectKeylessMetadata(); + const headers = formatMetadataHeaders(metadata); + + // Verify the full pipeline works correctly + expect(headers.get('Clerk-Client-User-Agent')).toBe('Integration-Test-Agent'); + expect(headers.get('Clerk-Client-Host')).toBe('localhost:4000'); + expect(headers.get('Clerk-Node-Port')).toBe('4000'); + expect(headers.get('Clerk-X-Port')).toBe('4000'); + expect(headers.get('Clerk-X-Host')).toBe('integration-forwarded-host'); + expect(headers.get('Clerk-X-Protocol')).toBe('https'); + expect(headers.get('Clerk-Auth-Status')).toBe('integration-status'); + expect(headers.get('Clerk-NPM-Config-User-Agent')).toBe('test-npm-agent'); + }); +}); From 1386278327bf7e6223efbb0e747938ce723526ac Mon Sep 17 00:00:00 2001 From: Heat Hamilton Date: Wed, 13 Aug 2025 16:33:30 -0400 Subject: [PATCH 5/7] Align mock type with actual usage (sync | async) for next/headers.headers --- .../nextjs/src/__tests__/keyless-custom-headers.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts index f199605869e..5d7423999d4 100644 --- a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts +++ b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts @@ -45,7 +45,8 @@ vi.mock('next/headers', () => ({ })), })); -const mockHeaders = headers as unknown as MockedFunction<() => Promise>; +type MockHeadersFn = () => MockHeaders | Promise; +const mockHeaders = headers as unknown as MockedFunction; // Type for mocking Next.js headers interface MockHeaders { @@ -66,7 +67,7 @@ function createMockHeaders(customHeaders: Record = {}): M const allHeaders = { ...defaultHeadersObj, ...customHeaders }; return { - get: vi.fn((name: string) => allHeaders[name] || null), + get: vi.fn((name: string) => allHeaders[name] ?? null), has: vi.fn((name: string) => Object.prototype.hasOwnProperty.call(allHeaders, name) && allHeaders[name] !== null), forEach: vi.fn((callback: (value: string, key: string) => void) => { Object.entries(allHeaders).forEach(([key, value]) => { @@ -93,16 +94,14 @@ function createMockHeaders(customHeaders: Record = {}): M describe('keyless-custom-headers', () => { beforeEach(() => { - // Reset all mocks before each test vi.clearAllMocks(); - mockHeaders.mockReset(); - // Default: use the defaultMockHeaders bag mockHeaders.mockImplementation(async () => createMockHeaders()); }); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); + mockHeaders.mockReset(); }); describe('formatMetadataHeaders', () => { From 164d097a3ce6b2c9f58b2f32b0c115c97a6e7aaa Mon Sep 17 00:00:00 2001 From: Heat Hamilton Date: Wed, 20 Aug 2025 10:28:23 -0400 Subject: [PATCH 6/7] Fix failing test --- .../src/__tests__/keyless-custom-headers.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts index 5d7423999d4..1f21d3f8e9f 100644 --- a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts +++ b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts @@ -67,8 +67,17 @@ function createMockHeaders(customHeaders: Record = {}): M const allHeaders = { ...defaultHeadersObj, ...customHeaders }; return { - get: vi.fn((name: string) => allHeaders[name] ?? null), - has: vi.fn((name: string) => Object.prototype.hasOwnProperty.call(allHeaders, name) && allHeaders[name] !== null), + get: vi.fn((name: string) => { + // Use the defaultMockHeaders.get() method for consistent behavior + const defaultValue = defaultMockHeaders.get(name); + const customValue = customHeaders[name]; + return customValue !== undefined ? customValue : defaultValue; + }), + has: vi.fn((name: string) => { + const hasDefault = defaultMockHeaders.has(name); + const hasCustom = Object.prototype.hasOwnProperty.call(customHeaders, name); + return hasDefault || (hasCustom && customHeaders[name] !== null); + }), forEach: vi.fn((callback: (value: string, key: string) => void) => { Object.entries(allHeaders).forEach(([key, value]) => { if (value !== null) callback(value, key); From 59097924532005f6829f53ecb3d0d9cd9116be3f Mon Sep 17 00:00:00 2001 From: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:27:00 -0400 Subject: [PATCH 7/7] Update .changeset/chatty-ways-bathe.md Co-authored-by: Bryce Kalow --- .changeset/chatty-ways-bathe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chatty-ways-bathe.md b/.changeset/chatty-ways-bathe.md index a5dce0e8cf9..80afd1a9049 100644 --- a/.changeset/chatty-ways-bathe.md +++ b/.changeset/chatty-ways-bathe.md @@ -2,4 +2,4 @@ '@clerk/nextjs': patch --- -Forward port, host, x-forwarded-port, x-forwarded-host, x-forwarded-proto, and x-clerk-auth-status with POST requests to /v1/accountless_applications /v1/accountless_applications/complete +Forward additional debugging data when creating Keyless applications.