Skip to content

Commit 76eb474

Browse files
Forward arch, platform, and user-agent information with POST requests to /accountless_applications
1 parent ce5617d commit 76eb474

File tree

5 files changed

+76
-58
lines changed

5 files changed

+76
-58
lines changed

.changeset/strong-chicken-lie.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clerk/backend": patch
3+
"@clerk/nextjs": patch
4+
---
5+
6+
feat(nextjs): Forward user-agent, arch, platform, and npm config with POST requests to /accountless_applications

packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ import { AbstractAPI } from './AbstractApi';
55
const basePath = '/accountless_applications';
66

77
export class AccountlessApplicationAPI extends AbstractAPI {
8-
public async createAccountlessApplication(headers: Record<string, string> = {}) {
8+
public async createAccountlessApplication(headers: Headers | undefined) {
9+
const headerParams = headers instanceof Headers ? Object.fromEntries(headers.entries()) : undefined;
910
return this.request<AccountlessApplication>({
1011
method: 'POST',
1112
path: basePath,
12-
headerParams: headers,
13+
headerParams,
1314
});
1415
}
1516

16-
public async completeAccountlessApplicationOnboarding(headers: Record<string, string> = {}) {
17+
public async completeAccountlessApplicationOnboarding(headers: Headers | undefined) {
18+
const headerParams = headers instanceof Headers ? Object.fromEntries(headers.entries()) : undefined;
19+
1720
return this.request<AccountlessApplication>({
1821
method: 'POST',
1922
path: joinPaths(basePath, 'complete'),
20-
headerParams: headers,
23+
headerParams,
2124
});
2225
}
2326

packages/nextjs/src/app-router/server/keyless-provider.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { PropsWithChildren } from 'react';
55
import React from 'react';
66

77
import { createClerkClientWithOptions } from '../../server/createClerkClient';
8+
import { collectKeylessMetadata, formatMetadataHeaders } from '../../server/keyless-custom-headers';
89
import type { NextClerkProviderProps } from '../../types';
910
import { canUseKeyless } from '../../utils/feature-flags';
1011
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
@@ -96,18 +97,17 @@ export const KeylessProvider = async (props: KeylessProviderProps) => {
9697
* Notifying the dashboard the should runs once. We are controlling this behaviour by caching the result of the request.
9798
* If the request fails, it will be considered stale after 10 minutes, otherwise it is cached for 24 hours.
9899
*/
99-
// Collect telemetry data
100-
let telemetryHeaders: Record<string, string> = {};
100+
// Collect metadata
101+
let keylessHeaders: Headers = new Headers();
101102
try {
102-
const { collectKeylessTelemetry, formatTelemetryHeaders } = await import('../../server/telemetry.js');
103-
const telemetry = await collectKeylessTelemetry().catch(() => ({}));
104-
telemetryHeaders = formatTelemetryHeaders(telemetry);
103+
const metadata = await collectKeylessMetadata();
104+
keylessHeaders = formatMetadataHeaders(metadata);
105105
} catch {
106-
// Ignore telemetry errors
106+
// noop
107107
}
108108

109109
await clerkDevelopmentCache?.run(
110-
() => client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding(telemetryHeaders),
110+
() => client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding(keylessHeaders),
111111
{
112112
cacheKey: `${newOrReadKeys.publishableKey}_complete`,
113113
onSuccessStale: 24 * 60 * 60 * 1000, // 24 hours
@@ -130,7 +130,7 @@ export const KeylessProvider = async (props: KeylessProviderProps) => {
130130

131131
const KeylessCookieSync = await import('../client/keyless-cookie-sync.js').then(mod => mod.KeylessCookieSync);
132132

133-
const headerStore = headers();
133+
const headerStore = await headers();
134134
/**
135135
* Allow developer to return to local application after claiming
136136
*/

packages/nextjs/src/server/keyless-custom-headers.ts

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,64 +8,73 @@ interface MetadataHeaders {
88
}
99

1010
/**
11-
* Detects the package manager being used
12-
* @returns Package manager type and version
11+
* Configuration for metadata header mapping
1312
*/
14-
function getNpmConfig(): string | undefined {
15-
// Use available environment variables from turbo.json globalEnv
16-
// eslint-disable-next-line
17-
if (process.env.npm_config_user_agent) {
18-
// eslint-disable-next-line
19-
return process.env.npm_config_user_agent; // 'npm/10.9.2 node/v22.16.0 darwin arm64 workspaces/false',
20-
}
13+
const HEADER_MAPPING = {
14+
nodeVersion: 'Clerk-Node-Version',
15+
nextVersion: 'Clerk-Next-Version',
16+
npmConfigUserAgent: 'Clerk-NPM-Config-User-Agent',
17+
userAgent: 'Clerk-Client-User-Agent',
18+
} as const;
2119

22-
// For other package managers, we can't reliably detect them
23-
// without access to their specific environment variables
24-
// This is a simplified approach that focuses on what's available
25-
return undefined;
26-
}
20+
/**
21+
* Environment-based metadata collectors
22+
*/
23+
const METADATA_COLLECTORS = {
24+
nodeVersion: () => process.version,
25+
nextVersion: () => {
26+
try {
27+
// Extract version from process.title: 'next-server (v15.4.5)' -> 'v15.4.5'
28+
const match = process.title.match(/\(v([\d.]+)\)/);
29+
return match ? `v${match[1]}` : process.title;
30+
} catch {
31+
return undefined;
32+
}
33+
},
34+
// eslint-disable-next-line
35+
npmConfigUserAgent: () => process.env.npm_config_user_agent,
36+
} as const;
2737

2838
/**
29-
* Gets the Next.js version from the package
39+
* Safely executes a metadata collector function
3040
*/
31-
function getNextVersion(): string | undefined {
41+
function safeCollect<T>(collector: () => T): T | undefined {
3242
try {
33-
return process.title; // title: 'next-server (v15.4.5)',
43+
return collector();
3444
} catch {
35-
// Fallback to checking process.env or other methods
3645
return undefined;
3746
}
3847
}
3948

49+
/**
50+
* Collects all available metadata from environment and request
51+
*/
4052
export async function collectKeylessMetadata(): Promise<MetadataHeaders> {
41-
const packageManager = getNpmConfig();
42-
43-
// Get client user agent from the incoming request headers
44-
const headerStore = await headers();
45-
const userAgent = headerStore.get('User-Agent') || undefined;
53+
const [headerStore, envMetadata] = await Promise.all([
54+
headers(),
55+
Promise.resolve(
56+
Object.fromEntries(Object.entries(METADATA_COLLECTORS).map(([key, collector]) => [key, safeCollect(collector)])),
57+
),
58+
]);
4659

4760
return {
48-
nodeVersion: process.version,
49-
nextVersion: getNextVersion(),
50-
npmConfigUserAgent: packageManager,
51-
userAgent,
52-
};
61+
...envMetadata,
62+
userAgent: headerStore.get('User-Agent') || undefined,
63+
} as MetadataHeaders;
5364
}
5465

66+
/**
67+
* Converts metadata to HTTP headers using centralized mapping
68+
*/
5569
export function formatMetadataHeaders(metadata: MetadataHeaders): Headers {
5670
const headers = new Headers();
5771

58-
if (metadata.nextVersion) {
59-
headers.set('Clerk-Next-Version', metadata.nextVersion);
60-
}
61-
62-
if (metadata.npmConfigUserAgent) {
63-
headers.set('Clerk-NPM-Config-User-Agent', metadata.npmConfigUserAgent);
64-
}
65-
66-
if (metadata.userAgent) {
67-
headers.set('Clerk-Client-User-Agent', metadata.userAgent);
68-
}
72+
// Use entries to iterate and reduce repetition
73+
Object.entries(metadata).forEach(([key, value]) => {
74+
if (value && key in HEADER_MAPPING) {
75+
headers.set(HEADER_MAPPING[key as keyof typeof HEADER_MAPPING], value);
76+
}
77+
});
6978

7079
return headers;
7180
}

packages/nextjs/src/server/keyless-node.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AccountlessApplication } from '@clerk/backend';
22

33
import { createClerkClientWithOptions } from './createClerkClient';
44
import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './fs/utils';
5+
import { collectKeylessMetadata, formatMetadataHeaders } from './keyless-custom-headers';
56

67
/**
78
* The Clerk-specific directory name.
@@ -135,18 +136,17 @@ async function createOrReadKeyless(): Promise<AccountlessApplication | null> {
135136
*/
136137
const client = createClerkClientWithOptions({});
137138

138-
// Collect telemetry data
139-
let telemetryHeaders: Record<string, string> = {};
139+
// Collect metadata
140+
let keylessHeaders: Headers = new Headers();
140141
try {
141-
const { collectKeylessTelemetry, formatTelemetryHeaders } = await import('./telemetry.js');
142-
const telemetry = await collectKeylessTelemetry().catch(() => ({}));
143-
telemetryHeaders = formatTelemetryHeaders(telemetry);
142+
const metadata = await collectKeylessMetadata();
143+
keylessHeaders = formatMetadataHeaders(metadata);
144144
} catch {
145-
// Ignore telemetry errors
145+
// Continue with empty headers if metadata header collection fails
146146
}
147147

148148
const accountlessApplication = await client.__experimental_accountlessApplications
149-
.createAccountlessApplication(telemetryHeaders)
149+
.createAccountlessApplication(keylessHeaders)
150150
.catch(() => null);
151151

152152
if (accountlessApplication) {

0 commit comments

Comments
 (0)