Skip to content

Commit c728888

Browse files
lindesvardclaude
andcommitted
feat(api): allow client id/secret in /track JSON body for Zaraz (#316)
Cloudflare Zaraz can't set custom request headers on outbound HTTP, so \`openpanel-client-id\` / \`openpanel-client-secret\` couldn't reach our auth extractor. \`validateSdkRequest\` now falls back to the same fields on the JSON body when the headers are missing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7d35e2e commit c728888

2 files changed

Lines changed: 31 additions & 22 deletions

File tree

apps/api/src/routes/track.router.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ const trackRouter: FastifyPluginAsyncZodOpenApi = async (fastify) => {
1515
method: 'POST',
1616
url: '/',
1717
schema: {
18-
body: zTrackHandlerPayload,
18+
body: zTrackHandlerPayload.and(
19+
z.object({
20+
clientId: z.string().optional(),
21+
clientSecret: z.string().optional(),
22+
})
23+
),
1924
tags: ['Track'],
2025
description:
2126
'Ingest a tracking event (track, identify, group, increment, decrement, replay).',

apps/api/src/utils/auth.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { FastifyRequest, RawRequestDefaultExpression } from 'fastify';
2-
31
import { verifyPassword } from '@openpanel/common/server';
42
import type { IServiceClientWithProject } from '@openpanel/db';
53
import { ClientType, getClientByIdCached } from '@openpanel/db';
@@ -10,6 +8,7 @@ import type {
108
IProjectFilterProfileId,
119
ITrackHandlerPayload,
1210
} from '@openpanel/validation';
11+
import type { FastifyRequest, RawRequestDefaultExpression } from 'fastify';
1312
import { path } from 'ramda';
1413

1514
const cleanDomain = (domain: string) =>
@@ -31,7 +30,7 @@ export class SdkAuthError extends Error {
3130
clientId?: string;
3231
clientSecret?: string;
3332
origin?: string;
34-
},
33+
}
3534
) {
3635
super(message);
3736
this.name = 'SdkAuthError';
@@ -43,15 +42,21 @@ export class SdkAuthError extends Error {
4342
export async function validateSdkRequest(
4443
req: FastifyRequest<{
4544
Body: ITrackHandlerPayload | DeprecatedPostEventPayload;
46-
}>,
45+
}>
4746
): Promise<IServiceClientWithProject> {
4847
const { headers, clientIp } = req;
4948
const clientIdNew = headers['openpanel-client-id'] as string;
5049
const clientIdOld = headers['mixan-client-id'] as string;
5150
const clientSecretNew = headers['openpanel-client-secret'] as string;
5251
const clientSecretOld = headers['mixan-client-secret'] as string;
53-
const clientId = clientIdNew || clientIdOld;
54-
const clientSecret = clientSecretNew || clientSecretOld;
52+
const clientIdFromBody = path<string | undefined>(['clientId'], req.body);
53+
const clientSecretFromBody = path<string | undefined>(
54+
['clientSecret'],
55+
req.body
56+
);
57+
const clientId = clientIdNew || clientIdOld || clientIdFromBody;
58+
const clientSecret =
59+
clientSecretNew || clientSecretOld || clientSecretFromBody;
5560
const origin = headers.origin;
5661

5762
const createError = (message: string) =>
@@ -70,7 +75,7 @@ export async function validateSdkRequest(
7075

7176
if (
7277
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
73-
clientId,
78+
clientId
7479
)
7580
) {
7681
throw createError('Ingestion: Client ID must be a valid UUIDv4');
@@ -88,15 +93,15 @@ export async function validateSdkRequest(
8893

8994
// Filter out blocked IPs
9095
const ipFilter = client.project.filters.filter(
91-
(filter): filter is IProjectFilterIp => filter.type === 'ip',
96+
(filter): filter is IProjectFilterIp => filter.type === 'ip'
9297
);
9398
if (ipFilter.some((filter) => filter.ip === clientIp)) {
9499
throw createError('Ingestion: IP address is blocked by project filter');
95100
}
96101

97102
// Filter out blocked profile ids
98103
const profileFilter = client.project.filters.filter(
99-
(filter): filter is IProjectFilterProfileId => filter.type === 'profile_id',
104+
(filter): filter is IProjectFilterProfileId => filter.type === 'profile_id'
100105
);
101106
const profileId =
102107
path<string | undefined>(['payload', 'profileId'], req.body) || // Track handler
@@ -113,12 +118,11 @@ export async function validateSdkRequest(
113118
// Only allow revenue tracking if it was sent with a client secret
114119
// or if the project has allowUnsafeRevenueTracking enabled
115120
if (
116-
!client.project.allowUnsafeRevenueTracking &&
117-
!clientSecret &&
121+
!(client.project.allowUnsafeRevenueTracking || clientSecret) &&
118122
typeof revenue !== 'undefined'
119123
) {
120124
throw createError(
121-
'Ingestion: Revenue tracking is not allowed without a client secret',
125+
'Ingestion: Revenue tracking is not allowed without a client secret'
122126
);
123127
}
124128

@@ -132,7 +136,7 @@ export async function validateSdkRequest(
132136
// support wildcard domains `*.foo.com`
133137
if (cleanedDomain.includes('*')) {
134138
const regex = new RegExp(
135-
`${cleanedDomain.replaceAll('.', '\\.').replaceAll('*', '.+?')}`,
139+
`${cleanedDomain.replaceAll('.', '\\.').replaceAll('*', '.+?')}`
136140
);
137141

138142
return regex.test(origin || '');
@@ -157,7 +161,7 @@ export async function validateSdkRequest(
157161
`client:auth:${clientId}:${Buffer.from(clientSecret).toString('base64')}`,
158162
60 * 5,
159163
async () => await verifyPassword(clientSecret, client.secret!),
160-
true,
164+
true
161165
);
162166
if (isVerified) {
163167
return client;
@@ -168,14 +172,14 @@ export async function validateSdkRequest(
168172
}
169173

170174
export async function validateExportRequest(
171-
headers: RawRequestDefaultExpression['headers'],
175+
headers: RawRequestDefaultExpression['headers']
172176
): Promise<IServiceClientWithProject> {
173177
const clientId = headers['openpanel-client-id'] as string;
174178
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
175179

176180
if (
177181
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
178-
clientId,
182+
clientId
179183
)
180184
) {
181185
throw new Error('Export: Client ID must be a valid UUIDv4');
@@ -203,14 +207,14 @@ export async function validateExportRequest(
203207
}
204208

205209
export async function validateImportRequest(
206-
headers: RawRequestDefaultExpression['headers'],
210+
headers: RawRequestDefaultExpression['headers']
207211
): Promise<IServiceClientWithProject> {
208212
const clientId = headers['openpanel-client-id'] as string;
209213
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
210214

211215
if (
212216
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
213-
clientId,
217+
clientId
214218
)
215219
) {
216220
throw new Error('Import: Client ID must be a valid UUIDv4');
@@ -238,14 +242,14 @@ export async function validateImportRequest(
238242
}
239243

240244
export async function validateManageRequest(
241-
headers: RawRequestDefaultExpression['headers'],
245+
headers: RawRequestDefaultExpression['headers']
242246
): Promise<IServiceClientWithProject> {
243247
const clientId = headers['openpanel-client-id'] as string;
244248
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
245249

246250
if (
247251
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
248-
clientId,
252+
clientId
249253
)
250254
) {
251255
throw new Error('Manage: Client ID must be a valid UUIDv4');
@@ -263,7 +267,7 @@ export async function validateManageRequest(
263267

264268
if (client.type !== ClientType.root) {
265269
throw new Error(
266-
'Manage: Only root clients are allowed to manage resources',
270+
'Manage: Only root clients are allowed to manage resources'
267271
);
268272
}
269273

0 commit comments

Comments
 (0)